Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,13 @@ services:

# External service configuration
CATALOGUE_BASE_URL: http://sticker-catalogue:8080

# OAuth/JWT configuration
# OAUTH_ISSUER: The issuer claim expected in JWT tokens (matches what user-management sets)
# Note: OpenIddict adds trailing slash to issuer, so we must include it here
# OAUTH_JWKS_URL: Internal Docker network URL to fetch JWKS for token validation
OAUTH_ISSUER: ${DEPLOYMENT_HOST_URL}/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we look into OpenIddict config to try and fix it? Or just live with it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think it's really broken, i think we just need to be actively consistent. This seems to be the easier way around - i stuffed about trying to get dotnet to not append /, but ultimately I don't care which way it goes, as long as they are all the same!

OAUTH_JWKS_URL: http://user-management:8080/.well-known/jwks
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 1s
Expand Down
77 changes: 77 additions & 0 deletions e2e/tests/api/auth-required.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { test, expect } from '@playwright/test';

test.describe('API Authentication Requirements', () => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests to validate

test.describe('Sticker Catalogue - Protected Endpoints', () => {
test('POST /api/stickers/v1/ requires authentication', async ({ request }) => {
const response = await request.post('/api/stickers/v1/', {
data: {
stickerName: 'Test Sticker',
stickerDescription: 'A test sticker',
},
});
expect(response.status()).toBe(401);
});

test('PUT /api/stickers/v1/{id} requires authentication', async ({ request }) => {
const response = await request.put('/api/stickers/v1/sticker-001', {
data: {
stickerName: 'Updated Sticker',
},
});
expect(response.status()).toBe(401);
});

test('DELETE /api/stickers/v1/{id} requires authentication', async ({ request }) => {
const response = await request.delete('/api/stickers/v1/sticker-001');
expect(response.status()).toBe(401);
});

test('POST /api/stickers/v1/{id}/image requires authentication', async ({ request }) => {
const response = await request.post('/api/stickers/v1/sticker-001/image', {
headers: {
'Content-Type': 'image/png',
},
data: Buffer.from('fake-image-data'),
});
expect(response.status()).toBe(401);
});
});

test.describe('Sticker Catalogue - Public Endpoints', () => {
test('GET /api/stickers/v1/ does not require authentication', async ({ request }) => {
const response = await request.get('/api/stickers/v1/');
expect(response.ok()).toBeTruthy();
});

test('GET /api/stickers/v1/{id} does not require authentication', async ({ request }) => {
const response = await request.get('/api/stickers/v1/sticker-001');
expect(response.ok()).toBeTruthy();
});

test('GET /api/stickers/v1/{id}/image does not require authentication', async ({ request }) => {
const response = await request.get('/api/stickers/v1/sticker-001/image');
expect(response.ok()).toBeTruthy();
});
});

test.describe('Sticker Award - Protected Endpoints', () => {
test('POST /api/awards/v1/assignments/{userId} requires authentication', async ({ request }) => {
const response = await request.post('/api/awards/v1/assignments/user-001', {
data: {
stickerId: 'sticker-001',
},
});
expect(response.status()).toBe(401);
});

test('DELETE /api/awards/v1/assignments/{userId}/{stickerId} requires authentication', async ({ request }) => {
const response = await request.delete('/api/awards/v1/assignments/user-001/sticker-001');
expect(response.status()).toBe(401);
});
});

test('GET /api/awards/v1/assignments/{userId} requires authentication', async ({ request }) => {
const response = await request.get('/api/awards/v1/assignments/user-001');
expect(response.status()).toBe(401);
});
});
12 changes: 9 additions & 3 deletions e2e/tests/api/endpoints.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ test.describe('API Endpoints', () => {
});

test('supports pagination parameters', async ({ request }) => {
const response = await request.get('/api/stickers/v1?page=0&size=5');
const response = await request.get('/api/stickers/v1/?page=0&size=5');
expect(response.ok()).toBeTruthy();

const data = await response.json();
Expand All @@ -43,7 +43,7 @@ test.describe('API Endpoints', () => {

test('returns individual sticker by ID', async ({ request }) => {
// First get a valid ID
const listResponse = await request.get('/api/stickers/v1');
const listResponse = await request.get('/api/stickers/v1/');
const listData = await listResponse.json();
const stickerId = listData.stickers[0]?.stickerId;

Expand All @@ -58,7 +58,13 @@ test.describe('API Endpoints', () => {
}
});

test('returns 404 for non-existent sticker', async ({ request }) => {
// SKIPPED: CloudFront's distribution-level errorResponses intercepts API 404s and returns
// the SPA's index.html with status 200 instead. This is because errorResponses cannot be
// scoped per-behavior/origin - they apply to all origins including the API Gateway.
// The fix requires using a CloudFront Function on the S3 behavior to handle SPA routing
// instead of relying on errorResponses.
// See: https://github.com/DataDog/stickerlandia/issues/173
test.skip('returns 404 for non-existent sticker', async ({ request }) => {
const response = await request.get('/api/stickers/v1/non-existent-id-xyz');
expect(response.status()).toBe(404);
});
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/authenticated/catalogue.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ test.describe('Sticker Detail Page', () => {

test('is accessible via direct URL with valid ID', async ({ page, request }) => {
// Get a valid sticker ID from API
const apiResponse = await request.get('/api/stickers/v1');
const apiResponse = await request.get('/api/stickers/v1/');
const data = await apiResponse.json();
const stickerId = data.stickers[0]?.stickerId;

Expand Down
5 changes: 3 additions & 2 deletions scripts/test-services.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ echo -e "Compose file: $COMPOSE_FILE"
echo ""

# Test configuration: URL, expected HTTP code, service name, description
# Note: Use health/public endpoints only - protected endpoints require authentication
declare -a TESTS=(
"http://localhost:8081/dashboard/,200,traefik,Traefik Dashboard"
"http://localhost:8082,200,redpanda-console,Redpanda Console"
"http://localhost:8080/api/stickers/v1/sticker-001,200,sticker-catalogue,Sticker Catalogue API"
"http://localhost:8080/api/awards/v1/assignments/user-001,200,sticker-award,Sticker Award API"
"http://localhost:8080/api/stickers/v1/,200,sticker-catalogue,Sticker Catalogue API"
"http://localhost:8080/api/awards/v1/health,200,sticker-award,Sticker Award Health"
"http://localhost:8080/api/users/v1/health,200,user-management,User Management Health"
)

Expand Down
15 changes: 14 additions & 1 deletion sticker-award/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
dd_logrus "github.com/DataDog/dd-trace-go/contrib/sirupsen/logrus/v2"
"github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
"github.com/DataDog/dd-trace-go/v2/profiler"
"github.com/datadog/stickerlandia/sticker-award/internal/api/middleware"
"github.com/datadog/stickerlandia/sticker-award/internal/api/router"
"github.com/datadog/stickerlandia/sticker-award/internal/clients/catalogue"
"github.com/datadog/stickerlandia/sticker-award/internal/config"
Expand Down Expand Up @@ -132,9 +133,21 @@ func main() {
assignmentService := service.NewAssigner(assignmentRepo, catalogueClient, validator.New(), producer)
log.Info("Assignment service initialized")

// Initialize JWT validator for authentication
log.WithFields(log.Fields{
"jwksUrl": cfg.Auth.JWKSUrl,
"issuer": cfg.Auth.Issuer,
}).Info("Initializing JWT validator...")
jwtValidator, err := middleware.NewJWTValidator(&cfg.Auth)
if err != nil {
log.WithFields(log.Fields{"error": err}).Fatal("Failed to initialize JWT validator")
}
defer jwtValidator.Close()
log.Info("JWT validator initialized")

// Initialize HTTP router
log.Info("Setting up HTTP router...")
r := router.Setup(db, cfg, assignmentService)
r := router.Setup(db, cfg, assignmentService, jwtValidator)
log.Info("HTTP router configured")

// Register our **user registered** event handler. This responds to new users appearing
Expand Down
14 changes: 3 additions & 11 deletions sticker-award/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ require (
github.com/DataDog/dd-trace-go/contrib/sirupsen/logrus/v2 v2.2.2
github.com/DataDog/dd-trace-go/v2 v2.2.2
github.com/IBM/sarama v1.45.2
github.com/aws/aws-cdk-go/awscdk/v2 v2.232.2
github.com/MicahParks/keyfunc/v3 v3.3.5
github.com/aws/aws-sdk-go-v2 v1.39.5
github.com/aws/aws-sdk-go-v2/config v1.31.16
github.com/aws/aws-sdk-go-v2/service/eventbridge v1.45.9
github.com/aws/aws-sdk-go-v2/service/sqs v1.42.12
github.com/aws/constructs-go/constructs/v10 v10.4.4
github.com/aws/jsii-runtime-go v1.121.0
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.15.5
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/golang-migrate/migrate/v4 v4.16.2
github.com/google/uuid v1.6.0
github.com/sirupsen/logrus v1.9.3
Expand Down Expand Up @@ -52,6 +51,7 @@ require (
github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.27.0 // indirect
github.com/DataDog/sketches-go v1.4.7 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/MicahParks/jwkset v0.5.19 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.12 // indirect
Expand All @@ -67,9 +67,6 @@ require (
github.com/aws/smithy-go v1.23.1 // indirect
github.com/bytedance/sonic v1.12.0 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cdklabs/awscdk-asset-awscli-go/awscliv1/v2 v2.2.242 // indirect
github.com/cdklabs/awscdk-asset-node-proxy-agent-go/nodeproxyagentv6/v2 v2.1.0 // indirect
github.com/cdklabs/cloud-assembly-schema-go/awscdkcloudassemblyschema/v48 v48.20.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect
Expand All @@ -91,7 +88,6 @@ require (
github.com/eapache/queue v1.1.0 // indirect
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
Expand Down Expand Up @@ -131,7 +127,6 @@ require (
github.com/lib/pq v1.10.9 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
Expand Down Expand Up @@ -172,7 +167,6 @@ require (
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/yuin/goldmark v1.4.13 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/collector/component v1.31.0 // indirect
Expand All @@ -193,14 +187,12 @@ require (
golang.org/x/arch v0.4.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.36.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
Expand Down
Loading
Loading