pytest-neon 2.3.1__tar.gz → 2.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/PKG-INFO +1 -1
  2. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/pyproject.toml +1 -1
  3. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/src/pytest_neon/__init__.py +1 -1
  4. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/src/pytest_neon/plugin.py +44 -27
  5. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_branch_name_prefix.py +6 -12
  6. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/uv.lock +1 -1
  7. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/.config/wt.toml +0 -0
  8. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/.env.example +0 -0
  9. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/.github/workflows/release.yml +0 -0
  10. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/.github/workflows/tests.yml +0 -0
  11. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/.gitignore +0 -0
  12. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/.neon +0 -0
  13. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/CLAUDE.md +0 -0
  14. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/LICENSE +0 -0
  15. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/README.md +0 -0
  16. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/src/pytest_neon/py.typed +0 -0
  17. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/conftest.py +0 -0
  18. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_branch_lifecycle.py +0 -0
  19. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_cli_options.py +0 -0
  20. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_default_branch_safety.py +0 -0
  21. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_dirty_isolated_fixtures.py +0 -0
  22. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_env_var.py +0 -0
  23. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_fixture_errors.py +0 -0
  24. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_integration.py +0 -0
  25. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_migrations.py +0 -0
  26. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_readwrite_readonly_fixtures.py +0 -0
  27. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_reset_behavior.py +0 -0
  28. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_service_classes.py +0 -0
  29. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_skip_behavior.py +0 -0
  30. {pytest_neon-2.3.1 → pytest_neon-2.3.2}/tests/test_xdist_worker_support.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-neon
3
- Version: 2.3.1
3
+ Version: 2.3.2
4
4
  Summary: Pytest plugin for Neon database branch isolation in tests
5
5
  Project-URL: Homepage, https://github.com/ZainRizvi/pytest-neon
6
6
  Project-URL: Repository, https://github.com/ZainRizvi/pytest-neon
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytest-neon"
7
- version = "2.3.1"
7
+ version = "2.3.2"
8
8
  description = "Pytest plugin for Neon database branch isolation in tests"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -9,7 +9,7 @@ from pytest_neon.plugin import (
9
9
  neon_engine,
10
10
  )
11
11
 
12
- __version__ = "2.3.1"
12
+ __version__ = "2.3.2"
13
13
  __all__ = [
14
14
  "NeonBranch",
15
15
  "neon_branch",
@@ -306,6 +306,37 @@ def _extract_password_from_connection_string(connection_string: str) -> str:
306
306
  raise ValueError(f"No password found in connection string: {connection_string}")
307
307
 
308
308
 
309
+ def _reveal_role_password(
310
+ api_key: str, project_id: str, branch_id: str, role_name: str
311
+ ) -> str:
312
+ """
313
+ Get the password for a role WITHOUT resetting it.
314
+
315
+ Uses Neon's reveal_password API endpoint (GET request).
316
+
317
+ Note: The neon-api library has a bug where it uses POST instead of GET,
318
+ so we make the request directly.
319
+ """
320
+ url = (
321
+ f"https://console.neon.tech/api/v2/projects/{project_id}"
322
+ f"/branches/{branch_id}/roles/{role_name}/reveal_password"
323
+ )
324
+ headers = {
325
+ "Authorization": f"Bearer {api_key}",
326
+ "Accept": "application/json",
327
+ }
328
+
329
+ response = requests.get(url, headers=headers, timeout=30)
330
+ try:
331
+ response.raise_for_status()
332
+ except requests.exceptions.HTTPError:
333
+ # Wrap in NeonAPIError for consistent error handling
334
+ raise NeonAPIError(response.text) from None
335
+
336
+ data = response.json()
337
+ return data["password"]
338
+
339
+
309
340
  def _get_schema_fingerprint(connection_string: str) -> tuple[tuple[Any, ...], ...]:
310
341
  """
311
342
  Get a fingerprint of the database schema for change detection.
@@ -508,7 +539,7 @@ class NeonBranchManager:
508
539
  )
509
540
 
510
541
  # Get password
511
- connection_string = self._reset_password_and_build_connection_string(
542
+ connection_string = self._get_password_and_build_connection_string(
512
543
  branch.id, host
513
544
  )
514
545
 
@@ -632,19 +663,19 @@ class NeonBranchManager:
632
663
  time.sleep(poll_interval)
633
664
  waited += poll_interval
634
665
 
635
- def _reset_password_and_build_connection_string(
666
+ def _get_password_and_build_connection_string(
636
667
  self, branch_id: str, host: str
637
668
  ) -> str:
638
- """Reset role password and build connection string."""
639
- password_response = _retry_on_rate_limit(
640
- lambda: self._neon.role_password_reset(
669
+ """Get role password (without resetting) and build connection string."""
670
+ password = _retry_on_rate_limit(
671
+ lambda: _reveal_role_password(
672
+ api_key=self.config.api_key,
641
673
  project_id=self.config.project_id,
642
674
  branch_id=branch_id,
643
675
  role_name=self.config.role_name,
644
676
  ),
645
- operation_name="role_password_reset",
677
+ operation_name="role_password_reveal",
646
678
  )
647
- password = password_response.role.password
648
679
 
649
680
  return (
650
681
  f"postgresql://{self.config.role_name}:{password}@{host}/"
@@ -1047,31 +1078,17 @@ def _create_neon_branch(
1047
1078
 
1048
1079
  host = endpoint.host
1049
1080
 
1050
- # SAFETY CHECK: Ensure we never reset password on the default/production branch
1051
- # This should be impossible since we just created this branch, but we check
1052
- # defensively to prevent catastrophic mistakes if there's ever a bug
1053
- default_branch_id = getattr(config, "_neon_default_branch_id", None)
1054
- if default_branch_id and branch.id == default_branch_id:
1055
- raise RuntimeError(
1056
- f"SAFETY CHECK FAILED: Attempted to reset password on default branch "
1057
- f"{branch.id}. This should never happen - the plugin creates new "
1058
- f"branches and should never operate on the default branch. "
1059
- f"Please report this bug at https://github.com/ZainRizvi/pytest-neon/issues"
1060
- )
1061
-
1062
- # Reset password to get the password value
1063
- # (newly created branches don't expose password)
1064
- # Wrap in retry logic to handle rate limits
1065
- # See: https://api-docs.neon.tech/reference/api-rate-limiting
1066
- password_response = _retry_on_rate_limit(
1067
- lambda: neon.role_password_reset(
1081
+ # Get password using reveal (not reset) to avoid invalidating existing connections
1082
+ # See: https://api-docs.neon.tech/reference/getprojectbranchrolepassword
1083
+ password = _retry_on_rate_limit(
1084
+ lambda: _reveal_role_password(
1085
+ api_key=api_key,
1068
1086
  project_id=project_id,
1069
1087
  branch_id=branch.id,
1070
1088
  role_name=role_name,
1071
1089
  ),
1072
- operation_name="role_password_reset",
1090
+ operation_name="role_password_reveal",
1073
1091
  )
1074
- password = password_response.role.password
1075
1092
 
1076
1093
  # Build connection string
1077
1094
  connection_string = (
@@ -136,9 +136,11 @@ class TestBranchNameWithGitBranch:
136
136
  with (
137
137
  patch("pytest_neon.plugin.NeonAPI") as mock_neon_cls,
138
138
  patch("pytest_neon.plugin._get_git_branch_name") as mock_git,
139
+ patch("pytest_neon.plugin._reveal_role_password") as mock_reveal,
139
140
  ):
140
141
  # _get_git_branch_name returns sanitized value (slashes -> hyphens)
141
142
  mock_git.return_value = "feature-my-branch"
143
+ mock_reveal.return_value = "testpass"
142
144
 
143
145
  mock_api = MagicMock()
144
146
  mock_neon_cls.return_value = mock_api
@@ -163,10 +165,6 @@ class TestBranchNameWithGitBranch:
163
165
  mock_endpoint_response.endpoint.host = "test.neon.tech"
164
166
  mock_api.endpoint.return_value = mock_endpoint_response
165
167
 
166
- mock_password = MagicMock()
167
- mock_password.role.password = "testpass"
168
- mock_api.role_password_reset.return_value = mock_password
169
-
170
168
  gen = _create_neon_branch(mock_request, branch_name_suffix="-migrated")
171
169
  with contextlib.suppress(StopIteration):
172
170
  next(gen)
@@ -210,9 +208,11 @@ class TestBranchNameWithGitBranch:
210
208
  with (
211
209
  patch("pytest_neon.plugin.NeonAPI") as mock_neon_cls,
212
210
  patch("pytest_neon.plugin._get_git_branch_name") as mock_git,
211
+ patch("pytest_neon.plugin._reveal_role_password") as mock_reveal,
213
212
  ):
214
213
  # _get_git_branch_name returns sanitized value
215
214
  mock_git.return_value = "feature-very-long-branch-name-truncated"
215
+ mock_reveal.return_value = "testpass"
216
216
 
217
217
  mock_api = MagicMock()
218
218
  mock_neon_cls.return_value = mock_api
@@ -237,10 +237,6 @@ class TestBranchNameWithGitBranch:
237
237
  mock_endpoint_response.endpoint.host = "test.neon.tech"
238
238
  mock_api.endpoint.return_value = mock_endpoint_response
239
239
 
240
- mock_password = MagicMock()
241
- mock_password.role.password = "testpass"
242
- mock_api.role_password_reset.return_value = mock_password
243
-
244
240
  gen = _create_neon_branch(mock_request, branch_name_suffix="-migrated")
245
241
  with contextlib.suppress(StopIteration):
246
242
  next(gen)
@@ -284,8 +280,10 @@ class TestBranchNameWithGitBranch:
284
280
  with (
285
281
  patch("pytest_neon.plugin.NeonAPI") as mock_neon_cls,
286
282
  patch("pytest_neon.plugin._get_git_branch_name") as mock_git,
283
+ patch("pytest_neon.plugin._reveal_role_password") as mock_reveal,
287
284
  ):
288
285
  mock_git.return_value = None # Not in a git repo
286
+ mock_reveal.return_value = "testpass"
289
287
 
290
288
  mock_api = MagicMock()
291
289
  mock_neon_cls.return_value = mock_api
@@ -310,10 +308,6 @@ class TestBranchNameWithGitBranch:
310
308
  mock_endpoint_response.endpoint.host = "test.neon.tech"
311
309
  mock_api.endpoint.return_value = mock_endpoint_response
312
310
 
313
- mock_password = MagicMock()
314
- mock_password.role.password = "testpass"
315
- mock_api.role_password_reset.return_value = mock_password
316
-
317
311
  gen = _create_neon_branch(mock_request, branch_name_suffix="-migrated")
318
312
  with contextlib.suppress(StopIteration):
319
313
  next(gen)
@@ -1212,7 +1212,7 @@ wheels = [
1212
1212
 
1213
1213
  [[package]]
1214
1214
  name = "pytest-neon"
1215
- version = "2.3.1"
1215
+ version = "2.3.2"
1216
1216
  source = { editable = "." }
1217
1217
  dependencies = [
1218
1218
  { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes