stackport 0.1.7__tar.gz → 0.1.8__tar.gz

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.
Files changed (81) hide show
  1. {stackport-0.1.7/stackport.egg-info → stackport-0.1.8}/PKG-INFO +4 -4
  2. {stackport-0.1.7 → stackport-0.1.8}/README.md +3 -3
  3. {stackport-0.1.7 → stackport-0.1.8}/backend/main.py +2 -1
  4. stackport-0.1.8/backend/routes/secretsmanager.py +112 -0
  5. {stackport-0.1.7 → stackport-0.1.8}/pyproject.toml +1 -1
  6. {stackport-0.1.7 → stackport-0.1.8/stackport.egg-info}/PKG-INFO +4 -4
  7. {stackport-0.1.7 → stackport-0.1.8}/stackport.egg-info/SOURCES.txt +6 -4
  8. stackport-0.1.8/tests/test_secretsmanager_routes.py +281 -0
  9. stackport-0.1.8/ui/dist/assets/index-BAMXqPoR.js +537 -0
  10. stackport-0.1.8/ui/dist/assets/index-CU1gzmix.css +1 -0
  11. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/index.html +2 -2
  12. stackport-0.1.7/ui/dist/assets/index-DI-V3ZCb.js +0 -527
  13. stackport-0.1.7/ui/dist/assets/index-DM3oKaVN.css +0 -1
  14. {stackport-0.1.7 → stackport-0.1.8}/LICENSE +0 -0
  15. {stackport-0.1.7 → stackport-0.1.8}/MANIFEST.in +0 -0
  16. {stackport-0.1.7 → stackport-0.1.8}/backend/__init__.py +0 -0
  17. {stackport-0.1.7 → stackport-0.1.8}/backend/aws_client.py +0 -0
  18. {stackport-0.1.7 → stackport-0.1.8}/backend/cache.py +0 -0
  19. {stackport-0.1.7 → stackport-0.1.8}/backend/config.py +0 -0
  20. {stackport-0.1.7 → stackport-0.1.8}/backend/routes/__init__.py +0 -0
  21. {stackport-0.1.7 → stackport-0.1.8}/backend/routes/dynamodb.py +0 -0
  22. {stackport-0.1.7 → stackport-0.1.8}/backend/routes/ec2.py +0 -0
  23. {stackport-0.1.7 → stackport-0.1.8}/backend/routes/iam.py +0 -0
  24. {stackport-0.1.7 → stackport-0.1.8}/backend/routes/lambda_svc.py +0 -0
  25. {stackport-0.1.7 → stackport-0.1.8}/backend/routes/logs.py +0 -0
  26. {stackport-0.1.7 → stackport-0.1.8}/backend/routes/resources.py +0 -0
  27. {stackport-0.1.7 → stackport-0.1.8}/backend/routes/s3.py +0 -0
  28. {stackport-0.1.7 → stackport-0.1.8}/backend/routes/sqs.py +0 -0
  29. {stackport-0.1.7 → stackport-0.1.8}/backend/routes/stats.py +0 -0
  30. {stackport-0.1.7 → stackport-0.1.8}/setup.cfg +0 -0
  31. {stackport-0.1.7 → stackport-0.1.8}/stackport.egg-info/dependency_links.txt +0 -0
  32. {stackport-0.1.7 → stackport-0.1.8}/stackport.egg-info/entry_points.txt +0 -0
  33. {stackport-0.1.7 → stackport-0.1.8}/stackport.egg-info/requires.txt +0 -0
  34. {stackport-0.1.7 → stackport-0.1.8}/stackport.egg-info/top_level.txt +0 -0
  35. {stackport-0.1.7 → stackport-0.1.8}/tests/test_cache.py +0 -0
  36. {stackport-0.1.7 → stackport-0.1.8}/tests/test_client.py +0 -0
  37. {stackport-0.1.7 → stackport-0.1.8}/tests/test_config.py +0 -0
  38. {stackport-0.1.7 → stackport-0.1.8}/tests/test_dynamodb_routes.py +0 -0
  39. {stackport-0.1.7 → stackport-0.1.8}/tests/test_ec2_routes.py +0 -0
  40. {stackport-0.1.7 → stackport-0.1.8}/tests/test_iam_routes.py +0 -0
  41. {stackport-0.1.7 → stackport-0.1.8}/tests/test_lambda_routes.py +0 -0
  42. {stackport-0.1.7 → stackport-0.1.8}/tests/test_logs_routes.py +0 -0
  43. {stackport-0.1.7 → stackport-0.1.8}/tests/test_registries.py +0 -0
  44. {stackport-0.1.7 → stackport-0.1.8}/tests/test_routes.py +0 -0
  45. {stackport-0.1.7 → stackport-0.1.8}/tests/test_sqs_routes.py +0 -0
  46. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/acm.svg +0 -0
  47. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/apigateway.svg +0 -0
  48. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/appsync.svg +0 -0
  49. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/athena.svg +0 -0
  50. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/cloudformation.svg +0 -0
  51. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/cloudfront.svg +0 -0
  52. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  53. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/dynamodb.svg +0 -0
  54. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/ec2.svg +0 -0
  55. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/ecr.svg +0 -0
  56. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/ecs.svg +0 -0
  57. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/elasticache.svg +0 -0
  58. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  59. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  60. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  61. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/events.svg +0 -0
  62. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/firehose.svg +0 -0
  63. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/glue.svg +0 -0
  64. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/iam.svg +0 -0
  65. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/kinesis.svg +0 -0
  66. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/kms.svg +0 -0
  67. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/lambda.svg +0 -0
  68. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/logs.svg +0 -0
  69. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/monitoring.svg +0 -0
  70. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/rds.svg +0 -0
  71. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/route53.svg +0 -0
  72. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/s3.svg +0 -0
  73. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  74. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/ses.svg +0 -0
  75. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/sns.svg +0 -0
  76. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/sqs.svg +0 -0
  77. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/ssm.svg +0 -0
  78. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  79. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/aws-icons/wafv2.svg +0 -0
  80. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/favicon.png +0 -0
  81. {stackport-0.1.7 → stackport-0.1.8}/ui/dist/favicon.svg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -50,13 +50,13 @@ Dynamic: license-file
50
50
  ## Screenshots
51
51
 
52
52
  **Dashboard** — Service overview with resource counts and health status
53
- ![StackPort Dashboard](docs/images/dashboard.jpeg)
53
+ ![StackPort Dashboard](docs/images/dashboard.jpeg?v=1)
54
54
 
55
55
  **DynamoDB Browser** — Generic resource table with search, pagination, and detail view
56
- ![DynamoDB Resources](docs/images/dynamo.jpeg)
56
+ ![DynamoDB Resources](docs/images/dynamo.jpeg?v=1)
57
57
 
58
58
  **S3 Browser** — File browser with folder navigation and object preview
59
- ![S3 Browser](docs/images/s3.jpeg)
59
+ ![S3 Browser](docs/images/s3.jpeg?v=1)
60
60
 
61
61
  ## Features
62
62
 
@@ -18,13 +18,13 @@
18
18
  ## Screenshots
19
19
 
20
20
  **Dashboard** — Service overview with resource counts and health status
21
- ![StackPort Dashboard](docs/images/dashboard.jpeg)
21
+ ![StackPort Dashboard](docs/images/dashboard.jpeg?v=1)
22
22
 
23
23
  **DynamoDB Browser** — Generic resource table with search, pagination, and detail view
24
- ![DynamoDB Resources](docs/images/dynamo.jpeg)
24
+ ![DynamoDB Resources](docs/images/dynamo.jpeg?v=1)
25
25
 
26
26
  **S3 Browser** — File browser with folder navigation and object preview
27
- ![S3 Browser](docs/images/s3.jpeg)
27
+ ![S3 Browser](docs/images/s3.jpeg?v=1)
28
28
 
29
29
  ## Features
30
30
 
@@ -8,7 +8,7 @@ from fastapi.responses import FileResponse
8
8
  from fastapi.staticfiles import StaticFiles
9
9
 
10
10
  from backend.config import LOG_LEVEL, STACKPORT_PORT
11
- from backend.routes import dynamodb, ec2, iam, lambda_svc, logs, resources, s3, sqs, stats
11
+ from backend.routes import dynamodb, ec2, iam, lambda_svc, logs, resources, s3, secretsmanager, sqs, stats
12
12
 
13
13
 
14
14
  class HealthcheckFilter(logging.Filter):
@@ -47,6 +47,7 @@ app.include_router(sqs.router, prefix="/api/sqs", tags=["sqs"])
47
47
  app.include_router(iam.router, prefix="/api/iam", tags=["iam"])
48
48
  app.include_router(ec2.router, prefix="/api/ec2", tags=["ec2"])
49
49
  app.include_router(logs.router, prefix="/api/logs", tags=["logs"])
50
+ app.include_router(secretsmanager.router, prefix="/api/secretsmanager", tags=["secretsmanager"])
50
51
  app.include_router(resources.router, prefix="/api")
51
52
 
52
53
  # Serve UI static files — mount assets under /assets, SPA fallback for everything else
@@ -0,0 +1,112 @@
1
+ """Secrets Manager service-specific routes."""
2
+
3
+ import base64
4
+ from typing import Any
5
+
6
+ from fastapi import APIRouter, HTTPException
7
+
8
+ from backend.aws_client import get_client
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ def _format_date(dt) -> str | None:
14
+ """Format a datetime to ISO string, or return None."""
15
+ if dt is None:
16
+ return None
17
+ try:
18
+ return dt.isoformat()
19
+ except Exception:
20
+ return str(dt)
21
+
22
+
23
+ @router.get("/secrets")
24
+ def list_secrets() -> dict[str, Any]:
25
+ """List all secrets with metadata."""
26
+ try:
27
+ client = get_client("secretsmanager")
28
+ paginator = client.get_paginator("list_secrets")
29
+
30
+ secrets = []
31
+ for page in paginator.paginate():
32
+ for secret in page.get("SecretList", []):
33
+ secrets.append(
34
+ {
35
+ "name": secret.get("Name"),
36
+ "arn": secret.get("ARN"),
37
+ "description": secret.get("Description", ""),
38
+ "createdDate": _format_date(secret.get("CreatedDate")),
39
+ "lastChangedDate": _format_date(
40
+ secret.get("LastChangedDate")
41
+ ),
42
+ "lastAccessedDate": _format_date(
43
+ secret.get("LastAccessedDate")
44
+ ),
45
+ "rotationEnabled": secret.get("RotationEnabled", False),
46
+ "tags": {
47
+ tag["Key"]: tag["Value"]
48
+ for tag in secret.get("Tags", [])
49
+ },
50
+ }
51
+ )
52
+
53
+ return {"secrets": secrets}
54
+ except Exception as e:
55
+ raise HTTPException(status_code=500, detail=str(e))
56
+
57
+
58
+ @router.get("/secrets/{secret_id:path}")
59
+ def get_secret_detail(secret_id: str) -> dict[str, Any]:
60
+ """Get secret metadata and value."""
61
+ try:
62
+ client = get_client("secretsmanager")
63
+
64
+ # Get metadata
65
+ try:
66
+ meta = client.describe_secret(SecretId=secret_id)
67
+ except client.exceptions.ResourceNotFoundException:
68
+ raise HTTPException(
69
+ status_code=404, detail=f"Secret '{secret_id}' not found"
70
+ )
71
+
72
+ # Get value
73
+ secret_value = None
74
+ secret_binary = None
75
+ version_id = None
76
+ version_stages = None
77
+ try:
78
+ value_resp = client.get_secret_value(SecretId=secret_id)
79
+ secret_value = value_resp.get("SecretString")
80
+ raw_binary = value_resp.get("SecretBinary")
81
+ if raw_binary is not None:
82
+ secret_binary = base64.b64encode(raw_binary).decode("utf-8")
83
+ version_id = value_resp.get("VersionId")
84
+ version_stages = value_resp.get("VersionStages")
85
+ except Exception:
86
+ # Value may not be retrievable (e.g., pending deletion)
87
+ pass
88
+
89
+ return {
90
+ "name": meta.get("Name"),
91
+ "arn": meta.get("ARN"),
92
+ "description": meta.get("Description", ""),
93
+ "createdDate": _format_date(meta.get("CreatedDate")),
94
+ "lastChangedDate": _format_date(meta.get("LastChangedDate")),
95
+ "lastAccessedDate": _format_date(meta.get("LastAccessedDate")),
96
+ "rotationEnabled": meta.get("RotationEnabled", False),
97
+ "rotationRules": meta.get("RotationRules"),
98
+ "rotationLambdaARN": meta.get("RotationLambdaARN"),
99
+ "deletedDate": _format_date(meta.get("DeletedDate")),
100
+ "tags": {
101
+ tag["Key"]: tag["Value"]
102
+ for tag in meta.get("Tags", [])
103
+ },
104
+ "versionId": version_id,
105
+ "versionStages": version_stages,
106
+ "secretValue": secret_value,
107
+ "secretBinary": secret_binary,
108
+ }
109
+ except HTTPException:
110
+ raise
111
+ except Exception as e:
112
+ raise HTTPException(status_code=500, detail=str(e))
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stackport"
3
- version = "0.1.7"
3
+ version = "0.1.8"
4
4
  description = "Universal AWS resource browser for local emulators"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
@@ -50,13 +50,13 @@ Dynamic: license-file
50
50
  ## Screenshots
51
51
 
52
52
  **Dashboard** — Service overview with resource counts and health status
53
- ![StackPort Dashboard](docs/images/dashboard.jpeg)
53
+ ![StackPort Dashboard](docs/images/dashboard.jpeg?v=1)
54
54
 
55
55
  **DynamoDB Browser** — Generic resource table with search, pagination, and detail view
56
- ![DynamoDB Resources](docs/images/dynamo.jpeg)
56
+ ![DynamoDB Resources](docs/images/dynamo.jpeg?v=1)
57
57
 
58
58
  **S3 Browser** — File browser with folder navigation and object preview
59
- ![S3 Browser](docs/images/s3.jpeg)
59
+ ![S3 Browser](docs/images/s3.jpeg?v=1)
60
60
 
61
61
  ## Features
62
62
 
@@ -10,8 +10,8 @@ backend/main.py
10
10
  backend/../ui/dist/favicon.png
11
11
  backend/../ui/dist/favicon.svg
12
12
  backend/../ui/dist/index.html
13
- backend/../ui/dist/assets/index-DI-V3ZCb.js
14
- backend/../ui/dist/assets/index-DM3oKaVN.css
13
+ backend/../ui/dist/assets/index-BAMXqPoR.js
14
+ backend/../ui/dist/assets/index-CU1gzmix.css
15
15
  backend/../ui/dist/aws-icons/acm.svg
16
16
  backend/../ui/dist/aws-icons/apigateway.svg
17
17
  backend/../ui/dist/aws-icons/appsync.svg
@@ -54,6 +54,7 @@ backend/routes/lambda_svc.py
54
54
  backend/routes/logs.py
55
55
  backend/routes/resources.py
56
56
  backend/routes/s3.py
57
+ backend/routes/secretsmanager.py
57
58
  backend/routes/sqs.py
58
59
  backend/routes/stats.py
59
60
  stackport.egg-info/PKG-INFO
@@ -72,12 +73,13 @@ tests/test_lambda_routes.py
72
73
  tests/test_logs_routes.py
73
74
  tests/test_registries.py
74
75
  tests/test_routes.py
76
+ tests/test_secretsmanager_routes.py
75
77
  tests/test_sqs_routes.py
76
78
  ui/dist/favicon.png
77
79
  ui/dist/favicon.svg
78
80
  ui/dist/index.html
79
- ui/dist/assets/index-DI-V3ZCb.js
80
- ui/dist/assets/index-DM3oKaVN.css
81
+ ui/dist/assets/index-BAMXqPoR.js
82
+ ui/dist/assets/index-CU1gzmix.css
81
83
  ui/dist/aws-icons/acm.svg
82
84
  ui/dist/aws-icons/apigateway.svg
83
85
  ui/dist/aws-icons/appsync.svg
@@ -0,0 +1,281 @@
1
+ """Integration tests for Secrets Manager API routes."""
2
+
3
+ import os
4
+
5
+ os.environ.setdefault("AWS_ENDPOINT_URL", "http://localhost:4566")
6
+
7
+ from datetime import datetime, timezone
8
+ from unittest.mock import MagicMock, patch
9
+
10
+ from fastapi.testclient import TestClient
11
+
12
+ from backend.main import app
13
+
14
+ client = TestClient(app)
15
+
16
+ CREATED = datetime(2025, 1, 15, 10, 30, 0, tzinfo=timezone.utc)
17
+ CHANGED = datetime(2025, 3, 20, 14, 0, 0, tzinfo=timezone.utc)
18
+
19
+
20
+ class TestListSecrets:
21
+ @patch("backend.routes.secretsmanager.get_client")
22
+ def test_list_secrets_empty(self, mock_get_client):
23
+ mock_sm = MagicMock()
24
+ mock_get_client.return_value = mock_sm
25
+ paginator = MagicMock()
26
+ mock_sm.get_paginator.return_value = paginator
27
+ paginator.paginate.return_value = [{"SecretList": []}]
28
+
29
+ resp = client.get("/api/secretsmanager/secrets")
30
+ assert resp.status_code == 200
31
+ data = resp.json()
32
+ assert data["secrets"] == []
33
+
34
+ @patch("backend.routes.secretsmanager.get_client")
35
+ def test_list_secrets_with_data(self, mock_get_client):
36
+ mock_sm = MagicMock()
37
+ mock_get_client.return_value = mock_sm
38
+ paginator = MagicMock()
39
+ mock_sm.get_paginator.return_value = paginator
40
+ paginator.paginate.return_value = [
41
+ {
42
+ "SecretList": [
43
+ {
44
+ "Name": "prod/db-password",
45
+ "ARN": "arn:aws:secretsmanager:us-east-1:000:secret:prod/db-password-abc",
46
+ "Description": "Production database password",
47
+ "CreatedDate": CREATED,
48
+ "LastChangedDate": CHANGED,
49
+ "LastAccessedDate": None,
50
+ "RotationEnabled": False,
51
+ "Tags": [
52
+ {"Key": "env", "Value": "prod"},
53
+ {"Key": "team", "Value": "backend"},
54
+ ],
55
+ }
56
+ ]
57
+ }
58
+ ]
59
+
60
+ resp = client.get("/api/secretsmanager/secrets")
61
+ assert resp.status_code == 200
62
+ data = resp.json()
63
+ assert len(data["secrets"]) == 1
64
+ s = data["secrets"][0]
65
+ assert s["name"] == "prod/db-password"
66
+ assert s["arn"] == "arn:aws:secretsmanager:us-east-1:000:secret:prod/db-password-abc"
67
+ assert s["description"] == "Production database password"
68
+ assert s["createdDate"] == CREATED.isoformat()
69
+ assert s["lastChangedDate"] == CHANGED.isoformat()
70
+ assert s["lastAccessedDate"] is None
71
+ assert s["rotationEnabled"] is False
72
+ assert s["tags"] == {"env": "prod", "team": "backend"}
73
+
74
+ @patch("backend.routes.secretsmanager.get_client")
75
+ def test_list_secrets_no_tags(self, mock_get_client):
76
+ mock_sm = MagicMock()
77
+ mock_get_client.return_value = mock_sm
78
+ paginator = MagicMock()
79
+ mock_sm.get_paginator.return_value = paginator
80
+ paginator.paginate.return_value = [
81
+ {
82
+ "SecretList": [
83
+ {
84
+ "Name": "my-secret",
85
+ "ARN": "arn:aws:secretsmanager:us-east-1:000:secret:my-secret-xyz",
86
+ }
87
+ ]
88
+ }
89
+ ]
90
+
91
+ resp = client.get("/api/secretsmanager/secrets")
92
+ assert resp.status_code == 200
93
+ s = resp.json()["secrets"][0]
94
+ assert s["tags"] == {}
95
+ assert s["description"] == ""
96
+ assert s["rotationEnabled"] is False
97
+
98
+ @patch("backend.routes.secretsmanager.get_client")
99
+ def test_list_secrets_multiple_pages(self, mock_get_client):
100
+ mock_sm = MagicMock()
101
+ mock_get_client.return_value = mock_sm
102
+ paginator = MagicMock()
103
+ mock_sm.get_paginator.return_value = paginator
104
+ paginator.paginate.return_value = [
105
+ {"SecretList": [{"Name": "secret-1", "ARN": "arn:1"}]},
106
+ {"SecretList": [{"Name": "secret-2", "ARN": "arn:2"}]},
107
+ ]
108
+
109
+ resp = client.get("/api/secretsmanager/secrets")
110
+ assert resp.status_code == 200
111
+ data = resp.json()
112
+ assert len(data["secrets"]) == 2
113
+ assert data["secrets"][0]["name"] == "secret-1"
114
+ assert data["secrets"][1]["name"] == "secret-2"
115
+
116
+ @patch("backend.routes.secretsmanager.get_client")
117
+ def test_list_secrets_rotation_enabled(self, mock_get_client):
118
+ mock_sm = MagicMock()
119
+ mock_get_client.return_value = mock_sm
120
+ paginator = MagicMock()
121
+ mock_sm.get_paginator.return_value = paginator
122
+ paginator.paginate.return_value = [
123
+ {
124
+ "SecretList": [
125
+ {
126
+ "Name": "rotated-secret",
127
+ "ARN": "arn:rotated",
128
+ "RotationEnabled": True,
129
+ }
130
+ ]
131
+ }
132
+ ]
133
+
134
+ resp = client.get("/api/secretsmanager/secrets")
135
+ assert resp.status_code == 200
136
+ assert resp.json()["secrets"][0]["rotationEnabled"] is True
137
+
138
+
139
+ class TestGetSecretDetail:
140
+ @patch("backend.routes.secretsmanager.get_client")
141
+ def test_get_secret_with_string_value(self, mock_get_client):
142
+ mock_sm = MagicMock()
143
+ mock_get_client.return_value = mock_sm
144
+ mock_sm.describe_secret.return_value = {
145
+ "Name": "prod/db-password",
146
+ "ARN": "arn:aws:secretsmanager:us-east-1:000:secret:prod/db-password-abc",
147
+ "Description": "Production database password",
148
+ "CreatedDate": CREATED,
149
+ "LastChangedDate": CHANGED,
150
+ "LastAccessedDate": None,
151
+ "RotationEnabled": False,
152
+ "RotationRules": None,
153
+ "RotationLambdaARN": None,
154
+ "DeletedDate": None,
155
+ "Tags": [{"Key": "env", "Value": "prod"}],
156
+ }
157
+ mock_sm.get_secret_value.return_value = {
158
+ "SecretString": '{"username":"admin","password":"s3cret"}',
159
+ "VersionId": "ver-001",
160
+ "VersionStages": ["AWSCURRENT"],
161
+ }
162
+
163
+ resp = client.get("/api/secretsmanager/secrets/prod%2Fdb-password")
164
+ assert resp.status_code == 200
165
+ data = resp.json()
166
+ assert data["name"] == "prod/db-password"
167
+ assert data["description"] == "Production database password"
168
+ assert data["secretValue"] == '{"username":"admin","password":"s3cret"}'
169
+ assert data["secretBinary"] is None
170
+ assert data["versionId"] == "ver-001"
171
+ assert data["versionStages"] == ["AWSCURRENT"]
172
+ assert data["tags"] == {"env": "prod"}
173
+ assert data["createdDate"] == CREATED.isoformat()
174
+ assert data["rotationEnabled"] is False
175
+
176
+ @patch("backend.routes.secretsmanager.get_client")
177
+ def test_get_secret_with_binary_value(self, mock_get_client):
178
+ mock_sm = MagicMock()
179
+ mock_get_client.return_value = mock_sm
180
+ mock_sm.describe_secret.return_value = {
181
+ "Name": "binary-secret",
182
+ "ARN": "arn:binary",
183
+ "Tags": [],
184
+ }
185
+ raw_bytes = b"\x00\x01\x02\x03\xff"
186
+ mock_sm.get_secret_value.return_value = {
187
+ "SecretBinary": raw_bytes,
188
+ "VersionId": "ver-bin",
189
+ "VersionStages": ["AWSCURRENT"],
190
+ }
191
+
192
+ resp = client.get("/api/secretsmanager/secrets/binary-secret")
193
+ assert resp.status_code == 200
194
+ data = resp.json()
195
+ assert data["secretValue"] is None
196
+ import base64
197
+ assert data["secretBinary"] == base64.b64encode(raw_bytes).decode("utf-8")
198
+ assert data["versionId"] == "ver-bin"
199
+
200
+ @patch("backend.routes.secretsmanager.get_client")
201
+ def test_get_secret_not_found(self, mock_get_client):
202
+ mock_sm = MagicMock()
203
+ mock_get_client.return_value = mock_sm
204
+ mock_sm.exceptions.ResourceNotFoundException = type(
205
+ "ResourceNotFoundException", (Exception,), {}
206
+ )
207
+ mock_sm.describe_secret.side_effect = (
208
+ mock_sm.exceptions.ResourceNotFoundException()
209
+ )
210
+
211
+ resp = client.get("/api/secretsmanager/secrets/nonexistent")
212
+ assert resp.status_code == 404
213
+
214
+ @patch("backend.routes.secretsmanager.get_client")
215
+ def test_get_secret_value_not_retrievable(self, mock_get_client):
216
+ """When get_secret_value fails (e.g. pending deletion), metadata is still returned."""
217
+ mock_sm = MagicMock()
218
+ mock_get_client.return_value = mock_sm
219
+ mock_sm.describe_secret.return_value = {
220
+ "Name": "deleted-secret",
221
+ "ARN": "arn:deleted",
222
+ "DeletedDate": CHANGED,
223
+ "Tags": [],
224
+ }
225
+ mock_sm.get_secret_value.side_effect = Exception("marked for deletion")
226
+
227
+ resp = client.get("/api/secretsmanager/secrets/deleted-secret")
228
+ assert resp.status_code == 200
229
+ data = resp.json()
230
+ assert data["name"] == "deleted-secret"
231
+ assert data["secretValue"] is None
232
+ assert data["secretBinary"] is None
233
+ assert data["versionId"] is None
234
+ assert data["versionStages"] is None
235
+ assert data["deletedDate"] == CHANGED.isoformat()
236
+
237
+ @patch("backend.routes.secretsmanager.get_client")
238
+ def test_get_secret_with_rotation(self, mock_get_client):
239
+ mock_sm = MagicMock()
240
+ mock_get_client.return_value = mock_sm
241
+ mock_sm.describe_secret.return_value = {
242
+ "Name": "rotated-secret",
243
+ "ARN": "arn:rotated",
244
+ "RotationEnabled": True,
245
+ "RotationRules": {"AutomaticallyAfterDays": 30},
246
+ "RotationLambdaARN": "arn:aws:lambda:us-east-1:000:function:rotate-fn",
247
+ "Tags": [],
248
+ }
249
+ mock_sm.get_secret_value.return_value = {
250
+ "SecretString": "rotated-value",
251
+ "VersionId": "ver-rot",
252
+ "VersionStages": ["AWSCURRENT"],
253
+ }
254
+
255
+ resp = client.get("/api/secretsmanager/secrets/rotated-secret")
256
+ assert resp.status_code == 200
257
+ data = resp.json()
258
+ assert data["rotationEnabled"] is True
259
+ assert data["rotationRules"] == {"AutomaticallyAfterDays": 30}
260
+ assert data["rotationLambdaARN"] == "arn:aws:lambda:us-east-1:000:function:rotate-fn"
261
+ assert data["secretValue"] == "rotated-value"
262
+
263
+ @patch("backend.routes.secretsmanager.get_client")
264
+ def test_get_secret_plain_text_value(self, mock_get_client):
265
+ mock_sm = MagicMock()
266
+ mock_get_client.return_value = mock_sm
267
+ mock_sm.describe_secret.return_value = {
268
+ "Name": "api-key",
269
+ "ARN": "arn:api-key",
270
+ "Tags": [],
271
+ }
272
+ mock_sm.get_secret_value.return_value = {
273
+ "SecretString": "sk-abc123def456",
274
+ "VersionId": "ver-plain",
275
+ "VersionStages": ["AWSCURRENT"],
276
+ }
277
+
278
+ resp = client.get("/api/secretsmanager/secrets/api-key")
279
+ assert resp.status_code == 200
280
+ data = resp.json()
281
+ assert data["secretValue"] == "sk-abc123def456"