GitHubBackupSettings.provider.test.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. /**
  2. * Tests for the provider selection UI in GitHubBackupSettings.
  3. */
  4. import { describe, it, expect, vi, beforeEach } from 'vitest';
  5. import { fireEvent, screen, waitFor } from '@testing-library/react';
  6. import { render } from '../utils';
  7. import { GitHubBackupSettings } from '../../components/GitHubBackupSettings';
  8. import { http, HttpResponse } from 'msw';
  9. import { server } from '../mocks/server';
  10. const baseHandlers = () => [
  11. http.get('/api/v1/github-backup/config', () => HttpResponse.json(null)),
  12. http.get('/api/v1/github-backup/status', () =>
  13. HttpResponse.json({
  14. configured: false,
  15. enabled: false,
  16. is_running: false,
  17. progress: null,
  18. last_backup_at: null,
  19. last_backup_status: null,
  20. next_scheduled_run: null,
  21. })
  22. ),
  23. http.get('/api/v1/github-backup/logs', () => HttpResponse.json([])),
  24. http.get('/api/v1/local-backup/status', () =>
  25. HttpResponse.json({
  26. enabled: false,
  27. schedule: 'daily',
  28. time: '03:00',
  29. retention: 5,
  30. path: '',
  31. default_path: '/data/backups',
  32. is_running: false,
  33. last_backup_at: null,
  34. last_status: null,
  35. last_message: null,
  36. next_run: null,
  37. })
  38. ),
  39. http.get('/api/v1/local-backup/backups', () => HttpResponse.json([])),
  40. http.get('/api/v1/cloud/status', () => HttpResponse.json({ is_authenticated: false })),
  41. http.get('/api/v1/printers', () => HttpResponse.json([])),
  42. http.put('/api/v1/settings/', () => HttpResponse.json({})),
  43. ];
  44. describe('GitHubBackupSettings - Provider Selection', () => {
  45. beforeEach(() => {
  46. vi.clearAllMocks();
  47. server.use(...baseHandlers());
  48. });
  49. it('renders provider dropdown with GitHub selected by default', async () => {
  50. render(<GitHubBackupSettings />);
  51. await waitFor(() => {
  52. expect(screen.getByText('Git Provider')).toBeInTheDocument();
  53. });
  54. const select = screen.getByRole('combobox', { name: /git provider/i });
  55. expect(select).toHaveValue('github');
  56. });
  57. it('does not show instance URL field for any provider', async () => {
  58. render(<GitHubBackupSettings />);
  59. await waitFor(() => {
  60. expect(screen.getByText('Git Provider')).toBeInTheDocument();
  61. });
  62. expect(screen.queryByText('Instance URL')).not.toBeInTheDocument();
  63. });
  64. it('loads provider from existing config', async () => {
  65. server.use(
  66. http.get('/api/v1/github-backup/config', () =>
  67. HttpResponse.json({
  68. id: 1,
  69. repository_url: 'https://git.example.com/owner/repo',
  70. has_token: true,
  71. branch: 'main',
  72. provider: 'gitea',
  73. schedule_enabled: false,
  74. schedule_type: 'daily',
  75. backup_kprofiles: true,
  76. backup_cloud_profiles: true,
  77. backup_settings: false,
  78. backup_spools: false,
  79. backup_archives: false,
  80. enabled: true,
  81. last_backup_at: null,
  82. last_backup_status: null,
  83. last_backup_message: null,
  84. last_backup_commit_sha: null,
  85. next_scheduled_run: null,
  86. created_at: '2024-01-01T00:00:00Z',
  87. updated_at: '2024-01-01T00:00:00Z',
  88. })
  89. )
  90. );
  91. render(<GitHubBackupSettings />);
  92. await waitFor(() => {
  93. const select = screen.getByRole('combobox', { name: /git provider/i });
  94. expect(select).toHaveValue('gitea');
  95. });
  96. });
  97. it('renders Forgejo as a separate dropdown option', async () => {
  98. render(<GitHubBackupSettings />);
  99. await waitFor(() => {
  100. expect(screen.getByRole('combobox', { name: /git provider/i })).toBeInTheDocument();
  101. });
  102. const select = screen.getByRole('combobox', { name: /git provider/i });
  103. const options = Array.from((select as HTMLSelectElement).options).map((o) => o.value);
  104. expect(options).toContain('gitea');
  105. expect(options).toContain('forgejo');
  106. });
  107. it('names the scopes of the selected provider under the token field', async () => {
  108. // #2775: one shared hint could only ever be right for one provider, and the
  109. // one it was right for was GitHub. A Forgejo user reading "Contents read and
  110. // write" has no such setting to find, and guesses wide.
  111. render(<GitHubBackupSettings />);
  112. await waitFor(() => {
  113. expect(screen.getByRole('combobox', { name: /git provider/i })).toBeInTheDocument();
  114. });
  115. expect(screen.getByText(/Contents read and write access/i)).toBeInTheDocument();
  116. const select = screen.getByRole('combobox', { name: /git provider/i });
  117. fireEvent.change(select, { target: { value: 'forgejo' } });
  118. await waitFor(() => {
  119. expect(screen.getByText(/write:repository/i)).toBeInTheDocument();
  120. });
  121. expect(screen.queryByText(/Contents read and write access/i)).not.toBeInTheDocument();
  122. fireEvent.change(select, { target: { value: 'gitlab' } });
  123. await waitFor(() => {
  124. expect(screen.getByText(/read_repository/i)).toBeInTheDocument();
  125. });
  126. });
  127. it('loads forgejo provider from existing config', async () => {
  128. server.use(
  129. http.get('/api/v1/github-backup/config', () =>
  130. HttpResponse.json({
  131. id: 2,
  132. repository_url: 'https://forgejo.example.com/owner/repo',
  133. has_token: true,
  134. branch: 'main',
  135. provider: 'forgejo',
  136. schedule_enabled: false,
  137. schedule_type: 'daily',
  138. backup_kprofiles: true,
  139. backup_cloud_profiles: true,
  140. backup_settings: false,
  141. backup_spools: false,
  142. backup_archives: false,
  143. enabled: true,
  144. last_backup_at: null,
  145. last_backup_status: null,
  146. last_backup_message: null,
  147. last_backup_commit_sha: null,
  148. next_scheduled_run: null,
  149. created_at: '2024-01-01T00:00:00Z',
  150. updated_at: '2024-01-01T00:00:00Z',
  151. })
  152. )
  153. );
  154. render(<GitHubBackupSettings />);
  155. await waitFor(() => {
  156. const select = screen.getByRole('combobox', { name: /git provider/i });
  157. expect(select).toHaveValue('forgejo');
  158. });
  159. });
  160. it('autosaves existing config changes after debounce', async () => {
  161. let patchBody: Record<string, unknown> | null = null;
  162. server.use(
  163. http.get('/api/v1/github-backup/config', () =>
  164. HttpResponse.json({
  165. id: 3,
  166. repository_url: 'https://git.example.com/owner/repo',
  167. has_token: true,
  168. branch: 'main',
  169. provider: 'gitea',
  170. allow_insecure_http: false,
  171. schedule_enabled: false,
  172. schedule_type: 'daily',
  173. backup_kprofiles: true,
  174. backup_cloud_profiles: true,
  175. backup_settings: false,
  176. backup_spools: false,
  177. backup_archives: false,
  178. enabled: true,
  179. last_backup_at: null,
  180. last_backup_status: null,
  181. last_backup_message: null,
  182. last_backup_commit_sha: null,
  183. next_scheduled_run: null,
  184. created_at: '2024-01-01T00:00:00Z',
  185. updated_at: '2024-01-01T00:00:00Z',
  186. })
  187. ),
  188. http.patch('/api/v1/github-backup/config', async ({ request }) => {
  189. patchBody = await request.json() as Record<string, unknown>;
  190. return HttpResponse.json({
  191. id: 3,
  192. repository_url: patchBody.repository_url,
  193. has_token: true,
  194. branch: patchBody.branch,
  195. provider: patchBody.provider,
  196. allow_insecure_http: patchBody.allow_insecure_http,
  197. schedule_enabled: patchBody.schedule_enabled,
  198. schedule_type: patchBody.schedule_type,
  199. backup_kprofiles: patchBody.backup_kprofiles,
  200. backup_cloud_profiles: patchBody.backup_cloud_profiles,
  201. backup_settings: patchBody.backup_settings,
  202. backup_spools: patchBody.backup_spools,
  203. backup_archives: patchBody.backup_archives,
  204. enabled: patchBody.enabled,
  205. last_backup_at: null,
  206. last_backup_status: null,
  207. last_backup_message: null,
  208. last_backup_commit_sha: null,
  209. next_scheduled_run: null,
  210. created_at: '2024-01-01T00:00:00Z',
  211. updated_at: '2024-01-01T00:00:00Z',
  212. });
  213. })
  214. );
  215. render(<GitHubBackupSettings />);
  216. const branchInput = await screen.findByDisplayValue('main');
  217. await waitFor(() => expect(branchInput).toHaveValue('main'));
  218. fireEvent.change(branchInput, { target: { value: 'dev' } });
  219. await waitFor(() => {
  220. expect(patchBody).toEqual({ branch: 'dev' });
  221. }, { timeout: 2000 });
  222. });
  223. it('autosaves provider changes after debounce', async () => {
  224. let patchBody: Record<string, unknown> | null = null;
  225. server.use(
  226. http.get('/api/v1/github-backup/config', () =>
  227. HttpResponse.json({
  228. id: 4,
  229. repository_url: 'https://git.example.com/owner/repo',
  230. has_token: true,
  231. branch: 'main',
  232. provider: 'gitea',
  233. allow_insecure_http: false,
  234. schedule_enabled: false,
  235. schedule_type: 'daily',
  236. backup_kprofiles: true,
  237. backup_cloud_profiles: true,
  238. backup_settings: false,
  239. backup_spools: false,
  240. backup_archives: false,
  241. enabled: true,
  242. last_backup_at: null,
  243. last_backup_status: null,
  244. last_backup_message: null,
  245. last_backup_commit_sha: null,
  246. next_scheduled_run: null,
  247. created_at: '2024-01-01T00:00:00Z',
  248. updated_at: '2024-01-01T00:00:00Z',
  249. })
  250. ),
  251. http.patch('/api/v1/github-backup/config', async ({ request }) => {
  252. patchBody = await request.json() as Record<string, unknown>;
  253. return HttpResponse.json({
  254. id: 4,
  255. repository_url: patchBody.repository_url,
  256. has_token: true,
  257. branch: patchBody.branch,
  258. provider: patchBody.provider,
  259. allow_insecure_http: patchBody.allow_insecure_http,
  260. schedule_enabled: patchBody.schedule_enabled,
  261. schedule_type: patchBody.schedule_type,
  262. backup_kprofiles: patchBody.backup_kprofiles,
  263. backup_cloud_profiles: patchBody.backup_cloud_profiles,
  264. backup_settings: patchBody.backup_settings,
  265. backup_spools: patchBody.backup_spools,
  266. backup_archives: patchBody.backup_archives,
  267. enabled: patchBody.enabled,
  268. last_backup_at: null,
  269. last_backup_status: null,
  270. last_backup_message: null,
  271. last_backup_commit_sha: null,
  272. next_scheduled_run: null,
  273. created_at: '2024-01-01T00:00:00Z',
  274. updated_at: '2024-01-01T00:00:00Z',
  275. });
  276. })
  277. );
  278. render(<GitHubBackupSettings />);
  279. const providerSelect = await screen.findByRole('combobox', { name: /git provider/i });
  280. await waitFor(() => expect(providerSelect).toHaveValue('gitea'));
  281. fireEvent.change(providerSelect, { target: { value: 'forgejo' } });
  282. await waitFor(() => {
  283. expect(patchBody).toEqual({ provider: 'forgejo' });
  284. }, { timeout: 2000 });
  285. });
  286. it('autosaves repository URL changes after debounce', async () => {
  287. let patchBody: Record<string, unknown> | null = null;
  288. server.use(
  289. http.get('/api/v1/github-backup/config', () =>
  290. HttpResponse.json({
  291. id: 5,
  292. repository_url: 'https://git.example.com/owner/repo',
  293. has_token: true,
  294. branch: 'main',
  295. provider: 'gitea',
  296. allow_insecure_http: false,
  297. schedule_enabled: false,
  298. schedule_type: 'daily',
  299. backup_kprofiles: true,
  300. backup_cloud_profiles: true,
  301. backup_settings: false,
  302. backup_spools: false,
  303. backup_archives: false,
  304. enabled: true,
  305. last_backup_at: null,
  306. last_backup_status: null,
  307. last_backup_message: null,
  308. last_backup_commit_sha: null,
  309. next_scheduled_run: null,
  310. created_at: '2024-01-01T00:00:00Z',
  311. updated_at: '2024-01-01T00:00:00Z',
  312. })
  313. ),
  314. http.patch('/api/v1/github-backup/config', async ({ request }) => {
  315. patchBody = await request.json() as Record<string, unknown>;
  316. return HttpResponse.json({
  317. id: 5,
  318. repository_url: patchBody.repository_url,
  319. has_token: true,
  320. branch: patchBody.branch,
  321. provider: patchBody.provider,
  322. allow_insecure_http: patchBody.allow_insecure_http,
  323. schedule_enabled: patchBody.schedule_enabled,
  324. schedule_type: patchBody.schedule_type,
  325. backup_kprofiles: patchBody.backup_kprofiles,
  326. backup_cloud_profiles: patchBody.backup_cloud_profiles,
  327. backup_settings: patchBody.backup_settings,
  328. backup_spools: patchBody.backup_spools,
  329. backup_archives: patchBody.backup_archives,
  330. enabled: patchBody.enabled,
  331. last_backup_at: null,
  332. last_backup_status: null,
  333. last_backup_message: null,
  334. last_backup_commit_sha: null,
  335. next_scheduled_run: null,
  336. created_at: '2024-01-01T00:00:00Z',
  337. updated_at: '2024-01-01T00:00:00Z',
  338. });
  339. })
  340. );
  341. render(<GitHubBackupSettings />);
  342. const repoInput = await screen.findByDisplayValue('https://git.example.com/owner/repo');
  343. fireEvent.change(repoInput, { target: { value: 'https://git.example.com/owner/other-repo' } });
  344. await waitFor(() => {
  345. expect(patchBody).toEqual({
  346. repository_url: 'https://git.example.com/owner/other-repo',
  347. });
  348. }, { timeout: 2000 });
  349. });
  350. it('does not let pending token autosave cancel provider settings autosave', async () => {
  351. let patchBody: Record<string, unknown> | null = null;
  352. let postBody: Record<string, unknown> | null = null;
  353. server.use(
  354. http.get('/api/v1/github-backup/config', () =>
  355. HttpResponse.json({
  356. id: 6,
  357. repository_url: 'http://git.example.com/owner/repo',
  358. has_token: true,
  359. branch: 'main',
  360. provider: 'gitea',
  361. allow_insecure_http: true,
  362. schedule_enabled: false,
  363. schedule_type: 'daily',
  364. backup_kprofiles: true,
  365. backup_cloud_profiles: true,
  366. backup_settings: false,
  367. backup_spools: false,
  368. backup_archives: false,
  369. enabled: true,
  370. last_backup_at: null,
  371. last_backup_status: null,
  372. last_backup_message: null,
  373. last_backup_commit_sha: null,
  374. next_scheduled_run: null,
  375. created_at: '2024-01-01T00:00:00Z',
  376. updated_at: '2024-01-01T00:00:00Z',
  377. })
  378. ),
  379. http.patch('/api/v1/github-backup/config', async ({ request }) => {
  380. patchBody = await request.json() as Record<string, unknown>;
  381. return HttpResponse.json({
  382. id: 6,
  383. repository_url: 'http://git.example.com/owner/repo',
  384. has_token: true,
  385. branch: 'main',
  386. provider: patchBody.provider ?? 'gitea',
  387. allow_insecure_http: true,
  388. schedule_enabled: false,
  389. schedule_type: 'daily',
  390. backup_kprofiles: true,
  391. backup_cloud_profiles: true,
  392. backup_settings: false,
  393. backup_spools: false,
  394. backup_archives: false,
  395. enabled: true,
  396. last_backup_at: null,
  397. last_backup_status: null,
  398. last_backup_message: null,
  399. last_backup_commit_sha: null,
  400. next_scheduled_run: null,
  401. created_at: '2024-01-01T00:00:00Z',
  402. updated_at: '2024-01-01T00:00:00Z',
  403. });
  404. }),
  405. http.post('/api/v1/github-backup/config', async ({ request }) => {
  406. postBody = await request.json() as Record<string, unknown>;
  407. return HttpResponse.json({
  408. id: 6,
  409. repository_url: postBody.repository_url,
  410. has_token: true,
  411. branch: postBody.branch,
  412. provider: postBody.provider,
  413. allow_insecure_http: postBody.allow_insecure_http,
  414. schedule_enabled: postBody.schedule_enabled,
  415. schedule_type: postBody.schedule_type,
  416. backup_kprofiles: postBody.backup_kprofiles,
  417. backup_cloud_profiles: postBody.backup_cloud_profiles,
  418. backup_settings: postBody.backup_settings,
  419. backup_spools: postBody.backup_spools,
  420. backup_archives: postBody.backup_archives,
  421. enabled: postBody.enabled,
  422. last_backup_at: null,
  423. last_backup_status: null,
  424. last_backup_message: null,
  425. last_backup_commit_sha: null,
  426. next_scheduled_run: null,
  427. created_at: '2024-01-01T00:00:00Z',
  428. updated_at: '2024-01-01T00:00:00Z',
  429. });
  430. })
  431. );
  432. render(<GitHubBackupSettings />);
  433. const tokenInput = await screen.findByPlaceholderText('Enter new token to update');
  434. fireEvent.change(tokenInput, { target: { value: 'new-token' } });
  435. const providerSelect = await screen.findByRole('combobox', { name: /git provider/i });
  436. fireEvent.change(providerSelect, { target: { value: 'forgejo' } });
  437. await waitFor(() => {
  438. expect(patchBody).toEqual({ provider: 'forgejo' });
  439. }, { timeout: 2000 });
  440. });
  441. });