githubBackupApi.test.ts 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. /**
  2. * Tests for the GitHub Backup API client functions.
  3. */
  4. import { describe, it, expect } from 'vitest';
  5. import { http, HttpResponse } from 'msw';
  6. import { setupServer } from 'msw/node';
  7. import { api } from '../../api/client';
  8. import type {
  9. GitHubBackupConfig,
  10. GitHubBackupStatus,
  11. GitHubBackupLog,
  12. } from '../../api/client';
  13. // Mock API base URL
  14. const API_BASE = 'http://localhost:5000/api/v1';
  15. // Create MSW server
  16. const server = setupServer();
  17. beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
  18. afterEach(() => server.resetHandlers());
  19. afterAll(() => server.close());
  20. describe('GitHub Backup API Types', () => {
  21. it('GitHubBackupConfig has correct shape', () => {
  22. const config: GitHubBackupConfig = {
  23. id: 1,
  24. repository_url: 'https://github.com/test/repo',
  25. has_token: true,
  26. branch: 'main',
  27. schedule_enabled: true,
  28. schedule_type: 'daily',
  29. backup_kprofiles: true,
  30. backup_cloud_profiles: true,
  31. backup_settings: false,
  32. backup_spools: false,
  33. backup_archives: false,
  34. enabled: true,
  35. last_backup_at: '2026-01-27T10:00:00Z',
  36. last_backup_status: 'success',
  37. last_backup_message: null,
  38. last_backup_commit_sha: 'abc123',
  39. next_scheduled_run: '2026-01-28T00:00:00Z',
  40. created_at: '2026-01-01T00:00:00Z',
  41. updated_at: '2026-01-27T10:00:00Z',
  42. };
  43. expect(config.id).toBe(1);
  44. expect(config.has_token).toBe(true);
  45. expect(config.schedule_type).toBe('daily');
  46. expect(config.backup_spools).toBe(false);
  47. expect(config.backup_archives).toBe(false);
  48. });
  49. it('GitHubBackupStatus has correct shape', () => {
  50. const status: GitHubBackupStatus = {
  51. configured: true,
  52. enabled: true,
  53. is_running: false,
  54. progress: null,
  55. last_backup_at: '2026-01-27T10:00:00Z',
  56. last_backup_status: 'success',
  57. next_scheduled_run: '2026-01-28T00:00:00Z',
  58. };
  59. expect(status.configured).toBe(true);
  60. expect(status.is_running).toBe(false);
  61. });
  62. it('GitHubBackupStatus can have progress', () => {
  63. const status: GitHubBackupStatus = {
  64. configured: true,
  65. enabled: true,
  66. is_running: true,
  67. progress: 'Pushing to GitHub...',
  68. last_backup_at: null,
  69. last_backup_status: null,
  70. next_scheduled_run: null,
  71. };
  72. expect(status.is_running).toBe(true);
  73. expect(status.progress).toBe('Pushing to GitHub...');
  74. });
  75. it('GitHubBackupLog has correct shape', () => {
  76. const log: GitHubBackupLog = {
  77. id: 1,
  78. config_id: 1,
  79. started_at: '2026-01-27T10:00:00Z',
  80. completed_at: '2026-01-27T10:01:00Z',
  81. status: 'success',
  82. trigger: 'manual',
  83. commit_sha: 'abc123',
  84. files_changed: 5,
  85. error_message: null,
  86. };
  87. expect(log.status).toBe('success');
  88. expect(log.trigger).toBe('manual');
  89. expect(log.files_changed).toBe(5);
  90. });
  91. it('GitHubBackupConfig supports spool and archive backup toggles', () => {
  92. const config: GitHubBackupConfig = {
  93. id: 2,
  94. repository_url: 'https://github.com/test/full-backup',
  95. has_token: true,
  96. branch: 'main',
  97. schedule_enabled: false,
  98. schedule_type: 'daily',
  99. backup_kprofiles: true,
  100. backup_cloud_profiles: false,
  101. backup_settings: false,
  102. backup_spools: true,
  103. backup_archives: true,
  104. enabled: true,
  105. last_backup_at: null,
  106. last_backup_status: null,
  107. last_backup_message: null,
  108. last_backup_commit_sha: null,
  109. next_scheduled_run: null,
  110. created_at: '2026-04-01T00:00:00Z',
  111. updated_at: '2026-04-01T00:00:00Z',
  112. };
  113. expect(config.backup_spools).toBe(true);
  114. expect(config.backup_archives).toBe(true);
  115. expect(config.backup_cloud_profiles).toBe(false);
  116. });
  117. it('GitHubBackupLog can have error', () => {
  118. const log: GitHubBackupLog = {
  119. id: 2,
  120. config_id: 1,
  121. started_at: '2026-01-27T10:00:00Z',
  122. completed_at: '2026-01-27T10:00:30Z',
  123. status: 'failed',
  124. trigger: 'scheduled',
  125. commit_sha: null,
  126. files_changed: 0,
  127. error_message: 'Authentication failed',
  128. };
  129. expect(log.status).toBe('failed');
  130. expect(log.error_message).toBe('Authentication failed');
  131. expect(log.commit_sha).toBeNull();
  132. });
  133. });
  134. describe('GitHub Backup API Endpoints', () => {
  135. it('GET /github-backup/config returns null when not configured', async () => {
  136. server.use(
  137. http.get(`${API_BASE}/github-backup/config`, () => {
  138. return HttpResponse.json(null);
  139. })
  140. );
  141. const response = await fetch(`${API_BASE}/github-backup/config`);
  142. const data = await response.json();
  143. expect(data).toBeNull();
  144. });
  145. it('GET /github-backup/config returns config when exists', async () => {
  146. const mockConfig: GitHubBackupConfig = {
  147. id: 1,
  148. repository_url: 'https://github.com/test/repo',
  149. has_token: true,
  150. branch: 'main',
  151. schedule_enabled: false,
  152. schedule_type: 'daily',
  153. backup_kprofiles: true,
  154. backup_cloud_profiles: true,
  155. backup_settings: false,
  156. backup_spools: true,
  157. backup_archives: true,
  158. enabled: true,
  159. last_backup_at: null,
  160. last_backup_status: null,
  161. last_backup_message: null,
  162. last_backup_commit_sha: null,
  163. next_scheduled_run: null,
  164. created_at: '2026-01-01T00:00:00Z',
  165. updated_at: '2026-01-01T00:00:00Z',
  166. };
  167. server.use(
  168. http.get(`${API_BASE}/github-backup/config`, () => {
  169. return HttpResponse.json(mockConfig);
  170. })
  171. );
  172. const response = await fetch(`${API_BASE}/github-backup/config`);
  173. const data = await response.json();
  174. expect(data.repository_url).toBe('https://github.com/test/repo');
  175. expect(data.has_token).toBe(true);
  176. });
  177. it('GET /github-backup/status returns not configured status', async () => {
  178. const mockStatus: GitHubBackupStatus = {
  179. configured: false,
  180. enabled: false,
  181. is_running: false,
  182. progress: null,
  183. last_backup_at: null,
  184. last_backup_status: null,
  185. next_scheduled_run: null,
  186. };
  187. server.use(
  188. http.get(`${API_BASE}/github-backup/status`, () => {
  189. return HttpResponse.json(mockStatus);
  190. })
  191. );
  192. const response = await fetch(`${API_BASE}/github-backup/status`);
  193. const data = await response.json();
  194. expect(data.configured).toBe(false);
  195. expect(data.enabled).toBe(false);
  196. });
  197. it('GET /github-backup/logs returns empty list when no logs', async () => {
  198. server.use(
  199. http.get(`${API_BASE}/github-backup/logs`, () => {
  200. return HttpResponse.json([]);
  201. })
  202. );
  203. const response = await fetch(`${API_BASE}/github-backup/logs`);
  204. const data = await response.json();
  205. expect(data).toEqual([]);
  206. });
  207. it('GET /github-backup/logs returns log entries', async () => {
  208. const mockLogs: GitHubBackupLog[] = [
  209. {
  210. id: 1,
  211. config_id: 1,
  212. started_at: '2026-01-27T10:00:00Z',
  213. completed_at: '2026-01-27T10:01:00Z',
  214. status: 'success',
  215. trigger: 'manual',
  216. commit_sha: 'abc123',
  217. files_changed: 5,
  218. error_message: null,
  219. },
  220. ];
  221. server.use(
  222. http.get(`${API_BASE}/github-backup/logs`, () => {
  223. return HttpResponse.json(mockLogs);
  224. })
  225. );
  226. const response = await fetch(`${API_BASE}/github-backup/logs`);
  227. const data = await response.json();
  228. expect(data.length).toBe(1);
  229. expect(data[0].status).toBe('success');
  230. });
  231. it('POST /github-backup/run returns 404 when not configured', async () => {
  232. server.use(
  233. http.post(`${API_BASE}/github-backup/run`, () => {
  234. return HttpResponse.json(
  235. { detail: 'No configuration found' },
  236. { status: 404 }
  237. );
  238. })
  239. );
  240. const response = await fetch(`${API_BASE}/github-backup/run`, {
  241. method: 'POST',
  242. });
  243. expect(response.status).toBe(404);
  244. });
  245. it('POST /github-backup/test returns success on valid credentials', async () => {
  246. server.use(
  247. http.post(`${API_BASE}/github-backup/test`, () => {
  248. return HttpResponse.json({
  249. success: true,
  250. message: 'Connection successful',
  251. repo_name: 'test/repo',
  252. default_branch: 'main',
  253. });
  254. })
  255. );
  256. const response = await fetch(`${API_BASE}/github-backup/test`, {
  257. method: 'POST',
  258. headers: { 'Content-Type': 'application/json' },
  259. body: JSON.stringify({ repo_url: 'https://github.com/test/repo', token: 'ghp_test' }),
  260. });
  261. const data = await response.json();
  262. expect(data.success).toBe(true);
  263. expect(data.repo_name).toBe('test/repo');
  264. });
  265. it('POST /github-backup/test returns failure on invalid credentials', async () => {
  266. server.use(
  267. http.post(`${API_BASE}/github-backup/test`, () => {
  268. return HttpResponse.json({
  269. success: false,
  270. message: 'Authentication failed',
  271. repo_name: null,
  272. default_branch: null,
  273. });
  274. })
  275. );
  276. const response = await fetch(`${API_BASE}/github-backup/test`, {
  277. method: 'POST',
  278. headers: { 'Content-Type': 'application/json' },
  279. body: JSON.stringify({ repo_url: 'https://github.com/test/repo', token: 'invalid' }),
  280. });
  281. const data = await response.json();
  282. expect(data.success).toBe(false);
  283. expect(data.message).toBe('Authentication failed');
  284. });
  285. it('testGitHubConnection carries the credentials in the request body, not the URL', async () => {
  286. let seen: { url: string; body: unknown } | null = null;
  287. server.use(
  288. http.post('*/api/v1/github-backup/test', async ({ request }) => {
  289. seen = { url: request.url, body: await request.json() };
  290. return HttpResponse.json({ success: true, message: 'ok', is_private: true });
  291. })
  292. );
  293. await api.testGitHubConnection('https://github.com/test/backup-repo', 'ghp_typed_value', 'gitea');
  294. expect(seen!.url).not.toContain('ghp_typed_value');
  295. expect(new URL(seen!.url).search).toBe('');
  296. expect(seen!.body).toEqual({
  297. repo_url: 'https://github.com/test/backup-repo',
  298. token: 'ghp_typed_value',
  299. provider: 'gitea',
  300. });
  301. });
  302. });