Skip to content

implement stronger input validation for tip metadata.#292

Merged
OlufunbiIK merged 3 commits intoOlufunbiIK:mainfrom
ohamamarachi474-del:#258
Mar 26, 2026
Merged

implement stronger input validation for tip metadata.#292
OlufunbiIK merged 3 commits intoOlufunbiIK:mainfrom
ohamamarachi474-del:#258

Conversation

@ohamamarachi474-del
Copy link
Contributor

@ohamamarachi474-del ohamamarachi474-del commented Mar 26, 2026

closes #258

Summary by CodeRabbit

  • New Features
    • Tips can now include optional metadata fields (source, campaign, platform, OS, version) for enhanced tracking and attribution.
    • WebSocket notifications now include sequence tracking for improved delivery reliability.
    • Clients automatically recover missed tip notifications when reconnecting, ensuring no notifications are lost during disconnections.

@vercel
Copy link

vercel bot commented Mar 26, 2026

@ohamamarachi474-del is attempting to deploy a commit to the olufunbiik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

The PR adds metadata validation and persistence to tip creation via new DTO fields with constraints, introduces an in-memory event store for sequencing notifications, and enables client catch-up by replaying missed WebSocket events. The WebSocketModule is registered in the application's module graph, and integration tests verify end-to-end notification delivery and replay behavior.

Changes

Cohort / File(s) Summary
Module Registration
backend/src/app.module.ts, backend/src/websocket/websocket.module.ts
WebSocketModule imported into AppModule to enable gateway initialization; EventStoreService added to WebSocketModule providers and exports.
Metadata Validation
backend/src/tips/create-tips.dto.ts
New TipMetadataDto with optional string fields (source, campaign, platform, os, version) constrained by @IsString() and @MaxLength(). CreateTipDto updated to include optional metadata?: TipMetadataDto with nested validation via @ValidateNested() and @Type().
Metadata Persistence
backend/src/tips/tips.service.ts
TipsService.create now destructures and persists metadata field, stringifying it to the Tip.metadata column or storing null if absent.
Event Sequencing & Storage
backend/src/websocket/event-store.service.ts
New EventStoreService maintains in-memory event log with incrementing sequenceId, enforces 1000-event retention window via sliding purge, and provides replay query via getEventsAfter() filtered by room membership.
WebSocket Gateway Enhancement
backend/src/websocket/websocket.gateway.ts
Gateway now listens for tip.verified events and stores notifications in EventStore with explicit sequenceId. handleConnection now emits connected.latestSequenceId. Room join handlers accept optional lastSequenceId and replay missed events. New handleAck handler logs message receipt. TipNotificationData updated with top-level sequenceId field.
Unit & Integration Tests
backend/test/tips-validation.unit.spec.ts, backend/test/websocket-delivery.spec.ts
Unit test validates TipMetadataDto constraints via ValidationPipe.transform(). Integration test boots full AppModule, triggers tip verification events via EventEmitter2, verifies notification delivery, and asserts client catch-up correctly replays only room-scoped missed events via lastSequenceId parameter.

Sequence Diagram

sequenceDiagram
    participant Client as Client (WebSocket)
    participant Gateway as WebSocket Gateway
    participant EventStore as Event Store
    participant TipsService as Tips Service
    participant EventBus as Event Bus

    Note over Client,EventBus: Tip Verification & Initial Notification

    Client->>Gateway: join_artist_room {artistId, lastSequenceId?}
    Gateway->>EventStore: getLatestSequenceId()
    Gateway->>Client: connected.latestSequenceId
    
    alt Client provides lastSequenceId
        Gateway->>EventStore: getEventsAfter(lastSequenceId, [artistId])
        EventStore-->>Gateway: missed events
        Gateway->>Client: tip_notification (replayed events)
    end

    TipsService->>EventBus: emit tip.verified event
    EventBus->>Gateway: `@OnEvent`('tip.verified')
    Gateway->>Gateway: sendTipNotification()
    
    rect rgba(100, 150, 200, 0.5)
        Gateway->>EventStore: storeEvent('tip_received', payload, rooms)
        EventStore-->>Gateway: StoredEvent {sequenceId, ...}
    end
    
    Gateway->>Client: tip_notification {type, sequenceId, data}
    Client->>Gateway: ack {sequenceId}
    Gateway-->>Gateway: log acknowledgement

    Note over Client,EventStore: Client Reconnects for Catch-up
    
    Client->>Gateway: join_artist_room {artistId, lastSequenceId: 5}
    Gateway->>EventStore: getEventsAfter(5, [artistId])
    EventStore-->>Gateway: events with sequenceId > 5
    Gateway->>Client: tip_notification (missed event with sequenceId 6)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 With sequenceId and rooms so fine,
Event Store remembers each tip in line,
Clients catch up when they reconnect,
No missed notifications—perfectly checked! 🎵✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Changes include WebSocket event-sourcing features (EventStoreService, event sequencing, message replay) that extend significantly beyond the linked issue #258's scope of 'tighten DTO schemas and sanitize nested metadata fields' for tips endpoints. Review whether event-sourcing infrastructure (EventStoreService, sequence IDs, replay mechanism) should be part of this PR or separated into a dedicated issue for architectural clarity and focused review scope.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'implement stronger input validation for tip metadata' directly reflects the main objective of the PR—adding stricter validation for tip metadata through DTO constraints and validation logic.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #258: DTO schemas are tightened with TipMetadataDto and validation decorators, nested metadata validation is enforced via @ValidateNested/@type, and a comprehensive test matrix demonstrates validation failures for invalid/oversized metadata.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/src/tips/create-tips.dto.ts`:
- Around line 6-35: TipMetadataDto's nested fields (source, campaign, platform,
os, version) are not sanitized before persistence; add the same sanitization
used for message by applying the `@SanitiseAsPlainText`() decorator to each of
these properties in TipMetadataDto so they are cleaned during validation, or
alternatively ensure tips.service.ts serializes only after passing them through
the existing sanitiser function used for message (the code path around the
serialization call in tips.service.ts where metadata is JSON.stringified).
Reference TipMetadataDto and the serialization point in tips.service.ts and
apply the sanitiser consistently to prevent HTML/control characters from
reaching storage.

In `@backend/src/websocket/event-store.service.ts`:
- Line 16: The code silently returns a truncated tail when a client-supplied
sequenceId precedes the oldest retained event (maxWindowSize), so update the
logic in the event-store code (e.g., inside getEventsAfter and the
reconnection/replay path that consumes sequenceId) to detect stale cursors:
compare the requested sequenceId against the earliest retained sequence (compute
from current store head and maxWindowSize) and, when the request is older,
return an explicit error/indicator (or special response) instead of returning a
partial backlog; ensure the error includes the requested sequenceId and the
earliest available sequence to allow the client to decide whether to request a
full snapshot or resync.

In `@backend/src/websocket/websocket.gateway.ts`:
- Around line 167-179: The notification payload in websocket.gateway.ts
currently sets asset: tip.asset which is undefined for newly verified tips
because tips.service.ts populates assetCode; update the payload construction
(the payload object near where tip is read) to include the correct asset
identifier by using tip.assetCode (or set assetCode: tip.assetCode and/or
replace asset with assetCode) so clients receive the non-XLM asset code instead
of undefined.
- Around line 190-198: Currently the gateway emits 'tip_notification' twice when
a socket is in both rooms by calling this.server.to(artistRoom).emit(...) and
again this.server.to(trackRoom).emit(...); change this to emit once to the union
of rooms by chaining room targets on the same server broadcast (use
this.server.to(artistRoom).to(trackRoom).emit(...) when trackRoom exists) so
clients in both rooms receive a single event; also collapse the two logger calls
into one single log after the emit (include storedEvent.sequenceId and the list
of target rooms like artistRoom and trackRoom) and keep the fallback to emit
only to artistRoom when trackRoom is falsy.

In `@backend/test/websocket-delivery.spec.ts`:
- Around line 17-19: The test currently boots the full AppModule via
Test.createTestingModule({ imports: [AppModule] }), which pulls real
Redis/Postgres wiring and makes the suite fragile; instead build a narrow test
module that only includes the gateway/event-store pieces or stub the external
providers: replace imports: [AppModule] with a Test.createTestingModule that
imports the specific gateway module(s) (e.g., the websocket gateway or
EventStore module) or registers the gateway provider(s) directly, and override
external providers like the Redis client provider (e.g., RedisService or the
token used in your app) and the DB providers (DataSource/TypeOrm or
getRepositoryToken(...) repositories) via .overrideProvider(...).useValue(mock)
so the test uses lightweight mocks rather than real Redis/Postgres.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1178aef2-f324-47db-a480-6d9f9d023fe2

📥 Commits

Reviewing files that changed from the base of the PR and between c6c6510 and eaf08f4.

📒 Files selected for processing (8)
  • backend/src/app.module.ts
  • backend/src/tips/create-tips.dto.ts
  • backend/src/tips/tips.service.ts
  • backend/src/websocket/event-store.service.ts
  • backend/src/websocket/websocket.gateway.ts
  • backend/src/websocket/websocket.module.ts
  • backend/test/tips-validation.unit.spec.ts
  • backend/test/websocket-delivery.spec.ts

Comment on lines +6 to +35
export class TipMetadataDto {
@ApiPropertyOptional({ description: 'Source of the tip', example: 'mobile_app' })
@IsOptional()
@IsString()
@MaxLength(50)
source?: string;

@ApiPropertyOptional({ description: 'Campaign ID', example: 'summer2023' })
@IsOptional()
@IsString()
@MaxLength(100)
campaign?: string;

@ApiPropertyOptional({ description: 'Platform (web/ios/android)', example: 'android' })
@IsOptional()
@IsString()
@MaxLength(20)
platform?: string;

@ApiPropertyOptional({ description: 'Operating System', example: 'Android 13' })
@IsOptional()
@IsString()
@MaxLength(50)
os?: string;

@ApiPropertyOptional({ description: 'Client App Version', example: '2.1.0' })
@IsOptional()
@IsString()
@MaxLength(20)
version?: string;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sanitize nested metadata before persisting it.

These fields are length/type checked, but unlike message they never run through @SanitiseAsPlainText(). Line 182 of backend/src/tips/tips.service.ts then serializes them directly, so HTML/control characters still reach storage.

Suggested fix
   `@IsOptional`()
   `@IsString`()
+  `@SanitiseAsPlainText`()
   `@MaxLength`(50)
   source?: string;
@@
   `@IsOptional`()
   `@IsString`()
+  `@SanitiseAsPlainText`()
   `@MaxLength`(100)
   campaign?: string;
@@
   `@IsOptional`()
   `@IsString`()
+  `@SanitiseAsPlainText`()
   `@MaxLength`(20)
   platform?: string;
@@
   `@IsOptional`()
   `@IsString`()
+  `@SanitiseAsPlainText`()
   `@MaxLength`(50)
   os?: string;
@@
   `@IsOptional`()
   `@IsString`()
+  `@SanitiseAsPlainText`()
   `@MaxLength`(20)
   version?: string;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export class TipMetadataDto {
@ApiPropertyOptional({ description: 'Source of the tip', example: 'mobile_app' })
@IsOptional()
@IsString()
@MaxLength(50)
source?: string;
@ApiPropertyOptional({ description: 'Campaign ID', example: 'summer2023' })
@IsOptional()
@IsString()
@MaxLength(100)
campaign?: string;
@ApiPropertyOptional({ description: 'Platform (web/ios/android)', example: 'android' })
@IsOptional()
@IsString()
@MaxLength(20)
platform?: string;
@ApiPropertyOptional({ description: 'Operating System', example: 'Android 13' })
@IsOptional()
@IsString()
@MaxLength(50)
os?: string;
@ApiPropertyOptional({ description: 'Client App Version', example: '2.1.0' })
@IsOptional()
@IsString()
@MaxLength(20)
version?: string;
export class TipMetadataDto {
`@ApiPropertyOptional`({ description: 'Source of the tip', example: 'mobile_app' })
`@IsOptional`()
`@IsString`()
`@SanitiseAsPlainText`()
`@MaxLength`(50)
source?: string;
`@ApiPropertyOptional`({ description: 'Campaign ID', example: 'summer2023' })
`@IsOptional`()
`@IsString`()
`@SanitiseAsPlainText`()
`@MaxLength`(100)
campaign?: string;
`@ApiPropertyOptional`({ description: 'Platform (web/ios/android)', example: 'android' })
`@IsOptional`()
`@IsString`()
`@SanitiseAsPlainText`()
`@MaxLength`(20)
platform?: string;
`@ApiPropertyOptional`({ description: 'Operating System', example: 'Android 13' })
`@IsOptional`()
`@IsString`()
`@SanitiseAsPlainText`()
`@MaxLength`(50)
os?: string;
`@ApiPropertyOptional`({ description: 'Client App Version', example: '2.1.0' })
`@IsOptional`()
`@IsString`()
`@SanitiseAsPlainText`()
`@MaxLength`(20)
version?: string;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/tips/create-tips.dto.ts` around lines 6 - 35, TipMetadataDto's
nested fields (source, campaign, platform, os, version) are not sanitized before
persistence; add the same sanitization used for message by applying the
`@SanitiseAsPlainText`() decorator to each of these properties in TipMetadataDto
so they are cleaned during validation, or alternatively ensure tips.service.ts
serializes only after passing them through the existing sanitiser function used
for message (the code path around the serialization call in tips.service.ts
where metadata is JSON.stringified). Reference TipMetadataDto and the
serialization point in tips.service.ts and apply the sanitiser consistently to
prevent HTML/control characters from reaching storage.

private readonly logger = new Logger(EventStoreService.name);
private events: StoredEvent[] = [];
private nextSequenceId = 1;
private readonly maxWindowSize = 1000; // Store up to 1000 recent events
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Detect stale cursors instead of silently replaying a partial backlog.

Once the 1000-event window rolls over, older events are evicted. If a client reconnects with a sequenceId older than the first retained event, getEventsAfter() returns only the tail and gives no signal that earlier events were lost. Clients will think replay completed successfully when it did not.

Also applies to: 33-35, 44-50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/websocket/event-store.service.ts` at line 16, The code silently
returns a truncated tail when a client-supplied sequenceId precedes the oldest
retained event (maxWindowSize), so update the logic in the event-store code
(e.g., inside getEventsAfter and the reconnection/replay path that consumes
sequenceId) to detect stale cursors: compare the requested sequenceId against
the earliest retained sequence (compute from current store head and
maxWindowSize) and, when the request is older, return an explicit
error/indicator (or special response) instead of returning a partial backlog;
ensure the error includes the requested sequenceId and the earliest available
sequence to allow the client to decide whether to request a full snapshot or
resync.

Comment on lines +167 to +179
const payload = {
tipId: tip.id,
artistId: tip.artistId,
trackId: tip.trackId,
amount: tip.amount,
asset: tip.asset,
message: tip.message,
senderAddress: tip.isAnonymous ? undefined : tip.senderAddress,
isAnonymous: tip.isAnonymous,
createdAt: tip.createdAt,
artist: tip.artist,
track: tip.track,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Send assetCode in the notification payload.

Lines 152-177 of backend/src/tips/tips.service.ts populate assetCode, not asset, so this emits asset: undefined for newly verified tips. Non-XLM tips will be mislabeled by clients that fall back to XLM.

Suggested fix
-        asset: tip.asset,
+        asset: tip.assetCode ?? tip.asset,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/websocket/websocket.gateway.ts` around lines 167 - 179, The
notification payload in websocket.gateway.ts currently sets asset: tip.asset
which is undefined for newly verified tips because tips.service.ts populates
assetCode; update the payload construction (the payload object near where tip is
read) to include the correct asset identifier by using tip.assetCode (or set
assetCode: tip.assetCode and/or replace asset with assetCode) so clients receive
the non-XLM asset code instead of undefined.

Comment on lines 190 to 198
// Send to artist room
const artistRoom = `artist_${tip.artistId}`;
this.server.to(artistRoom).emit('tip_notification', notificationData);
this.logger.log(`Sent tip notification to room: ${artistRoom}`);
this.logger.log(`Sent tip notification ${storedEvent.sequenceId} to room: ${artistRoom}`);

// Send to track room if track is specified
if (tip.trackId) {
const trackRoom = `track_${tip.trackId}`;
if (trackRoom) {
this.server.to(trackRoom).emit('tip_notification', notificationData);
this.logger.log(`Sent tip notification to room: ${trackRoom}`);
this.logger.log(`Sent tip notification ${storedEvent.sequenceId} to room: ${trackRoom}`);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "websocket.gateway.ts"

Repository: OlufunbiIK/tip-tune

Length of output: 107


🏁 Script executed:

wc -l ./backend/src/websocket/websocket.gateway.ts

Repository: OlufunbiIK/tip-tune

Length of output: 111


🏁 Script executed:

sed -n '150,220p' ./backend/src/websocket/websocket.gateway.ts

Repository: OlufunbiIK/tip-tune

Length of output: 2678


🏁 Script executed:

grep -n "socket.join\|client.join" ./backend/src/websocket/websocket.gateway.ts

Repository: OlufunbiIK/tip-tune

Length of output: 115


🏁 Script executed:

sed -n '75,125p' ./backend/src/websocket/websocket.gateway.ts

Repository: OlufunbiIK/tip-tune

Length of output: 1651


🏁 Script executed:

head -30 ./backend/src/websocket/websocket.gateway.ts

Repository: OlufunbiIK/tip-tune

Length of output: 879


🏁 Script executed:

cat > /tmp/test_socketio.js << 'EOF'
// Testing Socket.io room behavior
// Documenting Socket.io API

// According to Socket.io documentation:
// server.to(room1).to(room2).emit() - emits to the INTERSECTION (rooms that have both)
// To emit to UNION, you need to emit separately or use different approach

console.log("Socket.io room chaining behavior:");
console.log("server.to(room1).emit() - emits to all sockets in room1");
console.log("server.to(room1).to(room2).emit() - emits only to sockets in BOTH room1 AND room2 (intersection)");
console.log("");
console.log("If a socket is in artist_1 and track_5:");
console.log("- Current code: emits to artist_1, then emits to track_5 -> socket receives twice");
console.log("- Suggested fix with chaining: would only emit to sockets in BOTH rooms");
console.log("");
console.log("To solve the duplicate without losing messages:");
console.log("- Option 1: Track socket IDs and emit once per socket");
console.log("- Option 2: Use namespace or user-based approach");
console.log("- Option 3: Emit to union and deduplicate at client");
EOF
cat /tmp/test_socketio.js

Repository: OlufunbiIK/tip-tune

Length of output: 1110


🌐 Web query:

socket.io server.to(room1).to(room2).emit behavior intersection union

💡 Result:

The behavior of server.to(room1).to(room2).emit(...) in Socket.IO is a union of the rooms, not an intersection. Every socket that is in at least one of the specified rooms receives the event exactly once, even if it belongs to multiple rooms. This is explicitly stated in the official Socket.IO documentation across multiple versions: - "In that case, a union is performed: every socket that is at least in one of the rooms will get the event once (even if the socket is in two or more rooms)." (Rooms docs v3 and v4) - Emit cheatsheet example: io.to("room1").to(["room2", "room3"]).except("room4").emit("hello"); describes targeting clients "in at least one of the rooms". The to method chains by accumulating room names in the socket's _rooms array, and the broadcast targets the union via the adapter. There is no intersection behavior; except is used for exclusion instead.

Citations:


🏁 Script executed:

sed -n '188,220p' ./backend/src/websocket/websocket.gateway.ts

Repository: OlufunbiIK/tip-tune

Length of output: 1415


Broadcast once to the room union.

A socket can be in both artist_* and track_* rooms. The current code emits separately to each room, sending duplicate tip_notification events to clients subscribed to both. Use Socket.io's room chaining to emit to the union of rooms in a single broadcast:

Suggested fix
-      this.server.to(artistRoom).emit('tip_notification', notificationData);
-      this.logger.log(`Sent tip notification ${storedEvent.sequenceId} to room: ${artistRoom}`);
-
-      // Send to track room if track is specified
-      if (trackRoom) {
-        this.server.to(trackRoom).emit('tip_notification', notificationData);
-        this.logger.log(`Sent tip notification ${storedEvent.sequenceId} to room: ${trackRoom}`);
-      }
+      if (trackRoom) {
+        this.server
+          .to(artistRoom)
+          .to(trackRoom)
+          .emit('tip_notification', notificationData);
+        this.logger.log(
+          `Sent tip notification ${storedEvent.sequenceId} to rooms: ${artistRoom}, ${trackRoom}`,
+        );
+      } else {
+        this.server.to(artistRoom).emit('tip_notification', notificationData);
+        this.logger.log(`Sent tip notification ${storedEvent.sequenceId} to room: ${artistRoom}`);
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Send to artist room
const artistRoom = `artist_${tip.artistId}`;
this.server.to(artistRoom).emit('tip_notification', notificationData);
this.logger.log(`Sent tip notification to room: ${artistRoom}`);
this.logger.log(`Sent tip notification ${storedEvent.sequenceId} to room: ${artistRoom}`);
// Send to track room if track is specified
if (tip.trackId) {
const trackRoom = `track_${tip.trackId}`;
if (trackRoom) {
this.server.to(trackRoom).emit('tip_notification', notificationData);
this.logger.log(`Sent tip notification to room: ${trackRoom}`);
this.logger.log(`Sent tip notification ${storedEvent.sequenceId} to room: ${trackRoom}`);
}
if (trackRoom) {
this.server
.to(artistRoom)
.to(trackRoom)
.emit('tip_notification', notificationData);
this.logger.log(
`Sent tip notification ${storedEvent.sequenceId} to rooms: ${artistRoom}, ${trackRoom}`,
);
} else {
this.server.to(artistRoom).emit('tip_notification', notificationData);
this.logger.log(`Sent tip notification ${storedEvent.sequenceId} to room: ${artistRoom}`);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/websocket/websocket.gateway.ts` around lines 190 - 198, Currently
the gateway emits 'tip_notification' twice when a socket is in both rooms by
calling this.server.to(artistRoom).emit(...) and again
this.server.to(trackRoom).emit(...); change this to emit once to the union of
rooms by chaining room targets on the same server broadcast (use
this.server.to(artistRoom).to(trackRoom).emit(...) when trackRoom exists) so
clients in both rooms receive a single event; also collapse the two logger calls
into one single log after the emit (include storedEvent.sequenceId and the list
of target rooms like artistRoom and trackRoom) and keep the fallback to emit
only to artistRoom when trackRoom is falsy.

Comment on lines +17 to +19
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid booting the full AppModule here.

This pulls in the real Redis and Postgres wiring from Lines 48-83 of backend/src/app.module.ts. The suite will fail whenever that infra is unavailable, which turns a websocket regression test into an environment smoke test. Build a narrow test module for the gateway/event store, or override those external providers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/test/websocket-delivery.spec.ts` around lines 17 - 19, The test
currently boots the full AppModule via Test.createTestingModule({ imports:
[AppModule] }), which pulls real Redis/Postgres wiring and makes the suite
fragile; instead build a narrow test module that only includes the
gateway/event-store pieces or stub the external providers: replace imports:
[AppModule] with a Test.createTestingModule that imports the specific gateway
module(s) (e.g., the websocket gateway or EventStore module) or registers the
gateway provider(s) directly, and override external providers like the Redis
client provider (e.g., RedisService or the token used in your app) and the DB
providers (DataSource/TypeOrm or getRepositoryToken(...) repositories) via
.overrideProvider(...).useValue(mock) so the test uses lightweight mocks rather
than real Redis/Postgres.

@OlufunbiIK OlufunbiIK merged commit ca9b77c into OlufunbiIK:main Mar 26, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stronger Input Validation for Tip Metadata

2 participants