pytest-neon 2.1.2__py3-none-any.whl → 2.1.4__py3-none-any.whl

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.
pytest_neon/__init__.py CHANGED
@@ -9,7 +9,7 @@ from pytest_neon.plugin import (
9
9
  neon_engine,
10
10
  )
11
11
 
12
- __version__ = "2.1.2"
12
+ __version__ = "2.1.4"
13
13
  __all__ = [
14
14
  "NeonBranch",
15
15
  "neon_branch",
pytest_neon/plugin.py CHANGED
@@ -67,6 +67,50 @@ def _get_xdist_worker_id() -> str:
67
67
  return os.environ.get("PYTEST_XDIST_WORKER", "main")
68
68
 
69
69
 
70
+ def _sanitize_branch_name(name: str) -> str:
71
+ """
72
+ Sanitize a string for use in Neon branch names.
73
+
74
+ Only allows alphanumeric characters, hyphens, and underscores.
75
+ All other characters (including non-ASCII) are replaced with hyphens.
76
+ """
77
+ import re
78
+
79
+ # Replace anything that's not alphanumeric, hyphen, or underscore with hyphen
80
+ sanitized = re.sub(r"[^a-zA-Z0-9_-]", "-", name)
81
+ # Collapse multiple hyphens into one
82
+ sanitized = re.sub(r"-+", "-", sanitized)
83
+ # Remove leading/trailing hyphens
84
+ sanitized = sanitized.strip("-")
85
+ return sanitized
86
+
87
+
88
+ def _get_git_branch_name() -> str | None:
89
+ """
90
+ Get the current git branch name (sanitized), or None if not in a git repo.
91
+
92
+ Used to include the git branch in Neon branch names, making it easier
93
+ to identify which git branch/PR created orphaned test branches.
94
+
95
+ The branch name is sanitized to replace special characters with hyphens.
96
+ """
97
+ import subprocess
98
+
99
+ try:
100
+ result = subprocess.run(
101
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
102
+ capture_output=True,
103
+ text=True,
104
+ timeout=5,
105
+ )
106
+ if result.returncode == 0:
107
+ branch = result.stdout.strip()
108
+ return _sanitize_branch_name(branch) if branch else None
109
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
110
+ pass
111
+ return None
112
+
113
+
70
114
  def _get_schema_fingerprint(connection_string: str) -> tuple[tuple[Any, ...], ...]:
71
115
  """
72
116
  Get a fingerprint of the database schema for change detection.
@@ -110,6 +154,30 @@ class NeonBranch:
110
154
  parent_id: str | None = None
111
155
 
112
156
 
157
+ def _get_default_branch_id(neon: NeonAPI, project_id: str) -> str | None:
158
+ """
159
+ Get the default/primary branch ID for a project.
160
+
161
+ This is used as a safety check to ensure we never accidentally
162
+ perform destructive operations (like password reset) on the
163
+ production branch.
164
+
165
+ Returns:
166
+ The branch ID of the default branch, or None if not found.
167
+ """
168
+ try:
169
+ response = neon.branches(project_id=project_id)
170
+ for branch in response.branches:
171
+ # Check both 'default' and 'primary' flags for compatibility
172
+ if getattr(branch, "default", False) or getattr(branch, "primary", False):
173
+ return branch.id
174
+ except Exception:
175
+ # If we can't fetch branches, don't block - the safety check
176
+ # will be skipped but tests can still run
177
+ pass
178
+ return None
179
+
180
+
113
181
  def pytest_addoption(parser: pytest.Parser) -> None:
114
182
  """Add Neon-specific command line options and ini settings."""
115
183
  group = parser.getgroup("neon", "Neon database branching")
@@ -279,8 +347,21 @@ def _create_neon_branch(
279
347
 
280
348
  neon = NeonAPI(api_key=api_key)
281
349
 
350
+ # Cache the default branch ID for safety checks (only fetch once per session)
351
+ if not hasattr(config, "_neon_default_branch_id"):
352
+ config._neon_default_branch_id = _get_default_branch_id(neon, project_id) # type: ignore[attr-defined]
353
+
282
354
  # Generate unique branch name
283
- branch_name = f"pytest-{os.urandom(4).hex()}{branch_name_suffix}"
355
+ # Format: pytest-[git branch (first 15 chars)]-[random]-[suffix]
356
+ # This helps identify orphaned branches by showing which git branch created them
357
+ random_suffix = os.urandom(2).hex() # 2 bytes = 4 hex chars
358
+ git_branch = _get_git_branch_name()
359
+ if git_branch:
360
+ # Truncate git branch to 15 chars to keep branch names reasonable
361
+ git_prefix = git_branch[:15]
362
+ branch_name = f"pytest-{git_prefix}-{random_suffix}{branch_name_suffix}"
363
+ else:
364
+ branch_name = f"pytest-{random_suffix}{branch_name_suffix}"
284
365
 
285
366
  # Build branch creation payload
286
367
  branch_config: dict[str, Any] = {"name": branch_name}
@@ -341,6 +422,18 @@ def _create_neon_branch(
341
422
 
342
423
  host = endpoint.host
343
424
 
425
+ # SAFETY CHECK: Ensure we never reset password on the default/production branch
426
+ # This should be impossible since we just created this branch, but we check
427
+ # defensively to prevent catastrophic mistakes if there's ever a bug
428
+ default_branch_id = getattr(config, "_neon_default_branch_id", None)
429
+ if default_branch_id and branch.id == default_branch_id:
430
+ raise RuntimeError(
431
+ f"SAFETY CHECK FAILED: Attempted to reset password on default branch "
432
+ f"{branch.id}. This should never happen - the plugin creates new "
433
+ f"branches and should never operate on the default branch. "
434
+ f"Please report this bug at https://github.com/ZainRizvi/pytest-neon/issues"
435
+ )
436
+
344
437
  # Reset password to get the password value
345
438
  # (newly created branches don't expose password)
346
439
  password_response = neon.role_password_reset(
@@ -502,10 +595,10 @@ def _wait_for_operations(
502
595
  op_data = response.json().get("operation", {})
503
596
  status = op_data.get("status")
504
597
 
505
- if status == "error":
598
+ if status == "failed":
506
599
  err = op_data.get("error", "unknown error")
507
600
  raise RuntimeError(f"Operation {op_id} failed: {err}")
508
- if status not in ("finished", "skipped"):
601
+ if status not in ("finished", "skipped", "cancelled"):
509
602
  still_pending.append(op_id)
510
603
  except requests.RequestException:
511
604
  # On network error, assume still pending and retry
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-neon
3
- Version: 2.1.2
3
+ Version: 2.1.4
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
@@ -0,0 +1,8 @@
1
+ pytest_neon/__init__.py,sha256=bWWilGWaSJW2ofj0YsKqdC7r972s0c2Ar5DcE1Tn0Oc,398
2
+ pytest_neon/plugin.py,sha256=A9qv1mv0CXez-SDAWruzVxjRFp0YdSWkU-8j9TWV_LA,41770
3
+ pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pytest_neon-2.1.4.dist-info/METADATA,sha256=dmGCHSNi32pEA5wG6fZhjoFrcirza1kO16RdRSmyEJs,18734
5
+ pytest_neon-2.1.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ pytest_neon-2.1.4.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
7
+ pytest_neon-2.1.4.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
8
+ pytest_neon-2.1.4.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pytest_neon/__init__.py,sha256=EKni9NpcUiIPy8J8vj-s6oKH6S98JlNs5JYuF151cMA,398
2
- pytest_neon/plugin.py,sha256=r30wz96rQZerAiAF8wAVmWPksF6lM2dsJyUNj2X9rWo,38038
3
- pytest_neon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- pytest_neon-2.1.2.dist-info/METADATA,sha256=QA7xaOCbi_wD8LTwb8gWQpZe8rG8BWcMrETS78HUOks,18734
5
- pytest_neon-2.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
- pytest_neon-2.1.2.dist-info/entry_points.txt,sha256=5U88Idj_G8-PSDb9VF3OwYFbGLHnGOo_GxgYvi0dtXw,37
7
- pytest_neon-2.1.2.dist-info/licenses/LICENSE,sha256=aKKp_Ex4WBHTByY4BhXJ181dzB_qYhi2pCUmZ7Spn_0,1067
8
- pytest_neon-2.1.2.dist-info/RECORD,,