Skip to content

Commit 16e0dbc

Browse files
feat(chat): first version (not completed)
* feat(chat): done some basics APIs for chat * feat(messages): first trial * feat(notifications): setup websocket connection * feat(quotes): full websocket authenticated connection setup + follow notification * feat(notifications): some updates * feat(notifications): reply and like 'add' notifications * feat(chat): first version (not completed yet) --------- Co-authored-by: Mohamed Bahgat <mbahgat503@gmail.com>
1 parent 3d1f9ff commit 16e0dbc

File tree

77 files changed

+3482
-1316
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+3482
-1316
lines changed

package-lock.json

Lines changed: 354 additions & 123 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@
3838
"@google/genai": "^1.28.0",
3939
"@nestjs-modules/ioredis": "^2.0.2",
4040
"@nestjs-modules/mailer": "^2.0.2",
41-
"@nestjs/bull": "^11.0.4",
4241
"@nestjs/azure-storage": "^4.0.0",
42+
"@nestjs/bull": "^11.0.4",
4343
"@nestjs/common": "^11.0.1",
4444
"@nestjs/config": "^4.0.2",
4545
"@nestjs/core": "^11.0.1",
@@ -48,10 +48,13 @@
4848
"@nestjs/mongoose": "^11.0.3",
4949
"@nestjs/passport": "^11.0.5",
5050
"@nestjs/platform-express": "^11.0.1",
51+
"@nestjs/platform-socket.io": "^11.1.9",
5152
"@nestjs/swagger": "^11.2.0",
5253
"@nestjs/typeorm": "^11.0.0",
54+
"@nestjs/websockets": "^11.1.9",
5355
"@types/bull": "^3.15.9",
5456
"@types/multer": "^2.0.0",
57+
"@types/socket.io": "^3.0.1",
5558
"amqplib": "^0.10.9",
5659
"axios": "^1.12.1",
5760
"bcrypt": "^6.0.0",
@@ -73,6 +76,7 @@
7376
"pg": "^8.16.3",
7477
"reflect-metadata": "^0.2.2",
7578
"rxjs": "^7.8.1",
79+
"socket.io": "^4.8.1",
7680
"swagger-ui-express": "^5.0.1",
7781
"typeorm": "^0.3.26",
7882
"xlsx": "^0.18.5"

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ChatModule } from './chat/chat.module';
1818
import { CategoryModule } from './category/category.module';
1919
import { BackgroundJobsModule } from './background-jobs/background-jobs.module';
2020
import { AzureStorageModule } from './azure-storage/azure-storage.module';
21+
import { MessagesModule } from './messages/messages.module';
2122

2223
@Module({
2324
imports: [
@@ -42,6 +43,7 @@ import { AzureStorageModule } from './azure-storage/azure-storage.module';
4243
CategoryModule,
4344
BackgroundJobsModule,
4445
AzureStorageModule,
46+
MessagesModule,
4547
],
4648
controllers: [AppController],
4749
providers: [AppService],

src/auth/auth.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import { GoogleStrategy } from './strategies/google.strategy';
2121
import { FacebookStrategy } from './strategies/facebook.strategy';
2222
import { UserRepository } from 'src/user/user.repository';
2323
import { BackgroundJobsModule } from 'src/background-jobs/background-jobs.module';
24-
import { BackgroundJobsService } from 'src/background-jobs/background-jobs.service';
2524
import { PaginationService } from 'src/shared/services/pagination/pagination.service';
25+
import { EmailJobsService } from 'src/background-jobs/email/email.service';
2626

2727
@Module({
2828
imports: [
@@ -53,7 +53,7 @@ import { PaginationService } from 'src/shared/services/pagination/pagination.ser
5353
RedisService,
5454
VerificationService,
5555
EmailService,
56-
BackgroundJobsService,
56+
EmailJobsService,
5757
CaptchaService,
5858
UsernameService,
5959
GoogleStrategy,

src/auth/auth.service.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { VerificationService } from 'src/verification/verification.service';
2020
import { EmailService } from 'src/communication/email.service';
2121
import { CaptchaService } from './captcha.service';
2222
import { ConfigService } from '@nestjs/config';
23-
import { BackgroundJobsService } from 'src/background-jobs/background-jobs.service';
23+
import { BackgroundJobsService } from 'src/background-jobs/background-jobs';
2424
import {
2525
BadRequestException,
2626
ConflictException,

src/auth/auth.service.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { UsernameService } from './username.service';
2020
import { LoginDTO } from './dto/login.dto';
2121
import { RedisService } from 'src/redis/redis.service';
2222
import { VerificationService } from 'src/verification/verification.service';
23-
import { BackgroundJobsService } from 'src/background-jobs/background-jobs.service';
23+
import { BackgroundJobsService } from 'src/background-jobs/background-jobs';
2424
import { GitHubUserDto } from './dto/github-user.dto';
2525
import { CaptchaService } from './captcha.service';
2626
import * as crypto from 'crypto';
@@ -49,6 +49,7 @@ import { OAuth2Client } from 'google-auth-library';
4949
import axios from 'axios';
5050
import { UserRepository } from 'src/user/user.repository';
5151
import { ConfirmPasswordDto } from './dto/confirm-password.dto';
52+
import { EmailJobsService } from 'src/background-jobs/email/email.service';
5253

5354
@Injectable()
5455
export class AuthService {
@@ -58,7 +59,7 @@ export class AuthService {
5859
private readonly username_service: UsernameService,
5960
private readonly redis_service: RedisService,
6061
private readonly verification_service: VerificationService,
61-
private readonly background_jobs_service: BackgroundJobsService,
62+
private readonly background_jobs_service: EmailJobsService,
6263
private readonly captcha_service: CaptchaService,
6364
private readonly config_service: ConfigService
6465
) {}
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
3+
import { JwtService } from '@nestjs/jwt';
4+
import { ConfigService } from '@nestjs/config';
5+
import { WsJwtGuard } from './ws-jwt.guard';
6+
import { Socket } from 'socket.io';
7+
8+
describe('WsJwtGuard', () => {
9+
let guard: WsJwtGuard;
10+
let jwt_service: JwtService;
11+
let config_service: ConfigService;
12+
13+
beforeEach(async () => {
14+
const module: TestingModule = await Test.createTestingModule({
15+
providers: [
16+
WsJwtGuard,
17+
{
18+
provide: JwtService,
19+
useValue: {
20+
verify: jest.fn(),
21+
},
22+
},
23+
{
24+
provide: ConfigService,
25+
useValue: {
26+
get: jest.fn().mockReturnValue('test-secret'),
27+
},
28+
},
29+
],
30+
}).compile();
31+
32+
guard = module.get<WsJwtGuard>(WsJwtGuard);
33+
jwt_service = module.get<JwtService>(JwtService);
34+
config_service = module.get<ConfigService>(ConfigService);
35+
});
36+
37+
afterEach(() => {
38+
jest.clearAllMocks();
39+
jest.restoreAllMocks();
40+
});
41+
42+
describe('canActivate', () => {
43+
it('should be defined', () => {
44+
expect(guard).toBeDefined();
45+
});
46+
47+
it('should return true for non-WebSocket contexts', () => {
48+
const context = {
49+
getType: jest.fn().mockReturnValue('http'),
50+
} as any as ExecutionContext;
51+
52+
const result = guard.canActivate(context);
53+
54+
expect(result).toBe(true);
55+
});
56+
57+
it('should validate token and attach user to client for WebSocket context', () => {
58+
const mock_user = { id: '123', email: 'test@example.com' };
59+
const mock_client = {
60+
handshake: {
61+
headers: {
62+
authorization: 'Bearer valid-token',
63+
},
64+
},
65+
data: {},
66+
} as any as Socket;
67+
68+
const context = {
69+
getType: jest.fn().mockReturnValue('ws'),
70+
switchToWs: jest.fn().mockReturnValue({
71+
getClient: jest.fn().mockReturnValue(mock_client),
72+
}),
73+
} as any as ExecutionContext;
74+
75+
jest.spyOn(jwt_service, 'verify').mockReturnValue(mock_user);
76+
77+
const result = guard.canActivate(context);
78+
79+
expect(result).toBe(true);
80+
expect(mock_client.data.user).toEqual(mock_user);
81+
expect(jwt_service.verify).toHaveBeenCalledWith('valid-token', {
82+
secret: 'test-secret',
83+
});
84+
});
85+
86+
it('should return false when validateToken returns null', () => {
87+
const mock_client = {
88+
handshake: {
89+
headers: {
90+
authorization: 'Bearer invalid-token',
91+
},
92+
},
93+
data: {},
94+
} as any as Socket;
95+
96+
const context = {
97+
getType: jest.fn().mockReturnValue('ws'),
98+
switchToWs: jest.fn().mockReturnValue({
99+
getClient: jest.fn().mockReturnValue(mock_client),
100+
}),
101+
} as any as ExecutionContext;
102+
103+
jest.spyOn(WsJwtGuard, 'validateToken').mockReturnValue(null);
104+
105+
const result = guard.canActivate(context);
106+
107+
expect(result).toBe(false);
108+
});
109+
});
110+
111+
describe('validateToken', () => {
112+
it('should validate and return user payload for valid token', () => {
113+
const mock_user = { id: '123', email: 'test@example.com' };
114+
const mock_client = {
115+
handshake: {
116+
headers: {
117+
authorization: 'Bearer valid-token',
118+
},
119+
},
120+
} as any as Socket;
121+
122+
(jwt_service.verify as jest.Mock).mockReturnValue(mock_user);
123+
124+
const result = WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
125+
126+
expect(result).toEqual(mock_user);
127+
expect(jwt_service.verify).toHaveBeenCalledWith('valid-token', {
128+
secret: 'test-secret',
129+
});
130+
expect(config_service.get).toHaveBeenCalledWith('JWT_TOKEN_SECRET');
131+
});
132+
133+
it('should throw UnauthorizedException when authorization header is missing', () => {
134+
const mock_client = {
135+
handshake: {
136+
headers: {},
137+
},
138+
} as any as Socket;
139+
140+
expect(() => {
141+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
142+
}).toThrow(UnauthorizedException);
143+
144+
expect(() => {
145+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
146+
}).toThrow('No authorization header');
147+
});
148+
149+
it('should throw UnauthorizedException when authorization header is undefined', () => {
150+
const mock_client = {
151+
handshake: {
152+
headers: {
153+
authorization: undefined,
154+
},
155+
},
156+
} as any as Socket;
157+
158+
expect(() => {
159+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
160+
}).toThrow(UnauthorizedException);
161+
162+
expect(() => {
163+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
164+
}).toThrow('No authorization header');
165+
});
166+
167+
it('should throw UnauthorizedException when token is missing', () => {
168+
const mock_client = {
169+
handshake: {
170+
headers: {
171+
authorization: 'Bearer',
172+
},
173+
},
174+
} as any as Socket;
175+
176+
expect(() => {
177+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
178+
}).toThrow(UnauthorizedException);
179+
180+
expect(() => {
181+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
182+
}).toThrow('Invalid authorization format');
183+
});
184+
185+
it('should throw UnauthorizedException when token after Bearer is empty', () => {
186+
const mock_client = {
187+
handshake: {
188+
headers: {
189+
authorization: 'Bearer ',
190+
},
191+
},
192+
} as any as Socket;
193+
194+
expect(() => {
195+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
196+
}).toThrow(UnauthorizedException);
197+
198+
expect(() => {
199+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
200+
}).toThrow('Invalid authorization format');
201+
});
202+
203+
it('should throw UnauthorizedException when authorization format is invalid', () => {
204+
const mock_client = {
205+
handshake: {
206+
headers: {
207+
authorization: 'InvalidFormat',
208+
},
209+
},
210+
} as any as Socket;
211+
212+
expect(() => {
213+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
214+
}).toThrow(UnauthorizedException);
215+
216+
expect(() => {
217+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
218+
}).toThrow('Invalid authorization format');
219+
});
220+
221+
it('should throw UnauthorizedException when JWT verification fails', () => {
222+
const mock_client = {
223+
handshake: {
224+
headers: {
225+
authorization: 'Bearer invalid-token',
226+
},
227+
},
228+
} as any as Socket;
229+
230+
(jwt_service.verify as jest.Mock).mockImplementation(() => {
231+
throw new Error('Invalid token');
232+
});
233+
234+
expect(() => {
235+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
236+
}).toThrow(UnauthorizedException);
237+
238+
expect(() => {
239+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
240+
}).toThrow('Invalid token');
241+
});
242+
243+
it('should throw UnauthorizedException when JWT is expired', () => {
244+
const mock_client = {
245+
handshake: {
246+
headers: {
247+
authorization: 'Bearer expired-token',
248+
},
249+
},
250+
} as any as Socket;
251+
252+
(jwt_service.verify as jest.Mock).mockImplementation(() => {
253+
throw new Error('jwt expired');
254+
});
255+
256+
expect(() => {
257+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
258+
}).toThrow(UnauthorizedException);
259+
});
260+
261+
it('should handle malformed JWT tokens', () => {
262+
const mock_client = {
263+
handshake: {
264+
headers: {
265+
authorization: 'Bearer malformed.token',
266+
},
267+
},
268+
} as any as Socket;
269+
270+
(jwt_service.verify as jest.Mock).mockImplementation(() => {
271+
throw new Error('jwt malformed');
272+
});
273+
274+
expect(() => {
275+
WsJwtGuard.validateToken(mock_client, jwt_service, config_service);
276+
}).toThrow(UnauthorizedException);
277+
});
278+
});
279+
});

0 commit comments

Comments
 (0)