test_github_backup_api.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. """Integration tests for GitHub Backup API endpoints."""
  2. import pytest
  3. from httpx import AsyncClient
  4. class TestGitHubBackupConfigAPI:
  5. """Integration tests for /api/v1/github-backup endpoints."""
  6. @pytest.mark.asyncio
  7. @pytest.mark.integration
  8. async def test_get_config_no_config(self, async_client: AsyncClient):
  9. """Verify getting config when none exists returns null."""
  10. response = await async_client.get("/api/v1/github-backup/config")
  11. assert response.status_code == 200
  12. assert response.json() is None
  13. @pytest.mark.asyncio
  14. @pytest.mark.integration
  15. async def test_create_config(self, async_client: AsyncClient):
  16. """Verify GitHub backup config can be created."""
  17. data = {
  18. "repository_url": "https://github.com/test/repo",
  19. "access_token": "ghp_testtoken123",
  20. "branch": "main",
  21. "schedule_enabled": False,
  22. "schedule_type": "daily",
  23. "backup_kprofiles": True,
  24. "backup_cloud_profiles": True,
  25. "backup_settings": False,
  26. "backup_spools": False,
  27. "backup_archives": False,
  28. "enabled": True,
  29. }
  30. response = await async_client.post("/api/v1/github-backup/config", json=data)
  31. assert response.status_code == 200
  32. result = response.json()
  33. assert result["repository_url"] == "https://github.com/test/repo"
  34. assert result["branch"] == "main"
  35. assert result["has_token"] is True
  36. assert result["enabled"] is True
  37. assert result["backup_spools"] is False
  38. assert result["backup_archives"] is False
  39. # Token should not be exposed in response
  40. assert "access_token" not in result
  41. @pytest.mark.asyncio
  42. @pytest.mark.integration
  43. async def test_get_config_after_create(self, async_client: AsyncClient):
  44. """Verify getting config after creation returns the config."""
  45. # Create config first
  46. data = {
  47. "repository_url": "https://github.com/test/getrepo",
  48. "access_token": "ghp_testtoken456",
  49. "branch": "develop",
  50. "schedule_enabled": True,
  51. "schedule_type": "weekly",
  52. "backup_kprofiles": True,
  53. "backup_cloud_profiles": False,
  54. "backup_settings": True,
  55. "enabled": True,
  56. }
  57. await async_client.post("/api/v1/github-backup/config", json=data)
  58. # Get config
  59. response = await async_client.get("/api/v1/github-backup/config")
  60. assert response.status_code == 200
  61. result = response.json()
  62. assert result is not None
  63. assert result["repository_url"] == "https://github.com/test/getrepo"
  64. assert result["branch"] == "develop"
  65. assert result["schedule_type"] == "weekly"
  66. @pytest.mark.asyncio
  67. @pytest.mark.integration
  68. async def test_create_config_with_spools_and_archives(self, async_client: AsyncClient):
  69. """Verify config with spool and archive backup enabled."""
  70. data = {
  71. "repository_url": "https://github.com/test/spoolarchive",
  72. "access_token": "ghp_spooltoken",
  73. "branch": "main",
  74. "schedule_enabled": False,
  75. "schedule_type": "daily",
  76. "backup_kprofiles": True,
  77. "backup_cloud_profiles": False,
  78. "backup_settings": False,
  79. "backup_spools": True,
  80. "backup_archives": True,
  81. "enabled": True,
  82. }
  83. response = await async_client.post("/api/v1/github-backup/config", json=data)
  84. assert response.status_code == 200
  85. result = response.json()
  86. assert result["backup_spools"] is True
  87. assert result["backup_archives"] is True
  88. assert result["backup_cloud_profiles"] is False
  89. @pytest.mark.asyncio
  90. @pytest.mark.integration
  91. async def test_update_config_partial(self, async_client: AsyncClient):
  92. """Verify partial update of GitHub backup config."""
  93. # Create config first
  94. create_data = {
  95. "repository_url": "https://github.com/test/update",
  96. "access_token": "ghp_token",
  97. "branch": "main",
  98. "schedule_enabled": False,
  99. "schedule_type": "daily",
  100. "backup_kprofiles": True,
  101. "backup_cloud_profiles": True,
  102. "backup_settings": False,
  103. "backup_spools": False,
  104. "backup_archives": False,
  105. "enabled": True,
  106. }
  107. await async_client.post("/api/v1/github-backup/config", json=create_data)
  108. # Partial update
  109. update_data = {
  110. "branch": "develop",
  111. "schedule_enabled": True,
  112. }
  113. response = await async_client.patch("/api/v1/github-backup/config", json=update_data)
  114. assert response.status_code == 200
  115. result = response.json()
  116. assert result["branch"] == "develop"
  117. assert result["schedule_enabled"] is True
  118. # Original values should be preserved
  119. assert result["repository_url"] == "https://github.com/test/update"
  120. @pytest.mark.asyncio
  121. @pytest.mark.integration
  122. async def test_update_config_enable_spools_and_archives(self, async_client: AsyncClient):
  123. """Verify partial update can enable spool and archive backup."""
  124. # Create config first
  125. create_data = {
  126. "repository_url": "https://github.com/test/updatetoggle",
  127. "access_token": "ghp_toggletoken",
  128. "branch": "main",
  129. "schedule_enabled": False,
  130. "schedule_type": "daily",
  131. "backup_kprofiles": True,
  132. "backup_cloud_profiles": True,
  133. "backup_settings": False,
  134. "backup_spools": False,
  135. "backup_archives": False,
  136. "enabled": True,
  137. }
  138. await async_client.post("/api/v1/github-backup/config", json=create_data)
  139. # Enable spools and archives via partial update
  140. update_data = {
  141. "backup_spools": True,
  142. "backup_archives": True,
  143. }
  144. response = await async_client.patch("/api/v1/github-backup/config", json=update_data)
  145. assert response.status_code == 200
  146. result = response.json()
  147. assert result["backup_spools"] is True
  148. assert result["backup_archives"] is True
  149. # Other values preserved
  150. assert result["backup_kprofiles"] is True
  151. assert result["backup_settings"] is False
  152. @pytest.mark.asyncio
  153. @pytest.mark.integration
  154. async def test_update_config_rejects_disabling_insecure_http_for_stored_http_url(
  155. self, async_client: AsyncClient
  156. ):
  157. """Verify PATCH rejects leaving a stored HTTP URL without explicit insecure-HTTP allowance."""
  158. create_data = {
  159. "repository_url": "http://git.example.com/test/httprepo",
  160. "access_token": "gitea_token",
  161. "branch": "main",
  162. "provider": "gitea",
  163. "allow_insecure_http": True,
  164. "schedule_enabled": False,
  165. "schedule_type": "daily",
  166. "backup_kprofiles": True,
  167. "backup_cloud_profiles": True,
  168. "backup_settings": False,
  169. "backup_spools": False,
  170. "backup_archives": False,
  171. "enabled": True,
  172. }
  173. create_response = await async_client.post("/api/v1/github-backup/config", json=create_data)
  174. assert create_response.status_code == 200
  175. response = await async_client.patch("/api/v1/github-backup/config", json={"allow_insecure_http": False})
  176. assert response.status_code == 422
  177. assert "Allow insecure HTTP" in response.json()["detail"]
  178. stored_response = await async_client.get("/api/v1/github-backup/config")
  179. assert stored_response.status_code == 200
  180. stored = stored_response.json()
  181. assert stored["repository_url"] == "http://git.example.com/test/httprepo"
  182. assert stored["allow_insecure_http"] is True
  183. @pytest.mark.asyncio
  184. @pytest.mark.integration
  185. async def test_delete_config(self, async_client: AsyncClient):
  186. """Verify GitHub backup config can be deleted."""
  187. # Create config first
  188. create_data = {
  189. "repository_url": "https://github.com/test/delete",
  190. "access_token": "ghp_deletetoken",
  191. "branch": "main",
  192. "schedule_enabled": False,
  193. "schedule_type": "daily",
  194. "backup_kprofiles": True,
  195. "backup_cloud_profiles": True,
  196. "backup_settings": False,
  197. "enabled": True,
  198. }
  199. await async_client.post("/api/v1/github-backup/config", json=create_data)
  200. # Delete
  201. response = await async_client.delete("/api/v1/github-backup/config")
  202. assert response.status_code == 200
  203. # Verify it's deleted
  204. get_response = await async_client.get("/api/v1/github-backup/config")
  205. assert get_response.status_code == 200
  206. assert get_response.json() is None
  207. @pytest.mark.asyncio
  208. @pytest.mark.integration
  209. async def test_delete_config_not_found(self, async_client: AsyncClient):
  210. """Verify deleting non-existent config returns 404."""
  211. # Make sure no config exists
  212. await async_client.delete("/api/v1/github-backup/config")
  213. # Try to delete again
  214. response = await async_client.delete("/api/v1/github-backup/config")
  215. assert response.status_code == 404
  216. class TestGitHubBackupStatusAPI:
  217. """Integration tests for /api/v1/github-backup/status endpoint."""
  218. @pytest.mark.asyncio
  219. @pytest.mark.integration
  220. async def test_status_no_config(self, async_client: AsyncClient):
  221. """Verify status when no config exists."""
  222. # Ensure no config
  223. await async_client.delete("/api/v1/github-backup/config")
  224. response = await async_client.get("/api/v1/github-backup/status")
  225. assert response.status_code == 200
  226. result = response.json()
  227. assert result["configured"] is False
  228. assert result["enabled"] is False
  229. assert result["is_running"] is False
  230. @pytest.mark.asyncio
  231. @pytest.mark.integration
  232. async def test_status_with_config(self, async_client: AsyncClient):
  233. """Verify status when config exists."""
  234. # Create config
  235. create_data = {
  236. "repository_url": "https://github.com/test/status",
  237. "access_token": "ghp_statustoken",
  238. "branch": "main",
  239. "schedule_enabled": True,
  240. "schedule_type": "hourly",
  241. "backup_kprofiles": True,
  242. "backup_cloud_profiles": True,
  243. "backup_settings": False,
  244. "enabled": True,
  245. }
  246. await async_client.post("/api/v1/github-backup/config", json=create_data)
  247. response = await async_client.get("/api/v1/github-backup/status")
  248. assert response.status_code == 200
  249. result = response.json()
  250. assert result["configured"] is True
  251. assert result["enabled"] is True
  252. assert result["is_running"] is False
  253. assert result["next_scheduled_run"] is not None
  254. class TestGitHubBackupLogsAPI:
  255. """Integration tests for /api/v1/github-backup/logs endpoints."""
  256. @pytest.mark.asyncio
  257. @pytest.mark.integration
  258. async def test_logs_no_config(self, async_client: AsyncClient):
  259. """Verify getting logs when no config exists returns empty list."""
  260. # Ensure no config
  261. await async_client.delete("/api/v1/github-backup/config")
  262. response = await async_client.get("/api/v1/github-backup/logs")
  263. assert response.status_code == 200
  264. assert response.json() == []
  265. @pytest.mark.asyncio
  266. @pytest.mark.integration
  267. async def test_logs_with_config(self, async_client: AsyncClient):
  268. """Verify getting logs with config."""
  269. # Create config
  270. create_data = {
  271. "repository_url": "https://github.com/test/logs",
  272. "access_token": "ghp_logstoken",
  273. "branch": "main",
  274. "schedule_enabled": False,
  275. "schedule_type": "daily",
  276. "backup_kprofiles": True,
  277. "backup_cloud_profiles": True,
  278. "backup_settings": False,
  279. "enabled": True,
  280. }
  281. await async_client.post("/api/v1/github-backup/config", json=create_data)
  282. response = await async_client.get("/api/v1/github-backup/logs")
  283. assert response.status_code == 200
  284. # No backups run yet, so empty list
  285. assert response.json() == []
  286. class TestGitHubBackupTriggerAPI:
  287. """Integration tests for /api/v1/github-backup/run endpoint."""
  288. @pytest.mark.asyncio
  289. @pytest.mark.integration
  290. async def test_trigger_no_config(self, async_client: AsyncClient):
  291. """Verify triggering backup without config returns 404."""
  292. # Ensure no config
  293. await async_client.delete("/api/v1/github-backup/config")
  294. response = await async_client.post("/api/v1/github-backup/run")
  295. assert response.status_code == 404
  296. @pytest.mark.asyncio
  297. @pytest.mark.integration
  298. async def test_trigger_disabled_config(self, async_client: AsyncClient):
  299. """Verify triggering backup with disabled config returns 400."""
  300. # Create disabled config
  301. create_data = {
  302. "repository_url": "https://github.com/test/trigger",
  303. "access_token": "ghp_triggertoken",
  304. "branch": "main",
  305. "schedule_enabled": False,
  306. "schedule_type": "daily",
  307. "backup_kprofiles": True,
  308. "backup_cloud_profiles": True,
  309. "backup_settings": False,
  310. "enabled": False, # Disabled
  311. }
  312. await async_client.post("/api/v1/github-backup/config", json=create_data)
  313. response = await async_client.post("/api/v1/github-backup/run")
  314. assert response.status_code == 400
  315. assert "disabled" in response.json()["detail"].lower()