GitHubBackupSettings.pathCheck.test.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. /**
  2. * The backup card must say when it cannot write to the output directory (#2544).
  3. *
  4. * The reporter's NAS share was read-only *to the service* (our systemd unit ships
  5. * ProtectSystem=strict), his shell wrote to it fine, and the UI only ever said
  6. * "Failed". A week of nightly backups went nowhere. The banner below is what
  7. * turns that into something actionable.
  8. */
  9. import { describe, it, expect, vi, beforeEach } from 'vitest';
  10. import { screen, waitFor } from '@testing-library/react';
  11. import { render } from '../utils';
  12. import { GitHubBackupSettings } from '../../components/GitHubBackupSettings';
  13. import { api } from '../../api/client';
  14. vi.mock('../../api/client', () => ({
  15. api: {
  16. getGitHubBackupConfig: vi.fn().mockResolvedValue(null),
  17. getGitHubBackupStatus: vi.fn().mockResolvedValue({ is_running: false, configured: false, enabled: false }),
  18. getGitHubBackupLogs: vi.fn().mockResolvedValue([]),
  19. getCloudStatus: vi.fn().mockResolvedValue({ is_authenticated: false }),
  20. getPrinters: vi.fn().mockResolvedValue([]),
  21. getPrinterStatus: vi.fn().mockResolvedValue({ connected: false }),
  22. getSettings: vi.fn().mockResolvedValue({}),
  23. updateSettings: vi.fn().mockResolvedValue({}),
  24. getLocalBackups: vi.fn().mockResolvedValue([]),
  25. getLocalBackupStatus: vi.fn().mockResolvedValue({
  26. enabled: true,
  27. schedule: 'daily',
  28. time: '07:00',
  29. retention: 30,
  30. path: '/mnt/nasbackup',
  31. default_path: '/app/data/backups',
  32. is_running: false,
  33. last_backup_at: null,
  34. last_status: null,
  35. last_message: null,
  36. next_run: null,
  37. timezone: 'America/New_York',
  38. }),
  39. checkLocalBackupPath: vi.fn(),
  40. },
  41. }));
  42. const SANDBOXED = {
  43. writable: false,
  44. path: '/mnt/nasbackup',
  45. code: 'sandboxed',
  46. detail: "[Errno 30] Read-only file system: '/mnt/nasbackup/.bambuddy-write-test-x'",
  47. remedy: 'sudo systemctl edit bambuddy.service\n\n[Service]\nReadWritePaths=/mnt/nasbackup',
  48. message: '/mnt/nasbackup is read-only for the Bambuddy service.',
  49. warning: null,
  50. };
  51. describe('GitHubBackupSettings — backup path check', () => {
  52. beforeEach(() => {
  53. vi.clearAllMocks();
  54. });
  55. it('explains an unwritable path instead of leaving the user with an errno', async () => {
  56. vi.mocked(api.checkLocalBackupPath).mockResolvedValue(SANDBOXED);
  57. render(<GitHubBackupSettings />);
  58. expect(await screen.findByText(/cannot write to this directory/i)).toBeInTheDocument();
  59. // The cause, not just the symptom.
  60. expect(screen.getByText(/ProtectSystem=strict/)).toBeInTheDocument();
  61. // And the raw OS error is still there for anyone who wants it.
  62. expect(screen.getByText(/Errno 30/)).toBeInTheDocument();
  63. });
  64. it('shows the drop-in that fixes it, with the operator\'s own path in it', async () => {
  65. vi.mocked(api.checkLocalBackupPath).mockResolvedValue(SANDBOXED);
  66. render(<GitHubBackupSettings />);
  67. const remedy = await screen.findByText(/ReadWritePaths=\/mnt\/nasbackup/);
  68. expect(remedy).toBeInTheDocument();
  69. expect(remedy.textContent).toContain('systemctl edit bambuddy.service');
  70. });
  71. it('warns when the path is writable but lives inside the container', async () => {
  72. vi.mocked(api.checkLocalBackupPath).mockResolvedValue({
  73. writable: true,
  74. path: '/backups',
  75. code: 'ok',
  76. detail: null,
  77. remedy: 'services:\n bambuddy:\n volumes:\n - /backups:/backups',
  78. message: '/backups is writable.',
  79. warning: 'container_ephemeral',
  80. });
  81. render(<GitHubBackupSettings />);
  82. expect(await screen.findByText(/will not survive a container restart/i)).toBeInTheDocument();
  83. });
  84. it('stays quiet when the directory is fine', async () => {
  85. vi.mocked(api.checkLocalBackupPath).mockResolvedValue({
  86. writable: true,
  87. path: '/mnt/nasbackup',
  88. code: 'ok',
  89. detail: null,
  90. remedy: null,
  91. message: '/mnt/nasbackup is writable.',
  92. warning: null,
  93. });
  94. render(<GitHubBackupSettings />);
  95. await waitFor(() => expect(api.checkLocalBackupPath).toHaveBeenCalled());
  96. expect(screen.queryByText(/cannot write to this directory/i)).not.toBeInTheDocument();
  97. expect(screen.queryByText(/will not survive a container restart/i)).not.toBeInTheDocument();
  98. });
  99. });