test_auth_api.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. """Integration tests for Authentication API endpoints.
  2. Tests the full request/response cycle for /api/v1/auth/ and /api/v1/users/ endpoints.
  3. """
  4. import pytest
  5. from httpx import AsyncClient
  6. class TestAuthStatusAPI:
  7. """Integration tests for /api/v1/auth/status endpoint."""
  8. @pytest.mark.asyncio
  9. @pytest.mark.integration
  10. async def test_get_auth_status_disabled(self, async_client: AsyncClient):
  11. """Verify auth status returns disabled when not configured."""
  12. response = await async_client.get("/api/v1/auth/status")
  13. assert response.status_code == 200
  14. result = response.json()
  15. assert "auth_enabled" in result
  16. assert result["auth_enabled"] is False
  17. assert result["requires_setup"] is True
  18. class TestAuthSetupAPI:
  19. """Integration tests for /api/v1/auth/setup endpoint."""
  20. @pytest.mark.asyncio
  21. @pytest.mark.integration
  22. async def test_setup_auth_disabled(self, async_client: AsyncClient):
  23. """Verify auth can be set up with auth disabled (no password required)."""
  24. response = await async_client.post(
  25. "/api/v1/auth/setup",
  26. json={"auth_enabled": False},
  27. )
  28. assert response.status_code == 200
  29. result = response.json()
  30. assert result["auth_enabled"] is False
  31. assert result["admin_created"] is False
  32. @pytest.mark.asyncio
  33. @pytest.mark.integration
  34. async def test_setup_auth_enabled_requires_credentials(self, async_client: AsyncClient):
  35. """Verify enabling auth requires admin username and password."""
  36. response = await async_client.post(
  37. "/api/v1/auth/setup",
  38. json={"auth_enabled": True},
  39. )
  40. assert response.status_code == 400
  41. assert "Admin username and password are required" in response.json()["detail"]
  42. @pytest.mark.asyncio
  43. @pytest.mark.integration
  44. async def test_setup_auth_enabled_with_credentials(self, async_client: AsyncClient):
  45. """Verify auth can be enabled with admin credentials."""
  46. response = await async_client.post(
  47. "/api/v1/auth/setup",
  48. json={
  49. "auth_enabled": True,
  50. "admin_username": "testadmin",
  51. "admin_password": "testpassword123",
  52. },
  53. )
  54. assert response.status_code == 200
  55. result = response.json()
  56. assert result["auth_enabled"] is True
  57. assert result["admin_created"] is True
  58. class TestAuthLoginAPI:
  59. """Integration tests for /api/v1/auth/login endpoint."""
  60. @pytest.mark.asyncio
  61. @pytest.mark.integration
  62. async def test_login_auth_disabled(self, async_client: AsyncClient):
  63. """Verify login fails when auth is not enabled."""
  64. response = await async_client.post(
  65. "/api/v1/auth/login",
  66. json={"username": "admin", "password": "password"},
  67. )
  68. assert response.status_code == 400
  69. assert "Authentication is not enabled" in response.json()["detail"]
  70. @pytest.mark.asyncio
  71. @pytest.mark.integration
  72. async def test_login_success(self, async_client: AsyncClient):
  73. """Verify login succeeds with valid credentials after setup."""
  74. # First enable auth
  75. await async_client.post(
  76. "/api/v1/auth/setup",
  77. json={
  78. "auth_enabled": True,
  79. "admin_username": "logintest",
  80. "admin_password": "loginpassword123",
  81. },
  82. )
  83. # Now login
  84. response = await async_client.post(
  85. "/api/v1/auth/login",
  86. json={"username": "logintest", "password": "loginpassword123"},
  87. )
  88. assert response.status_code == 200
  89. result = response.json()
  90. assert "access_token" in result
  91. assert result["token_type"] == "bearer"
  92. assert result["user"]["username"] == "logintest"
  93. assert result["user"]["role"] == "admin"
  94. @pytest.mark.asyncio
  95. @pytest.mark.integration
  96. async def test_login_invalid_credentials(self, async_client: AsyncClient):
  97. """Verify login fails with invalid credentials."""
  98. # First enable auth
  99. await async_client.post(
  100. "/api/v1/auth/setup",
  101. json={
  102. "auth_enabled": True,
  103. "admin_username": "invalidtest",
  104. "admin_password": "correctpassword",
  105. },
  106. )
  107. # Try login with wrong password
  108. response = await async_client.post(
  109. "/api/v1/auth/login",
  110. json={"username": "invalidtest", "password": "wrongpassword"},
  111. )
  112. assert response.status_code == 401
  113. assert "Incorrect username or password" in response.json()["detail"]
  114. class TestAuthMeAPI:
  115. """Integration tests for /api/v1/auth/me endpoint."""
  116. @pytest.mark.asyncio
  117. @pytest.mark.integration
  118. async def test_me_without_token(self, async_client: AsyncClient):
  119. """Verify /me fails without authentication token."""
  120. response = await async_client.get("/api/v1/auth/me")
  121. assert response.status_code == 401
  122. @pytest.mark.asyncio
  123. @pytest.mark.integration
  124. async def test_me_with_valid_token(self, async_client: AsyncClient):
  125. """Verify /me returns user info with valid token."""
  126. # Setup and login
  127. await async_client.post(
  128. "/api/v1/auth/setup",
  129. json={
  130. "auth_enabled": True,
  131. "admin_username": "metest",
  132. "admin_password": "mepassword123",
  133. },
  134. )
  135. login_response = await async_client.post(
  136. "/api/v1/auth/login",
  137. json={"username": "metest", "password": "mepassword123"},
  138. )
  139. token = login_response.json()["access_token"]
  140. # Get current user
  141. response = await async_client.get(
  142. "/api/v1/auth/me",
  143. headers={"Authorization": f"Bearer {token}"},
  144. )
  145. assert response.status_code == 200
  146. result = response.json()
  147. assert result["username"] == "metest"
  148. assert result["role"] == "admin"
  149. assert result["is_active"] is True
  150. class TestUsersAPI:
  151. """Integration tests for /api/v1/users/ endpoints."""
  152. @pytest.fixture
  153. async def auth_token(self, async_client: AsyncClient):
  154. """Setup auth and return admin token."""
  155. await async_client.post(
  156. "/api/v1/auth/setup",
  157. json={
  158. "auth_enabled": True,
  159. "admin_username": "usersadmin",
  160. "admin_password": "adminpassword123",
  161. },
  162. )
  163. login_response = await async_client.post(
  164. "/api/v1/auth/login",
  165. json={"username": "usersadmin", "password": "adminpassword123"},
  166. )
  167. return login_response.json()["access_token"]
  168. @pytest.mark.asyncio
  169. @pytest.mark.integration
  170. async def test_list_users_requires_auth(self, async_client: AsyncClient):
  171. """Verify listing users requires authentication when auth is enabled."""
  172. # First enable auth
  173. await async_client.post(
  174. "/api/v1/auth/setup",
  175. json={
  176. "auth_enabled": True,
  177. "admin_username": "authreqadmin",
  178. "admin_password": "adminpassword123",
  179. },
  180. )
  181. # Now try to list users without a token
  182. response = await async_client.get("/api/v1/users/")
  183. assert response.status_code == 401
  184. @pytest.mark.asyncio
  185. @pytest.mark.integration
  186. async def test_list_users_as_admin(self, async_client: AsyncClient, auth_token: str):
  187. """Verify admin can list users."""
  188. response = await async_client.get(
  189. "/api/v1/users/",
  190. headers={"Authorization": f"Bearer {auth_token}"},
  191. )
  192. assert response.status_code == 200
  193. result = response.json()
  194. assert isinstance(result, list)
  195. assert len(result) >= 1 # At least the admin user
  196. @pytest.mark.asyncio
  197. @pytest.mark.integration
  198. async def test_create_user(self, async_client: AsyncClient, auth_token: str):
  199. """Verify admin can create a new user."""
  200. response = await async_client.post(
  201. "/api/v1/users/",
  202. headers={"Authorization": f"Bearer {auth_token}"},
  203. json={
  204. "username": "newuser",
  205. "password": "newuserpassword",
  206. "role": "user",
  207. },
  208. )
  209. assert response.status_code == 201
  210. result = response.json()
  211. assert result["username"] == "newuser"
  212. assert result["role"] == "user"
  213. assert result["is_active"] is True
  214. @pytest.mark.asyncio
  215. @pytest.mark.integration
  216. async def test_create_user_duplicate_username(self, async_client: AsyncClient, auth_token: str):
  217. """Verify creating user with duplicate username fails."""
  218. # Create first user
  219. await async_client.post(
  220. "/api/v1/users/",
  221. headers={"Authorization": f"Bearer {auth_token}"},
  222. json={
  223. "username": "duplicateuser",
  224. "password": "password123",
  225. "role": "user",
  226. },
  227. )
  228. # Try to create duplicate
  229. response = await async_client.post(
  230. "/api/v1/users/",
  231. headers={"Authorization": f"Bearer {auth_token}"},
  232. json={
  233. "username": "duplicateuser",
  234. "password": "password456",
  235. "role": "user",
  236. },
  237. )
  238. assert response.status_code == 400
  239. assert "Username already exists" in response.json()["detail"]
  240. @pytest.mark.asyncio
  241. @pytest.mark.integration
  242. async def test_update_user(self, async_client: AsyncClient, auth_token: str):
  243. """Verify admin can update a user."""
  244. # Create user
  245. create_response = await async_client.post(
  246. "/api/v1/users/",
  247. headers={"Authorization": f"Bearer {auth_token}"},
  248. json={
  249. "username": "updateuser",
  250. "password": "password123",
  251. "role": "user",
  252. },
  253. )
  254. user_id = create_response.json()["id"]
  255. # Update user
  256. response = await async_client.patch(
  257. f"/api/v1/users/{user_id}",
  258. headers={"Authorization": f"Bearer {auth_token}"},
  259. json={"role": "admin"},
  260. )
  261. assert response.status_code == 200
  262. assert response.json()["role"] == "admin"
  263. @pytest.mark.asyncio
  264. @pytest.mark.integration
  265. async def test_delete_user(self, async_client: AsyncClient, auth_token: str):
  266. """Verify admin can delete a user."""
  267. # Create user
  268. create_response = await async_client.post(
  269. "/api/v1/users/",
  270. headers={"Authorization": f"Bearer {auth_token}"},
  271. json={
  272. "username": "deleteuser",
  273. "password": "password123",
  274. "role": "user",
  275. },
  276. )
  277. user_id = create_response.json()["id"]
  278. # Delete user
  279. response = await async_client.delete(
  280. f"/api/v1/users/{user_id}",
  281. headers={"Authorization": f"Bearer {auth_token}"},
  282. )
  283. assert response.status_code == 204
  284. class TestAuthDisableAPI:
  285. """Integration tests for /api/v1/auth/disable endpoint."""
  286. @pytest.mark.asyncio
  287. @pytest.mark.integration
  288. async def test_disable_auth(self, async_client: AsyncClient):
  289. """Verify admin can disable authentication."""
  290. # Setup auth
  291. await async_client.post(
  292. "/api/v1/auth/setup",
  293. json={
  294. "auth_enabled": True,
  295. "admin_username": "disableadmin",
  296. "admin_password": "adminpassword123",
  297. },
  298. )
  299. # Login to get token
  300. login_response = await async_client.post(
  301. "/api/v1/auth/login",
  302. json={"username": "disableadmin", "password": "adminpassword123"},
  303. )
  304. token = login_response.json()["access_token"]
  305. # Disable auth
  306. response = await async_client.post(
  307. "/api/v1/auth/disable",
  308. headers={"Authorization": f"Bearer {token}"},
  309. )
  310. assert response.status_code == 200
  311. assert response.json()["auth_enabled"] is False
  312. # Verify auth is now disabled
  313. status_response = await async_client.get("/api/v1/auth/status")
  314. assert status_response.json()["auth_enabled"] is False
  315. class TestGroupsAPI:
  316. """Integration tests for /api/v1/groups/ endpoints."""
  317. @pytest.fixture
  318. async def auth_token(self, async_client: AsyncClient):
  319. """Setup auth and return admin token."""
  320. await async_client.post(
  321. "/api/v1/auth/setup",
  322. json={
  323. "auth_enabled": True,
  324. "admin_username": "groupsadmin",
  325. "admin_password": "adminpassword123",
  326. },
  327. )
  328. login_response = await async_client.post(
  329. "/api/v1/auth/login",
  330. json={"username": "groupsadmin", "password": "adminpassword123"},
  331. )
  332. return login_response.json()["access_token"]
  333. @pytest.mark.asyncio
  334. @pytest.mark.integration
  335. async def test_list_groups(self, async_client: AsyncClient, auth_token: str):
  336. """Verify listing groups returns default groups."""
  337. response = await async_client.get(
  338. "/api/v1/groups/",
  339. headers={"Authorization": f"Bearer {auth_token}"},
  340. )
  341. assert response.status_code == 200
  342. groups = response.json()
  343. assert isinstance(groups, list)
  344. # Should have default groups: Administrators, Operators, Viewers
  345. group_names = [g["name"] for g in groups]
  346. assert "Administrators" in group_names
  347. assert "Operators" in group_names
  348. assert "Viewers" in group_names
  349. @pytest.mark.asyncio
  350. @pytest.mark.integration
  351. async def test_get_permissions(self, async_client: AsyncClient, auth_token: str):
  352. """Verify getting available permissions."""
  353. response = await async_client.get(
  354. "/api/v1/groups/permissions",
  355. headers={"Authorization": f"Bearer {auth_token}"},
  356. )
  357. assert response.status_code == 200
  358. permissions = response.json()
  359. assert isinstance(permissions, dict)
  360. # Should have permission categories
  361. assert "Printers" in permissions or len(permissions) > 0
  362. @pytest.mark.asyncio
  363. @pytest.mark.integration
  364. async def test_create_group(self, async_client: AsyncClient, auth_token: str):
  365. """Verify creating a new group."""
  366. response = await async_client.post(
  367. "/api/v1/groups/",
  368. headers={"Authorization": f"Bearer {auth_token}"},
  369. json={
  370. "name": "Custom Group",
  371. "description": "A custom test group",
  372. "permissions": ["printers:read", "archives:read"],
  373. },
  374. )
  375. assert response.status_code == 201
  376. group = response.json()
  377. assert group["name"] == "Custom Group"
  378. assert group["description"] == "A custom test group"
  379. assert "printers:read" in group["permissions"]
  380. assert group["is_system"] is False
  381. @pytest.mark.asyncio
  382. @pytest.mark.integration
  383. async def test_update_group(self, async_client: AsyncClient, auth_token: str):
  384. """Verify updating a group."""
  385. # Create a group first
  386. create_response = await async_client.post(
  387. "/api/v1/groups/",
  388. headers={"Authorization": f"Bearer {auth_token}"},
  389. json={
  390. "name": "Update Test Group",
  391. "permissions": ["printers:read"],
  392. },
  393. )
  394. group_id = create_response.json()["id"]
  395. # Update the group
  396. response = await async_client.patch(
  397. f"/api/v1/groups/{group_id}",
  398. headers={"Authorization": f"Bearer {auth_token}"},
  399. json={
  400. "description": "Updated description",
  401. "permissions": ["printers:read", "printers:control"],
  402. },
  403. )
  404. assert response.status_code == 200
  405. group = response.json()
  406. assert group["description"] == "Updated description"
  407. assert "printers:control" in group["permissions"]
  408. @pytest.mark.asyncio
  409. @pytest.mark.integration
  410. async def test_cannot_delete_system_group(self, async_client: AsyncClient, auth_token: str):
  411. """Verify system groups cannot be deleted."""
  412. # Get the Administrators group
  413. list_response = await async_client.get(
  414. "/api/v1/groups/",
  415. headers={"Authorization": f"Bearer {auth_token}"},
  416. )
  417. admin_group = next(g for g in list_response.json() if g["name"] == "Administrators")
  418. # Try to delete it
  419. response = await async_client.delete(
  420. f"/api/v1/groups/{admin_group['id']}",
  421. headers={"Authorization": f"Bearer {auth_token}"},
  422. )
  423. assert response.status_code == 400
  424. assert "system group" in response.json()["detail"].lower()
  425. @pytest.mark.asyncio
  426. @pytest.mark.integration
  427. async def test_delete_custom_group(self, async_client: AsyncClient, auth_token: str):
  428. """Verify custom groups can be deleted."""
  429. # Create a group
  430. create_response = await async_client.post(
  431. "/api/v1/groups/",
  432. headers={"Authorization": f"Bearer {auth_token}"},
  433. json={"name": "Delete Test Group"},
  434. )
  435. group_id = create_response.json()["id"]
  436. # Delete it
  437. response = await async_client.delete(
  438. f"/api/v1/groups/{group_id}",
  439. headers={"Authorization": f"Bearer {auth_token}"},
  440. )
  441. assert response.status_code == 204
  442. class TestUserGroupsAPI:
  443. """Integration tests for user-group assignments."""
  444. @pytest.fixture
  445. async def auth_token(self, async_client: AsyncClient):
  446. """Setup auth and return admin token."""
  447. await async_client.post(
  448. "/api/v1/auth/setup",
  449. json={
  450. "auth_enabled": True,
  451. "admin_username": "usergroupadmin",
  452. "admin_password": "adminpassword123",
  453. },
  454. )
  455. login_response = await async_client.post(
  456. "/api/v1/auth/login",
  457. json={"username": "usergroupadmin", "password": "adminpassword123"},
  458. )
  459. return login_response.json()["access_token"]
  460. @pytest.mark.asyncio
  461. @pytest.mark.integration
  462. async def test_create_user_with_groups(self, async_client: AsyncClient, auth_token: str):
  463. """Verify creating a user with group assignments."""
  464. # Get Operators group ID
  465. groups_response = await async_client.get(
  466. "/api/v1/groups/",
  467. headers={"Authorization": f"Bearer {auth_token}"},
  468. )
  469. operators_group = next(g for g in groups_response.json() if g["name"] == "Operators")
  470. # Create user with group
  471. response = await async_client.post(
  472. "/api/v1/users/",
  473. headers={"Authorization": f"Bearer {auth_token}"},
  474. json={
  475. "username": "groupuser",
  476. "password": "password123",
  477. "group_ids": [operators_group["id"]],
  478. },
  479. )
  480. assert response.status_code == 201
  481. user = response.json()
  482. assert any(g["name"] == "Operators" for g in user["groups"])
  483. @pytest.mark.asyncio
  484. @pytest.mark.integration
  485. async def test_add_user_to_group(self, async_client: AsyncClient, auth_token: str):
  486. """Verify adding a user to a group."""
  487. # Create a user
  488. user_response = await async_client.post(
  489. "/api/v1/users/",
  490. headers={"Authorization": f"Bearer {auth_token}"},
  491. json={"username": "addtogroup", "password": "password123"},
  492. )
  493. user_id = user_response.json()["id"]
  494. # Get Viewers group
  495. groups_response = await async_client.get(
  496. "/api/v1/groups/",
  497. headers={"Authorization": f"Bearer {auth_token}"},
  498. )
  499. viewers_group = next(g for g in groups_response.json() if g["name"] == "Viewers")
  500. # Add user to group
  501. response = await async_client.post(
  502. f"/api/v1/groups/{viewers_group['id']}/users/{user_id}",
  503. headers={"Authorization": f"Bearer {auth_token}"},
  504. )
  505. assert response.status_code == 204
  506. # Verify user is in group
  507. user_check = await async_client.get(
  508. f"/api/v1/users/{user_id}",
  509. headers={"Authorization": f"Bearer {auth_token}"},
  510. )
  511. assert any(g["name"] == "Viewers" for g in user_check.json()["groups"])
  512. class TestChangePasswordAPI:
  513. """Integration tests for /api/v1/users/me/change-password endpoint."""
  514. @pytest.fixture
  515. async def user_token(self, async_client: AsyncClient):
  516. """Setup auth and return regular user token."""
  517. # Enable auth with admin
  518. await async_client.post(
  519. "/api/v1/auth/setup",
  520. json={
  521. "auth_enabled": True,
  522. "admin_username": "pwchangeadmin",
  523. "admin_password": "adminpassword123",
  524. },
  525. )
  526. admin_login = await async_client.post(
  527. "/api/v1/auth/login",
  528. json={"username": "pwchangeadmin", "password": "adminpassword123"},
  529. )
  530. admin_token = admin_login.json()["access_token"]
  531. # Create a regular user
  532. await async_client.post(
  533. "/api/v1/users/",
  534. headers={"Authorization": f"Bearer {admin_token}"},
  535. json={"username": "pwchangeuser", "password": "oldpassword123"},
  536. )
  537. # Login as regular user
  538. user_login = await async_client.post(
  539. "/api/v1/auth/login",
  540. json={"username": "pwchangeuser", "password": "oldpassword123"},
  541. )
  542. return user_login.json()["access_token"]
  543. @pytest.mark.asyncio
  544. @pytest.mark.integration
  545. async def test_change_password_success(self, async_client: AsyncClient, user_token: str):
  546. """Verify user can change their own password."""
  547. response = await async_client.post(
  548. "/api/v1/users/me/change-password",
  549. headers={"Authorization": f"Bearer {user_token}"},
  550. json={
  551. "current_password": "oldpassword123",
  552. "new_password": "newpassword456",
  553. },
  554. )
  555. assert response.status_code == 200
  556. assert "success" in response.json()["message"].lower()
  557. # Verify can login with new password
  558. login_response = await async_client.post(
  559. "/api/v1/auth/login",
  560. json={"username": "pwchangeuser", "password": "newpassword456"},
  561. )
  562. assert login_response.status_code == 200
  563. @pytest.mark.asyncio
  564. @pytest.mark.integration
  565. async def test_change_password_wrong_current(self, async_client: AsyncClient, user_token: str):
  566. """Verify changing password fails with wrong current password."""
  567. response = await async_client.post(
  568. "/api/v1/users/me/change-password",
  569. headers={"Authorization": f"Bearer {user_token}"},
  570. json={
  571. "current_password": "wrongpassword",
  572. "new_password": "newpassword456",
  573. },
  574. )
  575. assert response.status_code == 400
  576. assert "incorrect" in response.json()["detail"].lower()
  577. @pytest.mark.asyncio
  578. @pytest.mark.integration
  579. async def test_change_password_requires_auth(self, async_client: AsyncClient):
  580. """Verify changing password requires authentication."""
  581. response = await async_client.post(
  582. "/api/v1/users/me/change-password",
  583. json={
  584. "current_password": "oldpassword",
  585. "new_password": "newpassword",
  586. },
  587. )
  588. assert response.status_code == 401
  589. class TestAuthMiddlewarePublicRoutes:
  590. """Tests for auth middleware public route configuration.
  591. These routes must be accessible without authentication, even when auth is enabled,
  592. because browser elements like <img src> and <video src> don't send Authorization headers.
  593. """
  594. @pytest.fixture
  595. async def enabled_auth(self, async_client: AsyncClient):
  596. """Enable auth for testing middleware behavior."""
  597. await async_client.post(
  598. "/api/v1/auth/setup",
  599. json={
  600. "auth_enabled": True,
  601. "admin_username": "middlewareadmin",
  602. "admin_password": "adminpassword123",
  603. },
  604. )
  605. @pytest.mark.asyncio
  606. @pytest.mark.integration
  607. async def test_auth_status_is_public(self, async_client: AsyncClient, enabled_auth):
  608. """Verify /api/v1/auth/status is accessible without auth."""
  609. response = await async_client.get("/api/v1/auth/status")
  610. assert response.status_code == 200
  611. assert "auth_enabled" in response.json()
  612. @pytest.mark.asyncio
  613. @pytest.mark.integration
  614. async def test_auth_login_is_public(self, async_client: AsyncClient, enabled_auth):
  615. """Verify /api/v1/auth/login is accessible without auth."""
  616. response = await async_client.post(
  617. "/api/v1/auth/login",
  618. json={"username": "middlewareadmin", "password": "adminpassword123"},
  619. )
  620. # Should not return 401 (unauthorized) - it should either succeed or return
  621. # a different error (like 400 for wrong credentials)
  622. assert response.status_code != 401 or "token" in response.json()
  623. @pytest.mark.asyncio
  624. @pytest.mark.integration
  625. async def test_auth_setup_is_public(self, async_client: AsyncClient):
  626. """Verify /api/v1/auth/setup is accessible without auth (needed for setup/recovery)."""
  627. # Don't enable auth first - test that setup endpoint itself is accessible
  628. response = await async_client.post(
  629. "/api/v1/auth/setup",
  630. json={"auth_enabled": False},
  631. )
  632. # Should not be 401
  633. assert response.status_code != 401
  634. @pytest.mark.asyncio
  635. @pytest.mark.integration
  636. async def test_updates_version_is_public(self, async_client: AsyncClient, enabled_auth):
  637. """Verify /api/v1/updates/version is accessible without auth."""
  638. response = await async_client.get("/api/v1/updates/version")
  639. # Should not be 401
  640. assert response.status_code != 401
  641. @pytest.mark.asyncio
  642. @pytest.mark.integration
  643. async def test_protected_route_requires_auth(self, async_client: AsyncClient, enabled_auth):
  644. """Verify non-public routes return 401 without token."""
  645. response = await async_client.get("/api/v1/printers/")
  646. assert response.status_code == 401
  647. @pytest.mark.asyncio
  648. @pytest.mark.integration
  649. async def test_protected_route_works_with_token(self, async_client: AsyncClient, enabled_auth):
  650. """Verify non-public routes work with valid token."""
  651. # Login to get token
  652. login_response = await async_client.post(
  653. "/api/v1/auth/login",
  654. json={"username": "middlewareadmin", "password": "adminpassword123"},
  655. )
  656. token = login_response.json()["access_token"]
  657. # Access protected route
  658. response = await async_client.get(
  659. "/api/v1/printers/",
  660. headers={"Authorization": f"Bearer {token}"},
  661. )
  662. assert response.status_code == 200