Skip to content

Commit df24ccd

Browse files
CRUD operations for project.py along with unit tesst and example for integration testing
1 parent 6f24efe commit df24ccd

3 files changed

Lines changed: 213 additions & 107 deletions

File tree

Lines changed: 170 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,203 @@
11
"""
2-
Example integration test template for local development.
2+
Integration Test Example for python-tfe
33
4-
This file shows how to create integration tests for your local development.
5-
Copy this file to your tests/ directory and modify for your needs.
4+
This file demonstrates how to create integration tests that work with real HCP Terraform API.
5+
These tests create and delete actual resources, so use with caution!
66
7-
Requirements:
8-
1. Set environment variables: TFE_TOKEN and TFE_ORG
9-
2. Ensure your token has appropriate permissions
10-
3. Be careful - this makes real API calls to HCP Terraform!
7+
Setup Instructions:
8+
1. Create a test organization in HCP Terraform (https://app.terraform.io)
9+
2. Generate an organization or user API token with appropriate permissions
10+
3. Set environment variables:
11+
export TFE_TOKEN="your-api-token-here"
12+
export TFE_ORG="your-test-organization-name"
13+
4. Copy this file to your tests directory:
14+
cp examples/integration_test_example.py tests/test_integration_local.py
15+
5. Run the tests:
16+
pytest tests/test_integration_local.py -v -s
1117
12-
Usage:
13-
cp examples/integration_test_example.py tests/test_local_integration.py
14-
export TFE_TOKEN="your-token"
15-
export TFE_ORG="your-org"
16-
pytest tests/test_local_integration.py -v -s
18+
Important Notes:
19+
- These tests make real API calls and create/delete actual resources
20+
- Always use a dedicated test organization, never production
21+
- Tests will fail if you don't have proper permissions
22+
- Clean up is automatic, but verify resources are deleted after testing
1723
"""
1824

1925
import os
26+
import uuid
27+
2028
import pytest
29+
2130
from tfe._http import HTTPTransport
22-
from tfe.resources.projects import Projects
2331
from tfe.config import TFEConfig
32+
from tfe.resources.projects import Projects
2433

2534

2635
@pytest.fixture
27-
def real_projects_client():
28-
"""Create a real Projects client for local integration testing"""
36+
def integration_client():
37+
"""Create a real Projects client for integration testing"""
2938
token = os.environ.get("TFE_TOKEN")
3039
org = os.environ.get("TFE_ORG")
31-
32-
if not token or not org:
33-
pytest.skip("TFE_TOKEN and TFE_ORG environment variables required")
34-
40+
41+
if not token:
42+
pytest.skip(
43+
"TFE_TOKEN environment variable is required. "
44+
"Get your token from HCP Terraform: Settings → API Tokens"
45+
)
46+
47+
if not org:
48+
pytest.skip(
49+
"TFE_ORG environment variable is required. "
50+
"Use your organization name from HCP Terraform URL"
51+
)
52+
53+
print(f"\n🔧 Testing against organization: {org}")
54+
print(f"🔧 Using token: {token[:10]}...")
55+
3556
config = TFEConfig()
36-
transport = HTTPTransport(
37-
config.address,
38-
token,
39-
timeout=config.timeout,
40-
verify_tls=config.verify_tls,
41-
user_agent_suffix=None,
42-
max_retries=3,
43-
backoff_base=0.1,
44-
backoff_cap=1.0,
45-
backoff_jitter=True,
46-
http2=False,
47-
proxies=None,
48-
ca_bundle=None,
49-
)
50-
57+
58+
try:
59+
transport = HTTPTransport(
60+
config.address,
61+
token,
62+
timeout=config.timeout,
63+
verify_tls=config.verify_tls,
64+
user_agent_suffix=None,
65+
max_retries=3,
66+
backoff_base=0.1,
67+
backoff_cap=1.0,
68+
backoff_jitter=True,
69+
http2=False,
70+
proxies=None,
71+
ca_bundle=None,
72+
)
73+
except Exception as e:
74+
pytest.fail(f"Failed to create HTTP transport: {e}")
75+
5176
return Projects(transport), org
5277

5378

54-
def test_list_projects_integration(real_projects_client):
55-
"""Example integration test for listing projects"""
56-
projects, org = real_projects_client
57-
58-
project_list = list(projects.list(org))
59-
print(f"Found {len(project_list)} projects in organization '{org}'")
60-
61-
assert isinstance(project_list, list)
62-
# Add your assertions here
79+
def test_list_projects_integration(integration_client):
80+
"""Test listing projects in your HCP Terraform organization
81+
82+
This is the safest test to run first - it only reads data.
83+
"""
84+
projects, org = integration_client
85+
86+
try:
87+
project_list = list(projects.list(org))
88+
print(f"✅ Found {len(project_list)} projects in organization '{org}'")
89+
90+
assert isinstance(project_list, list)
91+
92+
if project_list:
93+
project = project_list[0]
94+
assert hasattr(project, 'id'), "Project should have an ID"
95+
assert hasattr(project, 'name'), "Project should have a name"
96+
assert hasattr(project, 'organization'), "Project should have an organization"
97+
print(f"📋 Example project: {project.name} (ID: {project.id})")
98+
else:
99+
print("📋 No projects found - this is normal for a new organization")
63100

101+
except Exception as e:
102+
pytest.fail(
103+
f"Failed to list projects. Check your TFE_TOKEN and TFE_ORG. Error: {e}"
104+
)
64105

65-
def test_project_crud_integration(real_projects_client):
66-
"""Example integration test for full CRUD operations
67-
68-
WARNING: This creates and deletes real resources in HCP Terraform!
106+
107+
def test_project_crud_integration(integration_client):
108+
"""Test complete Create, Read, Update, Delete operations
109+
110+
⚠️ WARNING: This test creates and deletes real resources!
111+
Only run this in a test organization, never in production.
69112
"""
70-
projects, org = real_projects_client
71-
72-
test_name = "example-test-project"
113+
projects, org = integration_client
114+
115+
# Generate unique names to avoid conflicts
116+
unique_id = str(uuid.uuid4())[:8]
117+
test_name = f"integration-test-{unique_id}"
118+
updated_name = f"integration-test-{unique_id}-updated"
73119
project_id = None
74-
120+
75121
try:
76-
# CREATE
122+
# CREATE - Test project creation
123+
print(f"🔨 Creating project: {test_name}")
77124
created_project = projects.create(org, test_name)
125+
126+
assert created_project.name == test_name, f"Expected name {test_name}, got {created_project.name}"
127+
assert created_project.organization == org, f"Expected org {org}, got {created_project.organization}"
128+
assert created_project.id.startswith("prj-"), f"Project ID should start with 'prj-', got {created_project.id}"
129+
78130
project_id = created_project.id
79-
assert created_project.name == test_name
80-
81-
# READ
131+
print(f"✅ Created project: {project_id}")
132+
133+
# READ - Test reading the created project
134+
print(f"📖 Reading project: {project_id}")
82135
read_project = projects.read(project_id)
83-
assert read_project.id == project_id
84-
85-
# UPDATE
86-
updated_name = f"{test_name}-updated"
136+
137+
assert read_project.id == project_id, f"Expected ID {project_id}, got {read_project.id}"
138+
assert read_project.name == test_name, f"Expected name {test_name}, got {read_project.name}"
139+
print(f"✅ Successfully read project: {read_project.name}")
140+
141+
# UPDATE - Test updating the project name
142+
print(f"✏️ Updating project name to: {updated_name}")
87143
updated_project = projects.update(project_id, updated_name)
88-
assert updated_project.name == updated_name
89-
144+
145+
assert updated_project.id == project_id, f"Project ID should remain {project_id}"
146+
assert updated_project.name == updated_name, f"Expected updated name {updated_name}, got {updated_project.name}"
147+
print(f"✅ Successfully updated project: {updated_project.name}")
148+
149+
except Exception as e:
150+
pytest.fail(f"CRUD operation failed: {e}")
151+
90152
finally:
91-
# CLEANUP - Always delete test resources
153+
# DELETE - Always clean up, even if tests fail
92154
if project_id:
93155
try:
156+
print(f"🗑️ Deleting test project: {project_id}")
94157
projects.delete(project_id)
95-
print(f"✅ Cleaned up project: {project_id}")
158+
print("✅ Test project deleted successfully")
96159
except Exception as e:
97-
print(f"❌ Cleanup failed: {e}")
160+
print(f"❌ Warning: Failed to clean up project {project_id}: {e}")
161+
print(" You may need to manually delete this project in HCP Terraform")
162+
163+
164+
def test_error_handling_integration(integration_client):
165+
"""Test that the client handles API errors appropriately"""
166+
projects, org = integration_client
167+
168+
# Test reading a non-existent project
169+
fake_project_id = "prj-nonexistent123456789"
170+
171+
try:
172+
projects.read(fake_project_id)
173+
pytest.fail("Should have raised an exception for non-existent project")
174+
except Exception as e:
175+
print(f"✅ Correctly handled error for non-existent project: {type(e).__name__}")
176+
# This should raise a NotFound or similar error
177+
assert "not found" in str(e).lower() or "404" in str(e)
178+
179+
180+
if __name__ == "__main__":
181+
"""
182+
You can also run this file directly for quick testing:
183+
184+
export TFE_TOKEN="your-token"
185+
export TFE_ORG="your-org"
186+
python examples/integration_test_example.py
187+
"""
188+
import sys
189+
190+
token = os.environ.get("TFE_TOKEN")
191+
org = os.environ.get("TFE_ORG")
192+
193+
if not token or not org:
194+
print("❌ Please set TFE_TOKEN and TFE_ORG environment variables")
195+
print(" export TFE_TOKEN='your-hcp-terraform-token'")
196+
print(" export TFE_ORG='your-organization-name'")
197+
sys.exit(1)
198+
199+
print("🧪 Running integration tests directly...")
200+
print(" For full pytest features, use: pytest examples/integration_test_example.py -v -s")
201+
202+
# Simple direct execution
203+
pytest.main([__file__, "-v", "-s"])

‎src/tfe/resources/projects.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ def create(self, organization: str, name: str) -> Project:
3131
}
3232
}
3333
}
34-
34+
3535
# Use json_body parameter (correct parameter name)
3636
response = self.t.request("POST", path, json_body=payload)
3737
data = response.json()["data"]
3838
attr = data.get("attributes", {}) or {}
39-
39+
4040
return Project(
4141
id=_safe_str(data.get("id")),
4242
name=_safe_str(attr.get("name")),
@@ -49,12 +49,12 @@ def read(self, project_id: str) -> Project:
4949
response = self.t.request("GET", path)
5050
data = response.json()["data"]
5151
attr = data.get("attributes", {}) or {}
52-
52+
5353
# Get organization from relationships if available
5454
relationships = data.get("relationships", {})
5555
org_data = relationships.get("organization", {}).get("data", {})
5656
organization = _safe_str(org_data.get("id"))
57-
57+
5858
return Project(
5959
id=_safe_str(data.get("id")),
6060
name=_safe_str(attr.get("name")),
@@ -73,17 +73,17 @@ def update(self, project_id: str, name: str) -> Project:
7373
}
7474
}
7575
}
76-
76+
7777
# Use json_body parameter (correct parameter name)
7878
response = self.t.request("PATCH", path, json_body=payload)
7979
data = response.json()["data"]
8080
attr = data.get("attributes", {}) or {}
81-
81+
8282
# Get organization from relationships if available
8383
relationships = data.get("relationships", {})
8484
org_data = relationships.get("organization", {}).get("data", {})
8585
organization = _safe_str(org_data.get("id"))
86-
86+
8787
return Project(
8888
id=_safe_str(data.get("id")),
8989
name=_safe_str(attr.get("name")),
@@ -93,4 +93,4 @@ def update(self, project_id: str, name: str) -> Project:
9393
def delete(self, project_id: str) -> None:
9494
"""Delete a project"""
9595
path = f"/api/v2/projects/{project_id}"
96-
self.t.request("DELETE", path)
96+
self.t.request("DELETE", path)

0 commit comments

Comments
 (0)