@theihtisham/dev-pulse 1.0.0

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.
@@ -0,0 +1,313 @@
1
+ """Tests for the FastAPI web API."""
2
+
3
+ import json
4
+
5
+ import pytest
6
+ from httpx import AsyncClient, ASGITransport
7
+
8
+ from devpulse.api.app import app
9
+
10
+
11
+ @pytest.fixture
12
+ def client(populated_db):
13
+ """Provide a test client with populated database."""
14
+ # Override DB in the app to use test DB
15
+ from devpulse.api import app as app_module
16
+
17
+ original_get_db = app_module._get_db
18
+ original_get_engine = app_module._get_engine
19
+ original_get_gen = app_module._get_report_gen
20
+
21
+ app_module._get_db = lambda: populated_db
22
+ app_module._get_engine = lambda: __import__("devpulse.core.analytics", fromlist=["AnalyticsEngine"]).AnalyticsEngine(db=populated_db)
23
+ app_module._get_report_gen = lambda: __import__("devpulse.core.report_generator", fromlist=["ReportGenerator"]).ReportGenerator(db=populated_db)
24
+
25
+ transport = ASGITransport(app=app)
26
+ yield AsyncClient(transport=transport, base_url="http://test")
27
+
28
+ app_module._get_db = original_get_db
29
+ app_module._get_engine = original_get_engine
30
+ app_module._get_gen = original_get_gen
31
+
32
+
33
+ class TestHealthCheck:
34
+ """Test API health check endpoints."""
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_root(self, client):
38
+ async with client as c:
39
+ response = await c.get("/")
40
+ assert response.status_code == 200
41
+ data = response.json()
42
+ assert data["name"] == "DevPulse API"
43
+ assert data["status"] == "healthy"
44
+ assert "version" in data
45
+
46
+
47
+ class TestCommitEndpoints:
48
+ """Test commit API endpoints."""
49
+
50
+ @pytest.mark.asyncio
51
+ async def test_list_commits(self, client):
52
+ async with client as c:
53
+ response = await c.get("/api/commits")
54
+ assert response.status_code == 200
55
+ data = response.json()
56
+ assert len(data) == 4
57
+
58
+ @pytest.mark.asyncio
59
+ async def test_filter_commits_by_author(self, client):
60
+ async with client as c:
61
+ response = await c.get("/api/commits", params={"author": "Alice"})
62
+ assert response.status_code == 200
63
+ data = response.json()
64
+ assert len(data) == 3
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_commit_heatmap(self, client):
68
+ async with client as c:
69
+ response = await c.get("/api/commits/heatmap", params={"days": 30})
70
+ assert response.status_code == 200
71
+ data = response.json()
72
+ assert len(data) >= 0
73
+
74
+
75
+ class TestPullRequestEndpoints:
76
+ """Test pull request API endpoints."""
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_list_prs(self, client):
80
+ async with client as c:
81
+ response = await c.get("/api/pull-requests")
82
+ assert response.status_code == 200
83
+ data = response.json()
84
+ assert len(data) == 3
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_filter_prs_by_state(self, client):
88
+ async with client as c:
89
+ response = await c.get("/api/pull-requests", params={"state": "merged"})
90
+ assert response.status_code == 200
91
+ data = response.json()
92
+ assert len(data) == 2
93
+
94
+
95
+ class TestIssueEndpoints:
96
+ """Test issue API endpoints."""
97
+
98
+ @pytest.mark.asyncio
99
+ async def test_list_issues(self, client):
100
+ async with client as c:
101
+ response = await c.get("/api/issues")
102
+ assert response.status_code == 200
103
+ data = response.json()
104
+ assert len(data) == 3
105
+
106
+ @pytest.mark.asyncio
107
+ async def test_filter_issues_by_state(self, client):
108
+ async with client as c:
109
+ response = await c.get("/api/issues", params={"state": "open"})
110
+ assert response.status_code == 200
111
+ data = response.json()
112
+ assert len(data) == 2
113
+
114
+
115
+ class TestMetricsEndpoints:
116
+ """Test metrics API endpoints."""
117
+
118
+ @pytest.mark.asyncio
119
+ async def test_developer_metrics(self, client):
120
+ async with client as c:
121
+ response = await c.get("/api/metrics/Alice", params={"days": 30})
122
+ assert response.status_code == 200
123
+ data = response.json()
124
+ assert data["author"] == "Alice"
125
+ assert data["commits_count"] == 3
126
+
127
+ @pytest.mark.asyncio
128
+ async def test_team_metrics(self, client):
129
+ async with client as c:
130
+ response = await c.get("/api/metrics", params={"days": 30})
131
+ assert response.status_code == 200
132
+ data = response.json()
133
+ assert len(data) >= 2
134
+
135
+
136
+ class TestHealthEndpoint:
137
+ """Test team health API endpoint."""
138
+
139
+ @pytest.mark.asyncio
140
+ async def test_team_health(self, client):
141
+ async with client as c:
142
+ response = await c.get("/api/health", params={"days": 30})
143
+ assert response.status_code == 200
144
+ data = response.json()
145
+ assert "overall_score" in data
146
+ assert "burnout_risk" in data
147
+ assert "recommendations" in data
148
+
149
+
150
+ class TestInsightsEndpoint:
151
+ """Test AI insights API endpoint."""
152
+
153
+ @pytest.mark.asyncio
154
+ async def test_get_insights(self, client):
155
+ async with client as c:
156
+ response = await c.get("/api/insights", params={"days": 30})
157
+ assert response.status_code == 200
158
+ data = response.json()
159
+ assert isinstance(data, list)
160
+ assert len(data) > 0
161
+ for insight in data:
162
+ assert "message" in insight
163
+ assert "recommendation" in insight
164
+
165
+
166
+ class TestGoalEndpoints:
167
+ """Test goals API endpoints."""
168
+
169
+ @pytest.mark.asyncio
170
+ async def test_create_goal(self, client):
171
+ async with client as c:
172
+ response = await c.post("/api/goals", json={
173
+ "title": "Test goal",
174
+ "target_value": 10,
175
+ "metric": "commits_per_day",
176
+ })
177
+ assert response.status_code == 201
178
+ data = response.json()
179
+ assert data["title"] == "Test goal"
180
+
181
+ @pytest.mark.asyncio
182
+ async def test_list_goals(self, client):
183
+ async with client as c:
184
+ # Create first
185
+ await c.post("/api/goals", json={
186
+ "title": "Goal 1",
187
+ "target_value": 5,
188
+ "metric": "test",
189
+ })
190
+ # Then list
191
+ response = await c.get("/api/goals")
192
+ assert response.status_code == 200
193
+ data = response.json()
194
+ assert len(data) >= 1
195
+
196
+ @pytest.mark.asyncio
197
+ async def test_delete_goal(self, client):
198
+ async with client as c:
199
+ create = await c.post("/api/goals", json={
200
+ "title": "Delete me",
201
+ "target_value": 1,
202
+ "metric": "test",
203
+ })
204
+ goal_id = create.json()["id"]
205
+ response = await c.delete(f"/api/goals/{goal_id}")
206
+ assert response.status_code == 200
207
+
208
+ @pytest.mark.asyncio
209
+ async def test_delete_nonexistent_goal(self, client):
210
+ async with client as c:
211
+ response = await c.delete("/api/goals/99999")
212
+ assert response.status_code == 404
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_goal_coaching(self, client):
216
+ async with client as c:
217
+ response = await c.get("/api/goals/coach")
218
+ assert response.status_code == 200
219
+
220
+
221
+ class TestSprintEndpoints:
222
+ """Test sprint API endpoints."""
223
+
224
+ @pytest.mark.asyncio
225
+ async def test_create_sprint_snapshot(self, client):
226
+ async with client as c:
227
+ response = await c.post("/api/sprints/snapshot", json={
228
+ "sprint_name": "Sprint 1",
229
+ "total_points": 30,
230
+ "completed_points": 10,
231
+ "added_points": 0,
232
+ })
233
+ assert response.status_code == 201
234
+
235
+ @pytest.mark.asyncio
236
+ async def test_sprint_burndown(self, client):
237
+ async with client as c:
238
+ # Create snapshot first
239
+ await c.post("/api/sprints/snapshot", json={
240
+ "sprint_name": "Sprint B",
241
+ "total_points": 20,
242
+ "completed_points": 5,
243
+ })
244
+ response = await c.get("/api/sprints/Sprint B/burndown")
245
+ assert response.status_code == 200
246
+
247
+
248
+ class TestQualityEndpoints:
249
+ """Test code quality API endpoints."""
250
+
251
+ @pytest.mark.asyncio
252
+ async def test_create_quality_snapshot(self, client):
253
+ async with client as c:
254
+ response = await c.post("/api/quality/snapshot", json={
255
+ "repo": "testorg/testrepo",
256
+ "test_coverage": 0.85,
257
+ "open_bugs": 3,
258
+ "tech_debt_score": 4.2,
259
+ })
260
+ assert response.status_code == 201
261
+
262
+ @pytest.mark.asyncio
263
+ async def test_quality_trends(self, client):
264
+ async with client as c:
265
+ response = await c.get("/api/quality", params={"days": 90})
266
+ assert response.status_code == 200
267
+
268
+ @pytest.mark.asyncio
269
+ async def test_quality_score(self, client):
270
+ async with client as c:
271
+ response = await c.get("/api/quality/score", params={"repo": "testorg/testrepo", "days": 30})
272
+ assert response.status_code == 200
273
+ data = response.json()
274
+ assert data["repo"] == "testorg/testrepo"
275
+
276
+
277
+ class TestReportEndpoints:
278
+ """Test report generation API endpoints."""
279
+
280
+ @pytest.mark.asyncio
281
+ async def test_daily_report_json(self, client):
282
+ async with client as c:
283
+ response = await c.get("/api/reports/daily", params={"date": "2026-04-01"})
284
+ assert response.status_code == 200
285
+ data = response.json()
286
+ assert data["date"] == "2026-04-01"
287
+
288
+ @pytest.mark.asyncio
289
+ async def test_daily_report_markdown(self, client):
290
+ async with client as c:
291
+ response = await c.get("/api/reports/daily", params={"date": "2026-04-01", "format": "markdown"})
292
+ assert response.status_code == 200
293
+ data = response.json()
294
+ assert "markdown" in data
295
+
296
+ @pytest.mark.asyncio
297
+ async def test_daily_report_html(self, client):
298
+ async with client as c:
299
+ response = await c.get("/api/reports/daily", params={"date": "2026-04-01", "format": "html"})
300
+ assert response.status_code == 200
301
+ assert "<!DOCTYPE html>" in response.text
302
+
303
+ @pytest.mark.asyncio
304
+ async def test_weekly_report(self, client):
305
+ async with client as c:
306
+ response = await c.get("/api/reports/weekly", params={"week_start": "2026-03-30"})
307
+ assert response.status_code == 200
308
+
309
+ @pytest.mark.asyncio
310
+ async def test_monthly_report(self, client):
311
+ async with client as c:
312
+ response = await c.get("/api/reports/monthly", params={"year": 2026, "month": 4})
313
+ assert response.status_code == 200
@@ -0,0 +1,204 @@
1
+ """Tests for CLI commands."""
2
+
3
+ import json
4
+ from unittest.mock import patch, MagicMock
5
+
6
+ import pytest
7
+ from click.testing import CliRunner
8
+
9
+ from devpulse.cli.main import cli
10
+
11
+
12
+ @pytest.fixture
13
+ def runner():
14
+ return CliRunner()
15
+
16
+
17
+ class TestCLIBase:
18
+ """Test CLI base commands."""
19
+
20
+ def test_version(self, runner):
21
+ result = runner.invoke(cli, ["--version"])
22
+ assert result.exit_code == 0
23
+ assert "1.0.0" in result.output
24
+
25
+ def test_help(self, runner):
26
+ result = runner.invoke(cli, ["--help"])
27
+ assert result.exit_code == 0
28
+ assert "DevPulse" in result.output
29
+ assert "sync" in result.output
30
+ assert "daily" in result.output
31
+ assert "metrics" in result.output
32
+
33
+
34
+ class TestSyncCommand:
35
+ """Test sync command (without actual GitHub)."""
36
+
37
+ def test_sync_no_token(self, runner, monkeypatch):
38
+ monkeypatch.delenv("DEVPULSE_GITHUB_TOKEN", raising=False)
39
+ monkeypatch.setenv("DEVPULSE_GITHUB_TOKEN", "")
40
+ from devpulse.core.config import reset_settings
41
+ reset_settings()
42
+
43
+ result = runner.invoke(cli, ["sync"])
44
+ assert result.exit_code != 0 or "DEVPULSE_GITHUB_TOKEN" in result.output
45
+
46
+
47
+ class TestReportCommands:
48
+ """Test report generation CLI commands."""
49
+
50
+ def test_daily_report_terminal(self, runner, populated_db):
51
+ with patch("devpulse.core.report_generator.ReportGenerator", return_value=MagicMock(
52
+ daily_report=MagicMock(return_value={
53
+ "date": "2026-04-01",
54
+ "author": "all",
55
+ "commits": 2,
56
+ "prs_opened": 0,
57
+ "prs_merged": 0,
58
+ "issues_opened": 0,
59
+ "issues_closed": 0,
60
+ "reviews_given": 0,
61
+ "lines_changed": 80,
62
+ "summary": "2 commit(s)",
63
+ "commit_details": [],
64
+ }),
65
+ )):
66
+ result = runner.invoke(cli, ["daily", "--date", "2026-04-01"])
67
+ # Should not crash (may print to console)
68
+
69
+ def test_daily_report_json(self, runner, populated_db):
70
+ with patch("devpulse.core.report_generator.ReportGenerator", return_value=MagicMock(
71
+ daily_report=MagicMock(return_value={
72
+ "date": "2026-04-01",
73
+ "author": "all",
74
+ "commits": 2,
75
+ "prs_opened": 0,
76
+ "prs_merged": 0,
77
+ "issues_opened": 0,
78
+ "issues_closed": 0,
79
+ "reviews_given": 0,
80
+ "lines_changed": 80,
81
+ "summary": "2 commits",
82
+ "commit_details": [],
83
+ }),
84
+ to_json=MagicMock(return_value='{"date": "2026-04-01"}'),
85
+ )):
86
+ result = runner.invoke(cli, ["daily", "--format", "json"])
87
+ # Should output JSON
88
+
89
+ def test_weekly_report_help(self, runner):
90
+ result = runner.invoke(cli, ["weekly", "--help"])
91
+ assert result.exit_code == 0
92
+ assert "--week-start" in result.output
93
+
94
+ def test_monthly_report_help(self, runner):
95
+ result = runner.invoke(cli, ["monthly", "--help"])
96
+ assert result.exit_code == 0
97
+ assert "--year" in result.output
98
+
99
+
100
+ class TestMetricsCommand:
101
+ """Test metrics CLI commands."""
102
+
103
+ def test_metrics_help(self, runner):
104
+ result = runner.invoke(cli, ["metrics", "--help"])
105
+ assert result.exit_code == 0
106
+ assert "--author" in result.output
107
+ assert "--days" in result.output
108
+
109
+
110
+ class TestInsightsCommand:
111
+ """Test insights CLI command."""
112
+
113
+ def test_insights_help(self, runner):
114
+ result = runner.invoke(cli, ["insights", "--help"])
115
+ assert result.exit_code == 0
116
+ assert "--days" in result.output
117
+
118
+
119
+ class TestHealthCommand:
120
+ """Test health CLI command."""
121
+
122
+ def test_health_help(self, runner):
123
+ result = runner.invoke(cli, ["health", "--help"])
124
+ assert result.exit_code == 0
125
+ assert "--days" in result.output
126
+
127
+
128
+ class TestHeatmapCommand:
129
+ """Test heatmap CLI command."""
130
+
131
+ def test_heatmap_help(self, runner):
132
+ result = runner.invoke(cli, ["heatmap", "--help"])
133
+ assert result.exit_code == 0
134
+ assert "--author" in result.output
135
+ assert "--days" in result.output
136
+
137
+
138
+ class TestGoalsCommands:
139
+ """Test goals CLI commands."""
140
+
141
+ def test_goals_help(self, runner):
142
+ result = runner.invoke(cli, ["goals", "--help"])
143
+ assert result.exit_code == 0
144
+
145
+ def test_goals_add_help(self, runner):
146
+ result = runner.invoke(cli, ["goals", "add", "--help"])
147
+ assert result.exit_code == 0
148
+ assert "--title" in result.output
149
+
150
+ def test_goals_list_help(self, runner):
151
+ result = runner.invoke(cli, ["goals", "list", "--help"])
152
+ assert result.exit_code == 0
153
+
154
+ def test_goals_coach_help(self, runner):
155
+ result = runner.invoke(cli, ["goals", "coach", "--help"])
156
+ assert result.exit_code == 0
157
+
158
+
159
+ class TestSprintCommands:
160
+ """Test sprint CLI commands."""
161
+
162
+ def test_sprint_help(self, runner):
163
+ result = runner.invoke(cli, ["sprint", "--help"])
164
+ assert result.exit_code == 0
165
+
166
+ def test_sprint_snapshot_help(self, runner):
167
+ result = runner.invoke(cli, ["sprint", "snapshot", "--help"])
168
+ assert result.exit_code == 0
169
+ assert "--name" in result.output
170
+
171
+ def test_sprint_burndown_help(self, runner):
172
+ result = runner.invoke(cli, ["sprint", "burndown", "--help"])
173
+ assert result.exit_code == 0
174
+
175
+ def test_sprint_predict_help(self, runner):
176
+ result = runner.invoke(cli, ["sprint", "predict", "--help"])
177
+ assert result.exit_code == 0
178
+
179
+
180
+ class TestQualityCommand:
181
+ """Test quality CLI command."""
182
+
183
+ def test_quality_help(self, runner):
184
+ result = runner.invoke(cli, ["quality", "--help"])
185
+ assert result.exit_code == 0
186
+ assert "--repo" in result.output
187
+
188
+
189
+ class TestServeCommand:
190
+ """Test serve CLI command."""
191
+
192
+ def test_serve_help(self, runner):
193
+ result = runner.invoke(cli, ["serve", "--help"])
194
+ assert result.exit_code == 0
195
+ assert "--port" in result.output
196
+
197
+
198
+ class TestDashboardCommand:
199
+ """Test dashboard CLI command."""
200
+
201
+ def test_dashboard_help(self, runner):
202
+ result = runner.invoke(cli, ["dashboard", "--help"])
203
+ assert result.exit_code == 0
204
+ assert "--author" in result.output
@@ -0,0 +1,47 @@
1
+ """Tests for configuration management."""
2
+
3
+ import os
4
+ import pytest
5
+
6
+ from devpulse.core.config import Settings, get_settings, reset_settings
7
+
8
+
9
+ class TestSettings:
10
+ """Test settings loading."""
11
+
12
+ def test_default_settings(self):
13
+ reset_settings()
14
+ # Create Settings directly to check defaults (env vars are set by conftest)
15
+ s = Settings(github_token="", github_username="")
16
+ assert s.api_port == 8742
17
+ assert s.log_level == "INFO"
18
+ assert s.redact_sensitive is True
19
+
20
+ def test_env_override(self, monkeypatch):
21
+ reset_settings()
22
+ monkeypatch.setenv("DEVPULSE_GITHUB_TOKEN", "ghp_test123")
23
+ monkeypatch.setenv("DEVPULSE_API_PORT", "9999")
24
+ settings = Settings()
25
+ assert settings.github_token == "ghp_test123"
26
+ assert settings.api_port == 9999
27
+
28
+ def test_get_settings_caches(self):
29
+ reset_settings()
30
+ s1 = get_settings()
31
+ s2 = get_settings()
32
+ assert s1 is s2
33
+
34
+ def test_reset_settings(self):
35
+ reset_settings()
36
+ s1 = get_settings()
37
+ reset_settings()
38
+ s2 = get_settings()
39
+ assert s1 is not s2
40
+
41
+ def test_database_path_default(self):
42
+ settings = Settings()
43
+ assert "devpulse.db" in settings.database_path
44
+
45
+ def test_burnout_threshold(self):
46
+ settings = Settings()
47
+ assert 0 < settings.burnout_threshold < 1