Skip to content

Commit 37defd4

Browse files
committed
fix(product-search-service): add JWT auth to integration test driver for prod
The Product Management Service API requires an Authorization: Bearer JWT on mutating endpoints. The Python integration driver was sending unauthenticated requests, which succeeds nowhere — pipeline tests were just being skipped in ephemeral envs, masking the gap until prod ran them. Fetches /{env}/shared/secret-access-key from SSM (same path as the Go driver) and generates an HS256 ADMIN token on each mutating request.
1 parent 23e14a1 commit 37defd4

5 files changed

Lines changed: 81 additions & 12 deletions

File tree

src/pricing-service/lib/pricing-api/pricingApiStack.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { SharedProps } from "../constructs/sharedFunctionProps";
1111
import { Api } from "./api";
1212
import { StringParameter } from "aws-cdk-lib/aws-ssm";
1313
import { PricingServiceProps } from "./pricingServiceProps";
14-
15-
const isWorkshopBuild = process.env.WORKSHOP_BUILD === "true";
14+
import { DatadogLambda } from "datadog-cdk-constructs-v2/lib/datadog-lambda";
15+
import { PricingEventHandlers } from "./pricingEventHandlers";
1616

1717
// no-dd-sa:typescript-best-practices/no-unnecessary-class
1818
export class PricingApiStack extends cdk.Stack {
@@ -23,13 +23,27 @@ export class PricingApiStack extends cdk.Stack {
2323
const version = process.env["COMMIT_HASH"] ?? "latest";
2424

2525
// TODO: Replace this code block with the code from the workshop
26+
const datadogConfiguration = new DatadogLambda(this, "Datadog", {
27+
extensionLayerVersion: 96,
28+
nodeLayerVersion: 137,
29+
site: process.env.DD_SITE ?? "datadoghq.com",
30+
apiKey: process.env.DD_API_KEY,
31+
service,
32+
version,
33+
env,
34+
enableColdStartTracing: true,
35+
enableDatadogTracing: true,
36+
captureLambdaPayload: true,
37+
injectLogContext: true,
38+
});
39+
2640
const sharedProps: SharedProps = {
2741
team: "pricing",
2842
domain: "pricing",
2943
environment: env,
3044
serviceName: service,
3145
version,
32-
datadogConfiguration: undefined,
46+
datadogConfiguration: datadogConfiguration,
3347
};
3448

3549
const pricingServiceProps = new PricingServiceProps(this, sharedProps);
@@ -39,9 +53,9 @@ export class PricingApiStack extends cdk.Stack {
3953
jwtSecret: pricingServiceProps.getJwtSecret(),
4054
});
4155

42-
// const _ = new PricingEventHandlers(this, "PricingEventHandlers", {
43-
// serviceProps: pricingServiceProps,
44-
// });
56+
const _ = new PricingEventHandlers(this, "PricingEventHandlers", {
57+
serviceProps: pricingServiceProps,
58+
});
4559

4660
const _param = new StringParameter(this, "PricingAPIEndpoint", {
4761
parameterName: `/${sharedProps.environment}/${sharedProps.serviceName}/api-endpoint`,

src/pricing-service/tests/integration/end-to-end.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describe("integration-tests", () => {
6969

7070
// The base repo should error when calculating prices to test the
7171
// starting state of the workshop repository.
72-
expect([502]).toContain(generatePricingResult.status);
72+
expect([200]).toContain(generatePricingResult.status);
7373
});
7474

7575
function generateJwt(secretAccessKey: string): string {

src/product-search-service/poetry.lock

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/product-search-service/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ cdk-monitoring-constructs = "*"
4040
pytest = "*"
4141
pytest-mock = "*"
4242
pytest-timeout = "*"
43+
PyJWT = "*"
4344
pycodestyle = "*"
4445
pytest-cov = "*"
4546
pytest-html = "*"

src/product-search-service/tests/integration/api_driver.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
import os
55
import random
66
import string
7-
from datetime import datetime, timezone
7+
from datetime import datetime, timedelta, timezone
88
from typing import Any
99

1010
import boto3
11+
import jwt
1112
import requests
1213

1314

@@ -18,11 +19,12 @@ class ProductSearchApiDriver:
1819
matching the pattern used by the activity-service integration tests.
1920
"""
2021

21-
def __init__(self, search_api_endpoint: str, product_api_endpoint: str, event_bus_name: str) -> None:
22+
def __init__(self, search_api_endpoint: str, product_api_endpoint: str, event_bus_name: str, jwt_secret: str = "") -> None:
2223
self.search_api_endpoint = search_api_endpoint.rstrip("/")
2324
self.product_api_endpoint = product_api_endpoint.rstrip("/")
2425
self.event_bus_name = event_bus_name
2526
self.environment = os.environ.get("ENV", "dev")
27+
self._jwt_secret = jwt_secret
2628
self._events_client = boto3.client("events")
2729
self._products_client = requests.Session()
2830

@@ -52,20 +54,46 @@ def search_raw(self, query: str) -> requests.Response:
5254
# Product Management Service API (used to seed test products)
5355
# ------------------------------------------------------------------
5456

57+
def _generate_admin_token(self) -> str:
58+
"""Generate a short-lived ADMIN JWT signed with the shared secret.
59+
60+
The Product Management Service validates:
61+
- HS256 signing algorithm
62+
- `user_type` claim must equal "ADMIN"
63+
- Token must not be expired
64+
See: src/product-management-service/src/product-api/internal/adapters/authentication.go
65+
"""
66+
now = datetime.now(timezone.utc)
67+
payload = {
68+
"sub": "admin@serverless-sample.com",
69+
"user_type": "ADMIN",
70+
"iat": now,
71+
"exp": now + timedelta(hours=1),
72+
}
73+
return jwt.encode(payload, self._jwt_secret, algorithm="HS256")
74+
5575
def create_product(self, name: str, price: float) -> dict[str, Any]:
5676
"""Create a product via the Product Management Service API."""
77+
headers = {}
78+
if self._jwt_secret:
79+
headers["Authorization"] = f"Bearer {self._generate_admin_token()}"
5780
response = self._products_client.post(
5881
f"{self.product_api_endpoint}/product",
5982
json={"name": name, "price": price},
83+
headers=headers,
6084
timeout=10,
6185
)
6286
response.raise_for_status()
6387
return response.json() # type: ignore[no-any-return]
6488

6589
def delete_product(self, product_id: str) -> None:
6690
"""Delete a product via the Product Management Service API."""
91+
headers = {}
92+
if self._jwt_secret:
93+
headers["Authorization"] = f"Bearer {self._generate_admin_token()}"
6794
response = self._products_client.delete(
6895
f"{self.product_api_endpoint}/product/{product_id}",
96+
headers=headers,
6997
timeout=10,
7098
)
7199
# 404 on cleanup is acceptable
@@ -153,9 +181,10 @@ def initialize_driver() -> ProductSearchApiDriver:
153181
search_endpoint = os.environ.get("SEARCH_API_ENDPOINT")
154182
product_endpoint = os.environ.get("PRODUCT_API_ENDPOINT")
155183
event_bus_name = os.environ.get("EVENT_BUS_NAME")
184+
jwt_secret = os.environ.get("JWT_SECRET_KEY", "")
156185

157186
if search_endpoint and product_endpoint and event_bus_name:
158-
return ProductSearchApiDriver(search_endpoint, product_endpoint, event_bus_name)
187+
return ProductSearchApiDriver(search_endpoint, product_endpoint, event_bus_name, jwt_secret)
159188

160189
env = os.environ.get("ENV", "dev")
161190
ssm = boto3.client("ssm")
@@ -179,10 +208,17 @@ def initialize_driver() -> ProductSearchApiDriver:
179208
)["Parameter"]["Value"]
180209
except ssm.exceptions.ParameterNotFound:
181210
event_bus_name = "default"
211+
try:
212+
jwt_secret = ssm.get_parameter(
213+
Name=f"/{env}/shared/secret-access-key",
214+
WithDecryption=True,
215+
)["Parameter"]["Value"]
216+
except ssm.exceptions.ParameterNotFound:
217+
jwt_secret = ""
182218
else:
183219
# Product Management Service and shared event bus are not deployed in ephemeral envs.
184220
# Pipeline tests are skipped; smoke tests only need the search endpoint.
185221
product_endpoint = ""
186222
event_bus_name = "default"
187223

188-
return ProductSearchApiDriver(search_endpoint, product_endpoint, event_bus_name)
224+
return ProductSearchApiDriver(search_endpoint, product_endpoint, event_bus_name, jwt_secret)

0 commit comments

Comments
 (0)