Skip to content

Commit 565ab9c

Browse files
kwschulzclaude
andcommitted
test: add table-driven tests and pre-push CI validation
- Add table-driven tests for Hasher class (59% → 98% coverage) - Add table-driven tests for validation/secrets (63% → 72% coverage) - Add pre-push hook to run pytest before pushing - Add scripts/ci-local.sh for local CI validation - Fix PT006 lint errors (parametrize tuple syntax) - Fix mypy type narrowing errors - Update pre-commit ruff to v0.9.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d4c7565 commit 565ab9c

8 files changed

Lines changed: 819 additions & 22 deletions

File tree

.pre-commit-config.yaml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Update: pre-commit autoupdate
55

66
default_language_version:
7-
python: python3.10
7+
python: python3
88

99
repos:
1010
# ==========================================================================
@@ -36,7 +36,7 @@ repos:
3636
# Ruff - Linting & Formatting
3737
# ==========================================================================
3838
- repo: https://github.com/astral-sh/ruff-pre-commit
39-
rev: v0.5.0
39+
rev: v0.9.0
4040
hooks:
4141
# Run the linter
4242
- id: ruff
@@ -76,6 +76,19 @@ repos:
7676
- id: commitizen
7777
stages: [commit-msg]
7878

79+
# ==========================================================================
80+
# Pre-push: Full test suite
81+
# ==========================================================================
82+
- repo: local
83+
hooks:
84+
- id: pytest
85+
name: pytest
86+
entry: pytest --tb=short -q
87+
language: system
88+
pass_filenames: false
89+
always_run: true
90+
stages: [pre-push]
91+
7992
# ==========================================================================
8093
# Documentation
8194
# ==========================================================================

CONTRIBUTING.md

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Thank you for your interest in contributing to har-capture!
1111
cd har-capture
1212
```
1313

14-
2. **Create a virtual environment**
14+
1. **Create a virtual environment**
1515

1616
```bash
1717
python -m venv .venv
@@ -20,19 +20,26 @@ Thank you for your interest in contributing to har-capture!
2020
.venv\Scripts\activate # Windows
2121
```
2222

23-
3. **Install in development mode**
23+
1. **Install in development mode**
2424

2525
```bash
2626
pip install -e ".[dev,full]"
2727
```
2828

29-
4. **Install pre-commit hooks**
29+
1. **Install pre-commit hooks**
3030

3131
```bash
3232
pre-commit install
3333
pre-commit install --hook-type commit-msg
34+
pre-commit install --hook-type pre-push
3435
```
3536

37+
This installs:
38+
39+
- **pre-commit**: Runs ruff lint/format on staged files
40+
- **commit-msg**: Validates commit message format
41+
- **pre-push**: Runs full test suite before push
42+
3643
## Code Quality Standards
3744

3845
This project enforces strict quality standards:
@@ -96,6 +103,7 @@ type(scope): description
96103
```
97104

98105
Types:
106+
99107
- `feat`: New feature
100108
- `fix`: Bug fix
101109
- `docs`: Documentation only
@@ -106,6 +114,7 @@ Types:
106114
- `chore`: Maintenance tasks
107115

108116
Examples:
117+
109118
```
110119
feat(sanitization): add WiFi credential detection
111120
fix(cli): handle missing output directory
@@ -116,30 +125,38 @@ test(validation): add PII detection tests
116125
## Pull Request Process
117126

118127
1. **Create a branch** from `main`:
128+
119129
```bash
120130
git checkout -b feat/my-feature
121131
```
122132

123-
2. **Make your changes** following the code standards above
133+
1. **Make your changes** following the code standards above
134+
135+
1. **Run all checks locally**:
124136

125-
3. **Run all checks locally**:
126137
```bash
138+
# One command to run everything CI runs:
139+
./scripts/ci-local.sh
140+
141+
# Or run each check separately:
127142
pre-commit run --all-files
128143
pytest
129144
mypy src/
130145
```
131146

132-
4. **Push and create a PR**:
147+
1. **Push and create a PR**:
148+
133149
```bash
134150
git push -u origin feat/my-feature
135151
```
136152

137-
5. **Fill out the PR template** with:
153+
1. **Fill out the PR template** with:
154+
138155
- Description of changes
139156
- Related issues
140157
- Testing performed
141158

142-
6. **Wait for CI** to pass and address any feedback
159+
1. **Wait for CI** to pass and address any feedback
143160

144161
## Project Structure
145162

@@ -158,10 +175,10 @@ har-capture/
158175
## Adding New Features
159176

160177
1. **Write tests first** (TDD encouraged)
161-
2. **Add type hints** to all functions
162-
3. **Add docstrings** (Google style)
163-
4. **Update CHANGELOG.md** under `[Unreleased]`
164-
5. **Update README.md** if adding user-facing features
178+
1. **Add type hints** to all functions
179+
1. **Add docstrings** (Google style)
180+
1. **Update CHANGELOG.md** under `[Unreleased]`
181+
1. **Update README.md** if adding user-facing features
165182

166183
## Dependencies
167184

@@ -177,7 +194,7 @@ har-capture/
177194

178195
Thank you for contributing!
179196

180-
---
197+
______________________________________________________________________
181198

182199
## Maintainer Setup
183200

@@ -186,9 +203,11 @@ One-time setup for repository maintainers:
186203
### GitHub Repository Settings
187204

188205
1. **Enable private vulnerability reporting**
206+
189207
- Settings → Code security and analysis → Private vulnerability reporting → Enable
190208

191-
2. **Create GitHub Environments**
209+
1. **Create GitHub Environments**
210+
192211
- Settings → Environments → New environment
193212
- Create `pypi` (for production releases)
194213
- Create `test-pypi` (for test releases)
@@ -198,13 +217,15 @@ One-time setup for repository maintainers:
198217
Configure OIDC publishing (no API tokens needed):
199218

200219
1. **PyPI:** https://pypi.org/manage/account/publishing/
220+
201221
- Add publisher
202222
- Owner: `solentlabs`
203223
- Repository: `har-capture`
204224
- Workflow: `publish.yml`
205225
- Environment: `pypi`
206226

207-
2. **Test PyPI:** https://test.pypi.org/manage/account/publishing/
227+
1. **Test PyPI:** https://test.pypi.org/manage/account/publishing/
228+
208229
- Same settings, environment: `test-pypi`
209230

210231
### Releasing

scripts/ci-local.sh

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env bash
2+
# Run the same checks as CI locally
3+
# Usage: ./scripts/ci-local.sh [--quick]
4+
#
5+
# --quick: Skip slow tests (pytest -m "not slow")
6+
7+
set -e
8+
9+
# Colors for output
10+
RED='\033[0;31m'
11+
GREEN='\033[0;32m'
12+
YELLOW='\033[1;33m'
13+
NC='\033[0m' # No Color
14+
15+
# Auto-activate virtual environment if not already active
16+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
18+
19+
if [ -z "$VIRTUAL_ENV" ]; then
20+
if [ -f "$REPO_ROOT/.venv/bin/activate" ]; then
21+
source "$REPO_ROOT/.venv/bin/activate"
22+
elif [ -f "$REPO_ROOT/venv/bin/activate" ]; then
23+
source "$REPO_ROOT/venv/bin/activate"
24+
fi
25+
fi
26+
27+
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
28+
echo -e "${YELLOW} Running local CI checks ${NC}"
29+
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
30+
31+
# Parse arguments
32+
QUICK=false
33+
for arg in "$@"; do
34+
case $arg in
35+
--quick)
36+
QUICK=true
37+
shift
38+
;;
39+
esac
40+
done
41+
42+
# Track failures
43+
FAILED=0
44+
45+
# Step 1: Ruff lint
46+
echo -e "\n${YELLOW}[1/3] Running ruff check...${NC}"
47+
if ruff check .; then
48+
echo -e "${GREEN}✓ Ruff check passed${NC}"
49+
else
50+
echo -e "${RED}✗ Ruff check failed${NC}"
51+
FAILED=1
52+
fi
53+
54+
# Step 2: Ruff format check
55+
echo -e "\n${YELLOW}[2/3] Checking ruff format...${NC}"
56+
if ruff format --check .; then
57+
echo -e "${GREEN}✓ Ruff format check passed${NC}"
58+
else
59+
echo -e "${RED}✗ Ruff format check failed${NC}"
60+
echo -e "${YELLOW} Run 'ruff format .' to fix${NC}"
61+
FAILED=1
62+
fi
63+
64+
# Step 3: Pytest
65+
echo -e "\n${YELLOW}[3/3] Running pytest...${NC}"
66+
if [ "$QUICK" = true ]; then
67+
echo -e "${YELLOW} (quick mode - skipping slow tests)${NC}"
68+
if pytest --tb=short -q -m "not slow"; then
69+
echo -e "${GREEN}✓ Tests passed${NC}"
70+
else
71+
echo -e "${RED}✗ Tests failed${NC}"
72+
FAILED=1
73+
fi
74+
else
75+
if pytest --tb=short -q; then
76+
echo -e "${GREEN}✓ Tests passed${NC}"
77+
else
78+
echo -e "${RED}✗ Tests failed${NC}"
79+
FAILED=1
80+
fi
81+
fi
82+
83+
# Summary
84+
echo -e "\n${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
85+
if [ $FAILED -eq 0 ]; then
86+
echo -e "${GREEN}✓ All CI checks passed - safe to push${NC}"
87+
exit 0
88+
else
89+
echo -e "${RED}✗ CI checks failed - fix before pushing${NC}"
90+
exit 1
91+
fi

src/har_capture/capture/browser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ def _is_missing_deps_error(error_msg: str) -> bool:
376376
if not keep_raw and result.sanitized_path and result.sanitized_path.exists():
377377
try:
378378
output_path.unlink()
379-
result.har_path = None # type: ignore[assignment]
379+
result.har_path = None
380380
except Exception as e:
381381
_LOGGER.warning("Failed to delete raw HAR: %s", e)
382382
except Exception as e:

src/har_capture/cli/capture.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def capture(
135135

136136
if requires_basic:
137137
typer.echo(f" Detected: HTTP Basic Auth{f' ({realm})' if realm else ''}")
138-
if username and password:
138+
if username is not None and password is not None:
139139
http_credentials = {"username": username, "password": password}
140140
else:
141141
typer.echo()
@@ -144,9 +144,9 @@ def capture(
144144
else:
145145
typer.echo("This site requires HTTP Basic Authentication.")
146146
typer.echo()
147-
username = typer.prompt("Username", default="admin")
148-
password = typer.prompt("Password", hide_input=True)
149-
http_credentials = {"username": username, "password": password}
147+
prompted_user = typer.prompt("Username", default="admin")
148+
prompted_pass = typer.prompt("Password", hide_input=True)
149+
http_credentials = {"username": prompted_user, "password": prompted_pass or ""}
150150
else:
151151
typer.echo(" Detected: Form-based or no auth required")
152152

0 commit comments

Comments
 (0)