recce-nightly 1.2.0.20250506__py3-none-any.whl → 1.26.0.20251124__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.
Potentially problematic release.
This version of recce-nightly might be problematic. Click here for more details.
- recce/VERSION +1 -1
- recce/__init__.py +27 -22
- recce/adapter/base.py +11 -14
- recce/adapter/dbt_adapter/__init__.py +810 -480
- recce/adapter/dbt_adapter/dbt_version.py +3 -0
- recce/adapter/sqlmesh_adapter.py +24 -35
- recce/apis/check_api.py +39 -28
- recce/apis/check_func.py +33 -27
- recce/apis/run_api.py +25 -19
- recce/apis/run_func.py +29 -23
- recce/artifact.py +119 -51
- recce/cli.py +1299 -323
- recce/config.py +42 -33
- recce/connect_to_cloud.py +138 -0
- recce/core.py +55 -47
- recce/data/404.html +1 -1
- recce/data/__next.__PAGE__.txt +10 -0
- recce/data/__next._full.txt +23 -0
- recce/data/__next._head.txt +8 -0
- recce/data/__next._index.txt +8 -0
- recce/data/__next._tree.txt +5 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_buildManifest.js +11 -0
- recce/data/_next/static/52aV_JrNUZU6dMFgvTQEO/_clientMiddlewareManifest.json +1 -0
- recce/data/_next/static/chunks/02b996c7f6a29a06.js +4 -0
- recce/data/_next/static/chunks/19c10d219a6a21ff.js +1 -0
- recce/data/_next/static/chunks/2df9ec28a061971d.js +11 -0
- recce/data/_next/static/chunks/3098c987393bda15.js +1 -0
- recce/data/_next/static/chunks/393dc43e483f717a.css +2 -0
- recce/data/_next/static/chunks/399e8d91a7e45073.js +2 -0
- recce/data/_next/static/chunks/4d0186f631230245.js +1 -0
- recce/data/_next/static/chunks/5794ba9e10a9c060.js +11 -0
- recce/data/_next/static/chunks/715761c929a3f28b.js +110 -0
- recce/data/_next/static/chunks/71f88fcc615bf282.js +1 -0
- recce/data/_next/static/chunks/80d2a95eaf1201ea.js +1 -0
- recce/data/_next/static/chunks/9979c6109bbbee35.js +1 -0
- recce/data/_next/static/chunks/99d638224186c118.js +1 -0
- recce/data/_next/static/chunks/d003eb36240e92f3.js +1 -0
- recce/data/_next/static/chunks/d3167cdfec4fc351.js +1 -0
- recce/data/_next/static/chunks/e124bccf574a3361.css +1 -0
- recce/data/_next/static/chunks/f40141db1bdb46f0.css +6 -0
- recce/data/_next/static/chunks/fcc53a88741a52f9.js +1 -0
- recce/data/_next/static/chunks/turbopack-b1920d28cfb1f28d.js +3 -0
- recce/data/_next/static/media/favicon.a8d38d84.ico +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.d80d830d.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.f9d58125.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.076c2a93.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.a4fa76b5.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.cde454cc.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.d5761935.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.40ec0659.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.b671449b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.9f7b8541.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.f9eb854e.woff2 +0 -0
- recce/data/_next/static/media/reload-image.7aa931c7.svg +4 -0
- recce/data/_not-found/__next._full.txt +17 -0
- recce/data/_not-found/__next._head.txt +8 -0
- recce/data/_not-found/__next._index.txt +8 -0
- recce/data/_not-found/__next._not-found.__PAGE__.txt +5 -0
- recce/data/_not-found/__next._not-found.txt +4 -0
- recce/data/_not-found/__next._tree.txt +3 -0
- recce/data/_not-found.html +1 -0
- recce/data/_not-found.txt +17 -0
- recce/data/auth_callback.html +68 -0
- recce/data/imgs/reload-image.svg +4 -0
- recce/data/index.html +1 -27
- recce/data/index.txt +23 -7
- recce/diff.py +6 -12
- recce/event/__init__.py +86 -74
- recce/event/collector.py +33 -22
- recce/event/track.py +49 -27
- recce/exceptions.py +1 -1
- recce/git.py +7 -7
- recce/github.py +57 -53
- recce/mcp_server.py +716 -0
- recce/models/__init__.py +4 -1
- recce/models/check.py +6 -7
- recce/models/run.py +1 -0
- recce/models/types.py +131 -28
- recce/pull_request.py +27 -25
- recce/run.py +165 -121
- recce/server.py +303 -111
- recce/state/__init__.py +31 -0
- recce/state/cloud.py +632 -0
- recce/state/const.py +26 -0
- recce/state/local.py +56 -0
- recce/state/state.py +119 -0
- recce/state/state_loader.py +174 -0
- recce/summary.py +188 -143
- recce/tasks/__init__.py +19 -3
- recce/tasks/core.py +11 -13
- recce/tasks/dataframe.py +82 -18
- recce/tasks/histogram.py +69 -34
- recce/tasks/lineage.py +2 -2
- recce/tasks/profile.py +152 -86
- recce/tasks/query.py +139 -87
- recce/tasks/rowcount.py +37 -31
- recce/tasks/schema.py +18 -15
- recce/tasks/top_k.py +35 -35
- recce/tasks/valuediff.py +216 -152
- recce/util/__init__.py +3 -0
- recce/util/api_token.py +80 -0
- recce/util/breaking.py +87 -85
- recce/util/cll.py +274 -219
- recce/util/io.py +22 -17
- recce/util/lineage.py +65 -16
- recce/util/logger.py +1 -1
- recce/util/onboarding_state.py +45 -0
- recce/util/perf_tracking.py +85 -0
- recce/util/recce_cloud.py +322 -72
- recce/util/singleton.py +4 -4
- recce/yaml/__init__.py +7 -10
- recce_cloud/__init__.py +24 -0
- recce_cloud/api/__init__.py +17 -0
- recce_cloud/api/base.py +111 -0
- recce_cloud/api/client.py +150 -0
- recce_cloud/api/exceptions.py +26 -0
- recce_cloud/api/factory.py +63 -0
- recce_cloud/api/github.py +76 -0
- recce_cloud/api/gitlab.py +82 -0
- recce_cloud/artifact.py +57 -0
- recce_cloud/ci_providers/__init__.py +9 -0
- recce_cloud/ci_providers/base.py +82 -0
- recce_cloud/ci_providers/detector.py +147 -0
- recce_cloud/ci_providers/github_actions.py +136 -0
- recce_cloud/ci_providers/gitlab_ci.py +130 -0
- recce_cloud/cli.py +245 -0
- recce_cloud/upload.py +214 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/METADATA +68 -37
- recce_nightly-1.26.0.20251124.dist-info/RECORD +180 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/WHEEL +1 -1
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/top_level.txt +1 -0
- tests/adapter/dbt_adapter/conftest.py +9 -5
- tests/adapter/dbt_adapter/dbt_test_helper.py +37 -22
- tests/adapter/dbt_adapter/test_dbt_adapter.py +0 -15
- tests/adapter/dbt_adapter/test_dbt_cll.py +656 -41
- tests/adapter/dbt_adapter/test_selector.py +22 -21
- tests/recce_cloud/__init__.py +0 -0
- tests/recce_cloud/test_ci_providers.py +351 -0
- tests/recce_cloud/test_cli.py +372 -0
- tests/recce_cloud/test_client.py +273 -0
- tests/recce_cloud/test_platform_clients.py +333 -0
- tests/tasks/conftest.py +1 -1
- tests/tasks/test_histogram.py +58 -66
- tests/tasks/test_lineage.py +36 -23
- tests/tasks/test_preset_checks.py +45 -31
- tests/tasks/test_profile.py +339 -15
- tests/tasks/test_query.py +46 -46
- tests/tasks/test_row_count.py +65 -46
- tests/tasks/test_schema.py +65 -42
- tests/tasks/test_top_k.py +22 -18
- tests/tasks/test_valuediff.py +43 -32
- tests/test_cli.py +174 -60
- tests/test_cli_mcp_optional.py +45 -0
- tests/test_cloud_listing_cli.py +324 -0
- tests/test_config.py +7 -9
- tests/test_connect_to_cloud.py +82 -0
- tests/test_core.py +151 -4
- tests/test_dbt.py +7 -7
- tests/test_mcp_server.py +332 -0
- tests/test_pull_request.py +1 -1
- tests/test_server.py +25 -19
- tests/test_summary.py +29 -17
- recce/data/_next/static/Kcbs3GEIyH2LxgLYat0es/_buildManifest.js +0 -1
- recce/data/_next/static/chunks/1f229bf6-d9fe92e56db8d93b.js +0 -1
- recce/data/_next/static/chunks/29e3cc0d-8c150e37dff9631b.js +0 -1
- recce/data/_next/static/chunks/368-7587b306577df275.js +0 -65
- recce/data/_next/static/chunks/36e1c10d-bb0210cbd6573a8d.js +0 -1
- recce/data/_next/static/chunks/3998a672-eaad84bdd88cc73e.js +0 -1
- recce/data/_next/static/chunks/3a92ee20-3b5d922d4157af5e.js +0 -1
- recce/data/_next/static/chunks/450c323b-1bb5db526e54435a.js +0 -1
- recce/data/_next/static/chunks/47d8844f-79a1b53c66a7d7ec.js +0 -1
- recce/data/_next/static/chunks/6dc81886-c94b9b91bc2c3caf.js +0 -1
- recce/data/_next/static/chunks/6ef81909-694dc38134099299.js +0 -1
- recce/data/_next/static/chunks/700-3b65fc3666820d00.js +0 -2
- recce/data/_next/static/chunks/7a8a3e83-d7fa409d97b38b2b.js +0 -1
- recce/data/_next/static/chunks/7f27ae6c-413f6b869a04183a.js +0 -1
- recce/data/_next/static/chunks/8d700b6a-f0b1f6b9e0d97ce2.js +0 -1
- recce/data/_next/static/chunks/9746af58-d74bef4d03eea6ab.js +0 -1
- recce/data/_next/static/chunks/a30376cd-7d806e1602f2dc3a.js +0 -1
- recce/data/_next/static/chunks/app/_not-found/page-8a886fa0855c3105.js +0 -1
- recce/data/_next/static/chunks/app/layout-9102e22cb73f74d6.js +0 -1
- recce/data/_next/static/chunks/app/page-cee661090afbd6aa.js +0 -1
- recce/data/_next/static/chunks/b63b1b3f-7395c74e11a14e95.js +0 -1
- recce/data/_next/static/chunks/c132bf7d-8102037f9ccf372a.js +0 -1
- recce/data/_next/static/chunks/c1ceaa8b-a1e442154d23515e.js +0 -1
- recce/data/_next/static/chunks/cd9f8d63-cf0d5a7b0f7a92e8.js +0 -54
- recce/data/_next/static/chunks/ce84277d-f42c2c58049cea2d.js +0 -1
- recce/data/_next/static/chunks/e24bf851-0f8cbc99656833e7.js +0 -1
- recce/data/_next/static/chunks/fee69bc6-f17d36c080742e74.js +0 -1
- recce/data/_next/static/chunks/framework-ded83d71b51ce901.js +0 -1
- recce/data/_next/static/chunks/main-a0859f1f36d0aa6c.js +0 -1
- recce/data/_next/static/chunks/main-app-0225a2255968e566.js +0 -1
- recce/data/_next/static/chunks/pages/_app-d5672bf3d8b6371b.js +0 -1
- recce/data/_next/static/chunks/pages/_error-ed75be3f25588548.js +0 -1
- recce/data/_next/static/chunks/webpack-567d72f0bc0820d5.js +0 -1
- recce/data/_next/static/css/c9ecb46a4b21c126.css +0 -14
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.22628180.woff2 +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-800-normal.31d693bb.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.7e2c1e62.woff +0 -0
- recce/data/_next/static/media/montserrat-cyrillic-ext-800-normal.94a63aea.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.6f8fa298.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-800-normal.97e20d5e.woff +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.013b84f9.woff2 +0 -0
- recce/data/_next/static/media/montserrat-latin-ext-800-normal.aff52ab0.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.5f21869b.woff +0 -0
- recce/data/_next/static/media/montserrat-vietnamese-800-normal.c0035377.woff2 +0 -0
- recce/state.py +0 -753
- recce_nightly-1.2.0.20250506.dist-info/RECORD +0 -142
- tests/test_state.py +0 -123
- /recce/data/_next/static/{Kcbs3GEIyH2LxgLYat0es → 52aV_JrNUZU6dMFgvTQEO}/_ssgManifest.js +0 -0
- /recce/data/_next/static/chunks/{polyfills-42372ed130431b0a.js → a6dad97d9634a72d.js} +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/entry_points.txt +0 -0
- {recce_nightly-1.2.0.20250506.dist-info → recce_nightly-1.26.0.20251124.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for recce-cloud CLI commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
from click.testing import CliRunner
|
|
12
|
+
|
|
13
|
+
from recce_cloud.cli import cloud_cli
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestUploadDryRun(unittest.TestCase):
|
|
17
|
+
"""Test cases for the --dry-run flag in upload command."""
|
|
18
|
+
|
|
19
|
+
def setUp(self):
|
|
20
|
+
"""Set up test fixtures."""
|
|
21
|
+
self.runner = CliRunner()
|
|
22
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
23
|
+
|
|
24
|
+
# Create mock dbt artifacts
|
|
25
|
+
manifest_path = Path(self.temp_dir) / "manifest.json"
|
|
26
|
+
catalog_path = Path(self.temp_dir) / "catalog.json"
|
|
27
|
+
|
|
28
|
+
manifest_content = {
|
|
29
|
+
"metadata": {"adapter_type": "postgres"},
|
|
30
|
+
"nodes": {},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
catalog_content = {
|
|
34
|
+
"nodes": {},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
|
|
39
|
+
with open(manifest_path, "w") as f:
|
|
40
|
+
json.dump(manifest_content, f)
|
|
41
|
+
|
|
42
|
+
with open(catalog_path, "w") as f:
|
|
43
|
+
json.dump(catalog_content, f)
|
|
44
|
+
|
|
45
|
+
def tearDown(self):
|
|
46
|
+
"""Clean up test fixtures."""
|
|
47
|
+
import shutil
|
|
48
|
+
|
|
49
|
+
if os.path.exists(self.temp_dir):
|
|
50
|
+
shutil.rmtree(self.temp_dir)
|
|
51
|
+
|
|
52
|
+
def test_dry_run_github_actions_pr_context(self):
|
|
53
|
+
"""Test dry-run with GitHub Actions PR context."""
|
|
54
|
+
env = {
|
|
55
|
+
"GITHUB_ACTIONS": "true",
|
|
56
|
+
"GITHUB_REPOSITORY": "DataRecce/recce",
|
|
57
|
+
"GITHUB_EVENT_NAME": "pull_request",
|
|
58
|
+
"GITHUB_SHA": "abc123def456",
|
|
59
|
+
"GITHUB_HEAD_REF": "feature/test-branch",
|
|
60
|
+
"GITHUB_BASE_REF": "main",
|
|
61
|
+
"RECCE_API_TOKEN": "test_token_123",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Create mock event file
|
|
65
|
+
event_file = Path(self.temp_dir) / "github_event.json"
|
|
66
|
+
import json
|
|
67
|
+
|
|
68
|
+
with open(event_file, "w") as f:
|
|
69
|
+
json.dump({"pull_request": {"number": 42}}, f)
|
|
70
|
+
|
|
71
|
+
env["GITHUB_EVENT_PATH"] = str(event_file)
|
|
72
|
+
|
|
73
|
+
with patch.dict(os.environ, env, clear=True):
|
|
74
|
+
result = self.runner.invoke(
|
|
75
|
+
cloud_cli,
|
|
76
|
+
["upload", "--target-path", self.temp_dir, "--dry-run"],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Assertions
|
|
80
|
+
self.assertEqual(result.exit_code, 0, f"Command failed: {result.output}")
|
|
81
|
+
self.assertIn("Dry run mode enabled", result.output)
|
|
82
|
+
self.assertIn("Platform Information:", result.output)
|
|
83
|
+
self.assertIn("Platform: github-actions", result.output)
|
|
84
|
+
self.assertIn("Repository: DataRecce/recce", result.output)
|
|
85
|
+
self.assertIn("CR Number: 42", result.output)
|
|
86
|
+
self.assertIn("Commit SHA: abc123de", result.output)
|
|
87
|
+
self.assertIn("Source Branch: feature/test-branch", result.output)
|
|
88
|
+
self.assertIn("Base Branch: main", result.output)
|
|
89
|
+
self.assertIn("Upload Workflow:", result.output)
|
|
90
|
+
self.assertIn("Auto-create session and upload", result.output)
|
|
91
|
+
self.assertIn("Platform-specific APIs will be used", result.output)
|
|
92
|
+
self.assertIn("Files to upload:", result.output)
|
|
93
|
+
self.assertIn("manifest.json:", result.output)
|
|
94
|
+
self.assertIn("catalog.json:", result.output)
|
|
95
|
+
self.assertIn("Adapter type: postgres", result.output)
|
|
96
|
+
self.assertIn("Dry run completed successfully", result.output)
|
|
97
|
+
|
|
98
|
+
def test_dry_run_gitlab_ci_mr_context(self):
|
|
99
|
+
"""Test dry-run with GitLab CI MR context."""
|
|
100
|
+
env = {
|
|
101
|
+
"GITLAB_CI": "true",
|
|
102
|
+
"CI_PROJECT_PATH": "recce/jaffle-shop",
|
|
103
|
+
"CI_PROJECT_URL": "https://gitlab.com/recce/jaffle-shop",
|
|
104
|
+
"CI_MERGE_REQUEST_IID": "5",
|
|
105
|
+
"CI_MERGE_REQUEST_SOURCE_BRANCH_NAME": "feature/new-models",
|
|
106
|
+
"CI_MERGE_REQUEST_TARGET_BRANCH_NAME": "main",
|
|
107
|
+
"CI_COMMIT_SHA": "def456abc789",
|
|
108
|
+
"CI_SERVER_URL": "https://gitlab.com",
|
|
109
|
+
"RECCE_API_TOKEN": "test_token_abc",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
with patch.dict(os.environ, env, clear=True):
|
|
113
|
+
result = self.runner.invoke(
|
|
114
|
+
cloud_cli,
|
|
115
|
+
["upload", "--target-path", self.temp_dir, "--dry-run"],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Assertions
|
|
119
|
+
self.assertEqual(result.exit_code, 0, f"Command failed: {result.output}")
|
|
120
|
+
self.assertIn("Dry run mode enabled", result.output)
|
|
121
|
+
self.assertIn("Platform Information:", result.output)
|
|
122
|
+
self.assertIn("Platform: gitlab-ci", result.output)
|
|
123
|
+
self.assertIn("Repository: recce/jaffle-shop", result.output)
|
|
124
|
+
self.assertIn("CR Number: 5", result.output)
|
|
125
|
+
self.assertIn("Commit SHA: def456ab", result.output)
|
|
126
|
+
self.assertIn("Source Branch: feature/new-models", result.output)
|
|
127
|
+
self.assertIn("Base Branch: main", result.output)
|
|
128
|
+
self.assertIn("Auto-create session and upload", result.output)
|
|
129
|
+
self.assertIn("Platform-specific APIs will be used", result.output)
|
|
130
|
+
self.assertIn("Adapter type: postgres", result.output)
|
|
131
|
+
|
|
132
|
+
def test_dry_run_gitlab_ci_self_hosted(self):
|
|
133
|
+
"""Test dry-run with self-hosted GitLab instance."""
|
|
134
|
+
env = {
|
|
135
|
+
"GITLAB_CI": "true",
|
|
136
|
+
"CI_PROJECT_PATH": "data-team/dbt-project",
|
|
137
|
+
"CI_PROJECT_URL": "https://gitlab.mycompany.com/data-team/dbt-project",
|
|
138
|
+
"CI_MERGE_REQUEST_IID": "25",
|
|
139
|
+
"CI_MERGE_REQUEST_SOURCE_BRANCH_NAME": "develop",
|
|
140
|
+
"CI_MERGE_REQUEST_TARGET_BRANCH_NAME": "production",
|
|
141
|
+
"CI_COMMIT_SHA": "fedcba987654",
|
|
142
|
+
"CI_SERVER_URL": "https://gitlab.mycompany.com",
|
|
143
|
+
"RECCE_API_TOKEN": "test_token_xyz",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
with patch.dict(os.environ, env, clear=True):
|
|
147
|
+
result = self.runner.invoke(
|
|
148
|
+
cloud_cli,
|
|
149
|
+
["upload", "--target-path", self.temp_dir, "--dry-run"],
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Assertions
|
|
153
|
+
self.assertEqual(result.exit_code, 0, f"Command failed: {result.output}")
|
|
154
|
+
self.assertIn("Platform: gitlab-ci", result.output)
|
|
155
|
+
self.assertIn("Repository: data-team/dbt-project", result.output)
|
|
156
|
+
self.assertIn("CR Number: 25", result.output)
|
|
157
|
+
self.assertIn("Source Branch: develop", result.output)
|
|
158
|
+
self.assertIn("Base Branch: production", result.output)
|
|
159
|
+
|
|
160
|
+
def test_dry_run_with_session_id(self):
|
|
161
|
+
"""Test dry-run with existing session ID (generic workflow)."""
|
|
162
|
+
env = {
|
|
163
|
+
"GITHUB_ACTIONS": "true",
|
|
164
|
+
"GITHUB_REPOSITORY": "DataRecce/recce",
|
|
165
|
+
"RECCE_API_TOKEN": "test_token_789",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
with patch.dict(os.environ, env, clear=True):
|
|
169
|
+
result = self.runner.invoke(
|
|
170
|
+
cloud_cli,
|
|
171
|
+
[
|
|
172
|
+
"upload",
|
|
173
|
+
"--target-path",
|
|
174
|
+
self.temp_dir,
|
|
175
|
+
"--session-id",
|
|
176
|
+
"sess_abc123xyz",
|
|
177
|
+
"--dry-run",
|
|
178
|
+
],
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Assertions
|
|
182
|
+
self.assertEqual(result.exit_code, 0, f"Command failed: {result.output}")
|
|
183
|
+
self.assertIn("Dry run mode enabled", result.output)
|
|
184
|
+
self.assertIn("Upload Workflow:", result.output)
|
|
185
|
+
self.assertIn("Upload to existing session", result.output)
|
|
186
|
+
self.assertIn("Session ID: sess_abc123xyz", result.output)
|
|
187
|
+
self.assertNotIn("Auto-create session", result.output)
|
|
188
|
+
|
|
189
|
+
def test_dry_run_github_main_branch(self):
|
|
190
|
+
"""Test dry-run with GitHub Actions main branch (no PR)."""
|
|
191
|
+
env = {
|
|
192
|
+
"GITHUB_ACTIONS": "true",
|
|
193
|
+
"GITHUB_REPOSITORY": "DataRecce/recce",
|
|
194
|
+
"GITHUB_EVENT_NAME": "push",
|
|
195
|
+
"GITHUB_REF": "refs/heads/main",
|
|
196
|
+
"GITHUB_SHA": "xyz789abc123",
|
|
197
|
+
"RECCE_API_TOKEN": "test_token_456",
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
with patch.dict(os.environ, env, clear=True):
|
|
201
|
+
result = self.runner.invoke(
|
|
202
|
+
cloud_cli,
|
|
203
|
+
["upload", "--target-path", self.temp_dir, "--dry-run"],
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Assertions
|
|
207
|
+
self.assertEqual(result.exit_code, 0, f"Command failed: {result.output}")
|
|
208
|
+
self.assertIn("Platform: github-actions", result.output)
|
|
209
|
+
self.assertIn("Repository: DataRecce/recce", result.output)
|
|
210
|
+
self.assertIn("Commit SHA: xyz789ab", result.output)
|
|
211
|
+
# Session type depends on git branch detection, could be prod or dev
|
|
212
|
+
self.assertIn("Session Type:", result.output)
|
|
213
|
+
# Should not have CR number
|
|
214
|
+
self.assertNotIn("CR Number:", result.output)
|
|
215
|
+
|
|
216
|
+
def test_dry_run_gitlab_main_branch(self):
|
|
217
|
+
"""Test dry-run with GitLab CI main branch (no MR)."""
|
|
218
|
+
env = {
|
|
219
|
+
"GITLAB_CI": "true",
|
|
220
|
+
"CI_PROJECT_PATH": "recce/analytics",
|
|
221
|
+
"CI_PROJECT_URL": "https://gitlab.com/recce/analytics",
|
|
222
|
+
"CI_COMMIT_BRANCH": "main",
|
|
223
|
+
"CI_COMMIT_SHA": "123abc456def",
|
|
224
|
+
"CI_SERVER_URL": "https://gitlab.com",
|
|
225
|
+
"RECCE_API_TOKEN": "test_token_main",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
with patch.dict(os.environ, env, clear=True):
|
|
229
|
+
result = self.runner.invoke(
|
|
230
|
+
cloud_cli,
|
|
231
|
+
["upload", "--target-path", self.temp_dir, "--dry-run"],
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Assertions
|
|
235
|
+
self.assertEqual(result.exit_code, 0, f"Command failed: {result.output}")
|
|
236
|
+
self.assertIn("Platform: gitlab-ci", result.output)
|
|
237
|
+
self.assertIn("Repository: recce/analytics", result.output)
|
|
238
|
+
# Session type depends on git branch detection, could be prod or dev
|
|
239
|
+
self.assertIn("Session Type:", result.output)
|
|
240
|
+
# Should not have CR number
|
|
241
|
+
self.assertNotIn("CR Number:", result.output)
|
|
242
|
+
|
|
243
|
+
def test_dry_run_with_manual_overrides(self):
|
|
244
|
+
"""Test dry-run with manual overrides."""
|
|
245
|
+
env = {
|
|
246
|
+
"GITHUB_ACTIONS": "true",
|
|
247
|
+
"GITHUB_REPOSITORY": "DataRecce/recce",
|
|
248
|
+
"GITHUB_EVENT_NAME": "pull_request",
|
|
249
|
+
"GITHUB_SHA": "abc123",
|
|
250
|
+
"RECCE_API_TOKEN": "test_token",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Create mock event file with PR number 42
|
|
254
|
+
event_file = Path(self.temp_dir) / "github_event.json"
|
|
255
|
+
import json
|
|
256
|
+
|
|
257
|
+
with open(event_file, "w") as f:
|
|
258
|
+
json.dump({"pull_request": {"number": 42}}, f)
|
|
259
|
+
|
|
260
|
+
env["GITHUB_EVENT_PATH"] = str(event_file)
|
|
261
|
+
|
|
262
|
+
with patch.dict(os.environ, env, clear=True):
|
|
263
|
+
result = self.runner.invoke(
|
|
264
|
+
cloud_cli,
|
|
265
|
+
[
|
|
266
|
+
"upload",
|
|
267
|
+
"--target-path",
|
|
268
|
+
self.temp_dir,
|
|
269
|
+
"--cr",
|
|
270
|
+
"100",
|
|
271
|
+
"--type",
|
|
272
|
+
"cr",
|
|
273
|
+
"--dry-run",
|
|
274
|
+
],
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Assertions
|
|
278
|
+
self.assertEqual(result.exit_code, 0, f"Command failed: {result.output}")
|
|
279
|
+
# Should show overridden CR number
|
|
280
|
+
self.assertIn("CR Number: 100", result.output)
|
|
281
|
+
self.assertIn("Session Type: cr", result.output)
|
|
282
|
+
|
|
283
|
+
def test_dry_run_no_ci_environment(self):
|
|
284
|
+
"""Test dry-run without CI environment (local development)."""
|
|
285
|
+
env = {
|
|
286
|
+
"RECCE_API_TOKEN": "test_token_local",
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
with patch.dict(os.environ, env, clear=True):
|
|
290
|
+
result = self.runner.invoke(
|
|
291
|
+
cloud_cli,
|
|
292
|
+
[
|
|
293
|
+
"upload",
|
|
294
|
+
"--target-path",
|
|
295
|
+
self.temp_dir,
|
|
296
|
+
"--session-id",
|
|
297
|
+
"sess_local123",
|
|
298
|
+
"--dry-run",
|
|
299
|
+
],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Assertions
|
|
303
|
+
self.assertEqual(result.exit_code, 0, f"Command failed: {result.output}")
|
|
304
|
+
self.assertIn("Dry run mode enabled", result.output)
|
|
305
|
+
self.assertIn("Upload to existing session", result.output)
|
|
306
|
+
self.assertIn("Session ID: sess_local123", result.output)
|
|
307
|
+
# Should not show platform information
|
|
308
|
+
self.assertNotIn("Platform Information:", result.output)
|
|
309
|
+
|
|
310
|
+
def test_dry_run_unsupported_platform_without_session_id(self):
|
|
311
|
+
"""Test dry-run with unsupported platform and no session ID."""
|
|
312
|
+
env = {
|
|
313
|
+
"RECCE_API_TOKEN": "test_token",
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
with patch.dict(os.environ, env, clear=True):
|
|
317
|
+
result = self.runner.invoke(
|
|
318
|
+
cloud_cli,
|
|
319
|
+
["upload", "--target-path", self.temp_dir, "--dry-run"],
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Assertions
|
|
323
|
+
self.assertEqual(result.exit_code, 0, f"Command failed: {result.output}")
|
|
324
|
+
self.assertIn("Dry run mode enabled", result.output)
|
|
325
|
+
self.assertIn("Auto-create session and upload", result.output)
|
|
326
|
+
self.assertIn("Warning: Platform not supported for auto-session creation", result.output)
|
|
327
|
+
|
|
328
|
+
def test_dry_run_missing_artifacts(self):
|
|
329
|
+
"""Test dry-run with missing dbt artifacts."""
|
|
330
|
+
import shutil
|
|
331
|
+
|
|
332
|
+
shutil.rmtree(self.temp_dir)
|
|
333
|
+
|
|
334
|
+
env = {
|
|
335
|
+
"GITHUB_ACTIONS": "true",
|
|
336
|
+
"RECCE_API_TOKEN": "test_token",
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
with patch.dict(os.environ, env, clear=True):
|
|
340
|
+
result = self.runner.invoke(
|
|
341
|
+
cloud_cli,
|
|
342
|
+
["upload", "--target-path", self.temp_dir, "--dry-run"],
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
# Assertions
|
|
346
|
+
# Should fail before dry-run validation happens
|
|
347
|
+
self.assertNotEqual(result.exit_code, 0)
|
|
348
|
+
self.assertIn("does not exist", result.output)
|
|
349
|
+
|
|
350
|
+
def test_dry_run_custom_target_path(self):
|
|
351
|
+
"""Test dry-run with custom target path."""
|
|
352
|
+
env = {
|
|
353
|
+
"GITHUB_ACTIONS": "true",
|
|
354
|
+
"GITHUB_REPOSITORY": "DataRecce/recce",
|
|
355
|
+
"RECCE_API_TOKEN": "test_token",
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
with patch.dict(os.environ, env, clear=True):
|
|
359
|
+
result = self.runner.invoke(
|
|
360
|
+
cloud_cli,
|
|
361
|
+
["upload", "--target-path", self.temp_dir, "--dry-run"],
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Assertions
|
|
365
|
+
self.assertEqual(result.exit_code, 0, f"Command failed: {result.output}")
|
|
366
|
+
self.assertIn(self.temp_dir, result.output)
|
|
367
|
+
self.assertIn("manifest.json:", result.output)
|
|
368
|
+
self.assertIn("catalog.json:", result.output)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
if __name__ == "__main__":
|
|
372
|
+
unittest.main()
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
from recce_cloud.api.client import RecceCloudClient, RecceCloudException
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RecceCloudClientTests(unittest.TestCase):
|
|
9
|
+
def setUp(self):
|
|
10
|
+
"""Set up test fixtures."""
|
|
11
|
+
self.api_token = "rct-test-token-123"
|
|
12
|
+
self.session_id = "session-123"
|
|
13
|
+
self.org_id = "org-456"
|
|
14
|
+
self.project_id = "project-789"
|
|
15
|
+
|
|
16
|
+
def test_init_with_api_token(self):
|
|
17
|
+
"""Test client initialization with Recce API token."""
|
|
18
|
+
client = RecceCloudClient(self.api_token)
|
|
19
|
+
self.assertEqual(client.token, self.api_token)
|
|
20
|
+
self.assertIn("/api/v2", client.base_url_v2)
|
|
21
|
+
|
|
22
|
+
def test_init_with_none_token_raises_error(self):
|
|
23
|
+
"""Test client initialization with None token raises ValueError."""
|
|
24
|
+
with self.assertRaises(ValueError) as context:
|
|
25
|
+
RecceCloudClient(None)
|
|
26
|
+
self.assertIn("Token cannot be None", str(context.exception))
|
|
27
|
+
|
|
28
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
29
|
+
def test_get_session_success(self, mock_request):
|
|
30
|
+
"""Test successful get_session call."""
|
|
31
|
+
client = RecceCloudClient(self.api_token)
|
|
32
|
+
|
|
33
|
+
# Mock successful response
|
|
34
|
+
mock_response = MagicMock()
|
|
35
|
+
mock_response.status_code = 200
|
|
36
|
+
mock_response.json.return_value = {
|
|
37
|
+
"success": True,
|
|
38
|
+
"session": {
|
|
39
|
+
"id": self.session_id,
|
|
40
|
+
"org_id": self.org_id,
|
|
41
|
+
"project_id": self.project_id,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
mock_request.return_value = mock_response
|
|
45
|
+
|
|
46
|
+
result = client.get_session(self.session_id)
|
|
47
|
+
|
|
48
|
+
self.assertEqual(result["id"], self.session_id)
|
|
49
|
+
self.assertEqual(result["org_id"], self.org_id)
|
|
50
|
+
self.assertEqual(result["project_id"], self.project_id)
|
|
51
|
+
|
|
52
|
+
# Verify request was made correctly
|
|
53
|
+
mock_request.assert_called_once()
|
|
54
|
+
call_args = mock_request.call_args
|
|
55
|
+
self.assertEqual(call_args[0][0], "GET")
|
|
56
|
+
self.assertIn(self.session_id, call_args[0][1])
|
|
57
|
+
self.assertEqual(call_args[1]["headers"]["Authorization"], f"Bearer {self.api_token}")
|
|
58
|
+
|
|
59
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
60
|
+
def test_get_session_not_found(self, mock_request):
|
|
61
|
+
"""Test get_session with 404 response."""
|
|
62
|
+
client = RecceCloudClient(self.api_token)
|
|
63
|
+
|
|
64
|
+
# Mock 404 response
|
|
65
|
+
mock_response = MagicMock()
|
|
66
|
+
mock_response.status_code = 404
|
|
67
|
+
mock_response.text = "Session not found"
|
|
68
|
+
mock_request.return_value = mock_response
|
|
69
|
+
|
|
70
|
+
with self.assertRaises(RecceCloudException) as context:
|
|
71
|
+
client.get_session(self.session_id)
|
|
72
|
+
|
|
73
|
+
self.assertEqual(context.exception.status_code, 404)
|
|
74
|
+
self.assertIn("Session not found", str(context.exception))
|
|
75
|
+
|
|
76
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
77
|
+
def test_get_session_forbidden(self, mock_request):
|
|
78
|
+
"""Test get_session with 403 response."""
|
|
79
|
+
client = RecceCloudClient(self.api_token)
|
|
80
|
+
|
|
81
|
+
# Mock 403 response
|
|
82
|
+
mock_response = MagicMock()
|
|
83
|
+
mock_response.status_code = 403
|
|
84
|
+
mock_response.json.return_value = {"detail": "Access denied"}
|
|
85
|
+
mock_request.return_value = mock_response
|
|
86
|
+
|
|
87
|
+
result = client.get_session(self.session_id)
|
|
88
|
+
|
|
89
|
+
self.assertEqual(result["status"], "error")
|
|
90
|
+
self.assertEqual(result["message"], "Access denied")
|
|
91
|
+
|
|
92
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
93
|
+
def test_get_session_api_error(self, mock_request):
|
|
94
|
+
"""Test get_session with API returning success=False."""
|
|
95
|
+
client = RecceCloudClient(self.api_token)
|
|
96
|
+
|
|
97
|
+
# Mock response with success=False
|
|
98
|
+
mock_response = MagicMock()
|
|
99
|
+
mock_response.status_code = 200
|
|
100
|
+
mock_response.json.return_value = {"success": False, "message": "Invalid session"}
|
|
101
|
+
mock_request.return_value = mock_response
|
|
102
|
+
|
|
103
|
+
with self.assertRaises(RecceCloudException) as context:
|
|
104
|
+
client.get_session(self.session_id)
|
|
105
|
+
|
|
106
|
+
self.assertIn("Invalid session", context.exception.reason)
|
|
107
|
+
|
|
108
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
109
|
+
def test_get_upload_urls_success(self, mock_request):
|
|
110
|
+
"""Test successful get_upload_urls_by_session_id call."""
|
|
111
|
+
client = RecceCloudClient(self.api_token)
|
|
112
|
+
|
|
113
|
+
# Mock successful response
|
|
114
|
+
mock_response = MagicMock()
|
|
115
|
+
mock_response.status_code = 200
|
|
116
|
+
mock_response.json.return_value = {
|
|
117
|
+
"presigned_urls": {
|
|
118
|
+
"manifest_url": "https://s3.amazonaws.com/bucket/manifest.json?token=abc",
|
|
119
|
+
"catalog_url": "https://s3.amazonaws.com/bucket/catalog.json?token=def",
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
mock_request.return_value = mock_response
|
|
123
|
+
|
|
124
|
+
result = client.get_upload_urls_by_session_id(self.org_id, self.project_id, self.session_id)
|
|
125
|
+
|
|
126
|
+
self.assertIn("manifest_url", result)
|
|
127
|
+
self.assertIn("catalog_url", result)
|
|
128
|
+
self.assertIn("s3.amazonaws.com", result["manifest_url"])
|
|
129
|
+
|
|
130
|
+
# Verify request was made correctly
|
|
131
|
+
mock_request.assert_called_once()
|
|
132
|
+
call_args = mock_request.call_args
|
|
133
|
+
self.assertEqual(call_args[0][0], "GET")
|
|
134
|
+
self.assertIn(self.org_id, call_args[0][1])
|
|
135
|
+
self.assertIn(self.project_id, call_args[0][1])
|
|
136
|
+
self.assertIn(self.session_id, call_args[0][1])
|
|
137
|
+
self.assertIn("upload-url", call_args[0][1])
|
|
138
|
+
|
|
139
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
140
|
+
def test_get_upload_urls_no_presigned_urls(self, mock_request):
|
|
141
|
+
"""Test get_upload_urls_by_session_id with no presigned URLs."""
|
|
142
|
+
client = RecceCloudClient(self.api_token)
|
|
143
|
+
|
|
144
|
+
# Mock response with null presigned_urls
|
|
145
|
+
mock_response = MagicMock()
|
|
146
|
+
mock_response.status_code = 200
|
|
147
|
+
mock_response.json.return_value = {"presigned_urls": None}
|
|
148
|
+
mock_request.return_value = mock_response
|
|
149
|
+
|
|
150
|
+
with self.assertRaises(RecceCloudException) as context:
|
|
151
|
+
client.get_upload_urls_by_session_id(self.org_id, self.project_id, self.session_id)
|
|
152
|
+
|
|
153
|
+
self.assertEqual(context.exception.status_code, 404)
|
|
154
|
+
self.assertIn("No presigned URLs", str(context.exception))
|
|
155
|
+
|
|
156
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
157
|
+
def test_get_upload_urls_failure(self, mock_request):
|
|
158
|
+
"""Test get_upload_urls_by_session_id with API failure."""
|
|
159
|
+
client = RecceCloudClient(self.api_token)
|
|
160
|
+
|
|
161
|
+
# Mock error response
|
|
162
|
+
mock_response = MagicMock()
|
|
163
|
+
mock_response.status_code = 500
|
|
164
|
+
mock_response.text = "Internal server error"
|
|
165
|
+
mock_request.return_value = mock_response
|
|
166
|
+
|
|
167
|
+
with self.assertRaises(RecceCloudException) as context:
|
|
168
|
+
client.get_upload_urls_by_session_id(self.org_id, self.project_id, self.session_id)
|
|
169
|
+
|
|
170
|
+
self.assertEqual(context.exception.status_code, 500)
|
|
171
|
+
|
|
172
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
173
|
+
def test_update_session_success(self, mock_request):
|
|
174
|
+
"""Test successful update_session call."""
|
|
175
|
+
client = RecceCloudClient(self.api_token)
|
|
176
|
+
adapter_type = "postgres"
|
|
177
|
+
|
|
178
|
+
# Mock successful response
|
|
179
|
+
mock_response = MagicMock()
|
|
180
|
+
mock_response.status_code = 200
|
|
181
|
+
mock_response.json.return_value = {"success": True, "session": {"adapter_type": adapter_type}}
|
|
182
|
+
mock_request.return_value = mock_response
|
|
183
|
+
|
|
184
|
+
result = client.update_session(self.org_id, self.project_id, self.session_id, adapter_type)
|
|
185
|
+
|
|
186
|
+
self.assertTrue(result["success"])
|
|
187
|
+
self.assertEqual(result["session"]["adapter_type"], adapter_type)
|
|
188
|
+
|
|
189
|
+
# Verify request was made correctly
|
|
190
|
+
mock_request.assert_called_once()
|
|
191
|
+
call_args = mock_request.call_args
|
|
192
|
+
self.assertEqual(call_args[0][0], "PATCH")
|
|
193
|
+
self.assertIn(self.org_id, call_args[0][1])
|
|
194
|
+
self.assertIn(self.project_id, call_args[0][1])
|
|
195
|
+
self.assertIn(self.session_id, call_args[0][1])
|
|
196
|
+
self.assertEqual(call_args[1]["json"]["adapter_type"], adapter_type)
|
|
197
|
+
|
|
198
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
199
|
+
def test_update_session_forbidden(self, mock_request):
|
|
200
|
+
"""Test update_session with 403 response."""
|
|
201
|
+
client = RecceCloudClient(self.api_token)
|
|
202
|
+
|
|
203
|
+
# Mock 403 response
|
|
204
|
+
mock_response = MagicMock()
|
|
205
|
+
mock_response.status_code = 403
|
|
206
|
+
mock_response.json.return_value = {"detail": "Insufficient permissions"}
|
|
207
|
+
mock_request.return_value = mock_response
|
|
208
|
+
|
|
209
|
+
result = client.update_session(self.org_id, self.project_id, self.session_id, "postgres")
|
|
210
|
+
|
|
211
|
+
self.assertEqual(result["status"], "error")
|
|
212
|
+
self.assertEqual(result["message"], "Insufficient permissions")
|
|
213
|
+
|
|
214
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
215
|
+
def test_update_session_failure(self, mock_request):
|
|
216
|
+
"""Test update_session with API failure."""
|
|
217
|
+
client = RecceCloudClient(self.api_token)
|
|
218
|
+
|
|
219
|
+
# Mock error response
|
|
220
|
+
mock_response = MagicMock()
|
|
221
|
+
mock_response.status_code = 400
|
|
222
|
+
mock_response.text = "Bad request"
|
|
223
|
+
mock_request.return_value = mock_response
|
|
224
|
+
|
|
225
|
+
with self.assertRaises(RecceCloudException) as context:
|
|
226
|
+
client.update_session(self.org_id, self.project_id, self.session_id, "invalid_adapter")
|
|
227
|
+
|
|
228
|
+
self.assertEqual(context.exception.status_code, 400)
|
|
229
|
+
|
|
230
|
+
def test_recce_cloud_exception_with_json_detail(self):
|
|
231
|
+
"""Test RecceCloudException parses JSON detail."""
|
|
232
|
+
json_reason = json.dumps({"detail": "Invalid session ID"})
|
|
233
|
+
exception = RecceCloudException(reason=json_reason, status_code=400)
|
|
234
|
+
|
|
235
|
+
self.assertEqual(exception.status_code, 400)
|
|
236
|
+
self.assertEqual(exception.reason, "Invalid session ID")
|
|
237
|
+
self.assertIn("Invalid session ID", str(exception))
|
|
238
|
+
|
|
239
|
+
def test_recce_cloud_exception_with_plain_text(self):
|
|
240
|
+
"""Test RecceCloudException with plain text reason."""
|
|
241
|
+
plain_reason = "Connection timeout"
|
|
242
|
+
exception = RecceCloudException(reason=plain_reason, status_code=500)
|
|
243
|
+
|
|
244
|
+
self.assertEqual(exception.status_code, 500)
|
|
245
|
+
self.assertEqual(exception.reason, plain_reason)
|
|
246
|
+
|
|
247
|
+
@patch.dict("os.environ", {"RECCE_INSTANCE_ENV": "docker"})
|
|
248
|
+
@patch("recce_cloud.api.client.requests.request")
|
|
249
|
+
def test_docker_internal_url_replacement(self, mock_request):
|
|
250
|
+
"""Test localhost URL is replaced with docker internal URL."""
|
|
251
|
+
client = RecceCloudClient(self.api_token)
|
|
252
|
+
|
|
253
|
+
# Mock response with localhost URL
|
|
254
|
+
mock_response = MagicMock()
|
|
255
|
+
mock_response.status_code = 200
|
|
256
|
+
mock_response.json.return_value = {
|
|
257
|
+
"presigned_urls": {
|
|
258
|
+
"manifest_url": "http://localhost:8000/manifest.json",
|
|
259
|
+
"catalog_url": "http://localhost:8000/catalog.json",
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
mock_request.return_value = mock_response
|
|
263
|
+
|
|
264
|
+
result = client.get_upload_urls_by_session_id(self.org_id, self.project_id, self.session_id)
|
|
265
|
+
|
|
266
|
+
# URLs should be replaced with docker internal
|
|
267
|
+
self.assertIn("host.docker.internal", result["manifest_url"])
|
|
268
|
+
self.assertIn("host.docker.internal", result["catalog_url"])
|
|
269
|
+
self.assertNotIn("localhost", result["manifest_url"])
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
unittest.main()
|