test_github_backup_api.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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(self, async_client: AsyncClient):
  155. """Verify PATCH rejects leaving a stored HTTP URL without explicit insecure-HTTP allowance."""
  156. create_data = {
  157. "repository_url": "http://git.example.com/test/httprepo",
  158. "access_token": "gitea_token",
  159. "branch": "main",
  160. "provider": "gitea",
  161. "allow_insecure_http": True,
  162. "schedule_enabled": False,
  163. "schedule_type": "daily",
  164. "backup_kprofiles": True,
  165. "backup_cloud_profiles": True,
  166. "backup_settings": False,
  167. "backup_spools": False,
  168. "backup_archives": False,
  169. "enabled": True,
  170. }
  171. create_response = await async_client.post("/api/v1/github-backup/config", json=create_data)
  172. assert create_response.status_code == 200
  173. response = await async_client.patch("/api/v1/github-backup/config", json={"allow_insecure_http": False})
  174. assert response.status_code == 422
  175. assert "Allow insecure HTTP" in response.json()["detail"]
  176. stored_response = await async_client.get("/api/v1/github-backup/config")
  177. assert stored_response.status_code == 200
  178. stored = stored_response.json()
  179. assert stored["repository_url"] == "http://git.example.com/test/httprepo"
  180. assert stored["allow_insecure_http"] is True
  181. @pytest.mark.asyncio
  182. @pytest.mark.integration
  183. async def test_delete_config(self, async_client: AsyncClient):
  184. """Verify GitHub backup config can be deleted."""
  185. # Create config first
  186. create_data = {
  187. "repository_url": "https://github.com/test/delete",
  188. "access_token": "ghp_deletetoken",
  189. "branch": "main",
  190. "schedule_enabled": False,
  191. "schedule_type": "daily",
  192. "backup_kprofiles": True,
  193. "backup_cloud_profiles": True,
  194. "backup_settings": False,
  195. "enabled": True,
  196. }
  197. await async_client.post("/api/v1/github-backup/config", json=create_data)
  198. # Delete
  199. response = await async_client.delete("/api/v1/github-backup/config")
  200. assert response.status_code == 200
  201. # Verify it's deleted
  202. get_response = await async_client.get("/api/v1/github-backup/config")
  203. assert get_response.status_code == 200
  204. assert get_response.json() is None
  205. @pytest.mark.asyncio
  206. @pytest.mark.integration
  207. async def test_delete_config_not_found(self, async_client: AsyncClient):
  208. """Verify deleting non-existent config returns 404."""
  209. # Make sure no config exists
  210. await async_client.delete("/api/v1/github-backup/config")
  211. # Try to delete again
  212. response = await async_client.delete("/api/v1/github-backup/config")
  213. assert response.status_code == 404
  214. class TestGitHubBackupStatusAPI:
  215. """Integration tests for /api/v1/github-backup/status endpoint."""
  216. @pytest.mark.asyncio
  217. @pytest.mark.integration
  218. async def test_status_no_config(self, async_client: AsyncClient):
  219. """Verify status when no config exists."""
  220. # Ensure no config
  221. await async_client.delete("/api/v1/github-backup/config")
  222. response = await async_client.get("/api/v1/github-backup/status")
  223. assert response.status_code == 200
  224. result = response.json()
  225. assert result["configured"] is False
  226. assert result["enabled"] is False
  227. assert result["is_running"] is False
  228. @pytest.mark.asyncio
  229. @pytest.mark.integration
  230. async def test_status_with_config(self, async_client: AsyncClient):
  231. """Verify status when config exists."""
  232. # Create config
  233. create_data = {
  234. "repository_url": "https://github.com/test/status",
  235. "access_token": "ghp_statustoken",
  236. "branch": "main",
  237. "schedule_enabled": True,
  238. "schedule_type": "hourly",
  239. "backup_kprofiles": True,
  240. "backup_cloud_profiles": True,
  241. "backup_settings": False,
  242. "enabled": True,
  243. }
  244. await async_client.post("/api/v1/github-backup/config", json=create_data)
  245. response = await async_client.get("/api/v1/github-backup/status")
  246. assert response.status_code == 200
  247. result = response.json()
  248. assert result["configured"] is True
  249. assert result["enabled"] is True
  250. assert result["is_running"] is False
  251. assert result["next_scheduled_run"] is not None
  252. class TestGitHubBackupLogsAPI:
  253. """Integration tests for /api/v1/github-backup/logs endpoints."""
  254. @pytest.mark.asyncio
  255. @pytest.mark.integration
  256. async def test_logs_no_config(self, async_client: AsyncClient):
  257. """Verify getting logs when no config exists returns empty list."""
  258. # Ensure no config
  259. await async_client.delete("/api/v1/github-backup/config")
  260. response = await async_client.get("/api/v1/github-backup/logs")
  261. assert response.status_code == 200
  262. assert response.json() == []
  263. @pytest.mark.asyncio
  264. @pytest.mark.integration
  265. async def test_logs_with_config(self, async_client: AsyncClient):
  266. """Verify getting logs with config."""
  267. # Create config
  268. create_data = {
  269. "repository_url": "https://github.com/test/logs",
  270. "access_token": "ghp_logstoken",
  271. "branch": "main",
  272. "schedule_enabled": False,
  273. "schedule_type": "daily",
  274. "backup_kprofiles": True,
  275. "backup_cloud_profiles": True,
  276. "backup_settings": False,
  277. "enabled": True,
  278. }
  279. await async_client.post("/api/v1/github-backup/config", json=create_data)
  280. response = await async_client.get("/api/v1/github-backup/logs")
  281. assert response.status_code == 200
  282. # No backups run yet, so empty list
  283. assert response.json() == []
  284. class TestGitHubBackupTriggerAPI:
  285. """Integration tests for /api/v1/github-backup/run endpoint."""
  286. @pytest.mark.asyncio
  287. @pytest.mark.integration
  288. async def test_trigger_no_config(self, async_client: AsyncClient):
  289. """Verify triggering backup without config returns 404."""
  290. # Ensure no config
  291. await async_client.delete("/api/v1/github-backup/config")
  292. response = await async_client.post("/api/v1/github-backup/run")
  293. assert response.status_code == 404
  294. @pytest.mark.asyncio
  295. @pytest.mark.integration
  296. async def test_trigger_disabled_config(self, async_client: AsyncClient):
  297. """Verify triggering backup with disabled config returns 400."""
  298. # Create disabled config
  299. create_data = {
  300. "repository_url": "https://github.com/test/trigger",
  301. "access_token": "ghp_triggertoken",
  302. "branch": "main",
  303. "schedule_enabled": False,
  304. "schedule_type": "daily",
  305. "backup_kprofiles": True,
  306. "backup_cloud_profiles": True,
  307. "backup_settings": False,
  308. "enabled": False, # Disabled
  309. }
  310. await async_client.post("/api/v1/github-backup/config", json=create_data)
  311. response = await async_client.post("/api/v1/github-backup/run")
  312. assert response.status_code == 400
  313. assert "disabled" in response.json()["detail"].lower()