Skip to content

Commit 69accc3

Browse files
Merge remote-tracking branch 'origin/main' into feat/analytics-csv-export
2 parents 8465feb + 7a3e719 commit 69accc3

16 files changed

Lines changed: 884 additions & 446 deletions

File tree

CODE_OF_CONDUCT.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Code of Conduct
2+
3+
## Our Pledge
4+
5+
We are committed to creating a welcoming, respectful, and inclusive environment for everyone participating in this project.
6+
7+
## Expected Behavior
8+
9+
- Be respectful and supportive
10+
- Use inclusive and professional language
11+
- Accept constructive feedback gracefully
12+
- Collaborate positively with others
13+
14+
## Unacceptable Behavior
15+
16+
- Harassment or discrimination
17+
- Offensive or abusive language
18+
- Personal attacks or trolling
19+
- Sharing private information without permission
20+
21+
## Responsibilities
22+
23+
Maintainers are responsible for enforcing this Code of Conduct and ensuring a safe environment for all contributors.
24+
25+
## Enforcement
26+
27+
Violations may result in warnings, temporary restrictions, or permanent removal from the community depending on the severity of the behavior.
28+
29+
## Final Note
30+
31+
By participating in this project, you agree to follow this Code of Conduct and help maintain a positive community.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import Fastify from 'fastify';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import { followRoutes } from '../routes/follow.js';
5+
6+
vi.mock('../utils/encryption.js', () => ({
7+
decrypt: vi.fn(() => 'fake-access-token'),
8+
}));
9+
10+
describe('POST /api/follow/:platform/:targetUsername', () => {
11+
it('returns 400 when API follow is not supported for the platform', async () => {
12+
const app = Fastify({ logger: false });
13+
14+
const findUnique = vi.fn().mockResolvedValue({
15+
id: 'token-1',
16+
userId: 'user-1',
17+
platform: 'unknown',
18+
accessToken: 'encrypted-token',
19+
});
20+
21+
app.decorate('prisma', {
22+
oAuthToken: {
23+
findUnique,
24+
},
25+
followLog: {
26+
create: vi.fn(),
27+
},
28+
}as any);
29+
30+
app.decorate('authenticate', async (request: any) => {
31+
request.user = { id: 'user-1' };
32+
});
33+
34+
await app.register(followRoutes, { prefix: '/api/follow' });
35+
await app.ready();
36+
37+
const response = await app.inject({
38+
method: 'POST',
39+
url: '/api/follow/unknown/targetUser',
40+
});
41+
42+
const body = response.json();
43+
44+
expect(response.statusCode).toBe(400);
45+
expect(body.error).toContain('API follow not supported');
46+
expect(findUnique).toHaveBeenCalledWith({
47+
where: {
48+
userId_platform: {
49+
userId: 'user-1',
50+
platform: 'unknown',
51+
},
52+
},
53+
});
54+
55+
await app.close();
56+
});
57+
});
Lines changed: 97 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,103 @@
11
import { describe, it, expect, beforeEach, vi } from 'vitest';
2+
import Fastify from 'fastify';
3+
import { profileRoutes } from '../routes/profiles.js';
24

3-
// Mock test for duplicate username check
4-
// Note: This test verifies the expected behavior of the /api/profiles/me PUT endpoint
5-
// when attempting to change username to one that's already taken.
6-
//
7-
// The actual implementation in profiles.ts (lines 54-63) already handles this correctly:
8-
// - Checks for existing username with different user ID
9-
// - Returns 409 status with { error: "Username already taken" }
10-
//
11-
// Concurrency note: The current implementation uses a simple findFirst query.
12-
// For production, consider adding a timestamp/version field to handle race conditions
13-
// where two users might try to claim the same username simultaneously.
14-
15-
describe('PUT /api/profiles/me - Duplicate Username', () => {
16-
// This test would require setting up the full Fastify app with test database
17-
// For now, documenting the expected behavior based on profiles.ts implementation
18-
19-
it('should return 409 with error "Username already taken" when username exists', async () => {
20-
// Expected behavior (from profiles.ts lines 54-63):
21-
// const existing = await app.prisma.user.findFirst({
22-
// where: {
23-
// username: parsed.data.username,
24-
// NOT: { id: userId },
25-
// },
26-
// });
27-
// if (existing) {
28-
// return reply.status(409).send({ error: 'Username already taken' });
29-
// }
30-
31-
// Expected response:
32-
expect(true).toBe(true); // Placeholder - actual test needs full app setup
5+
const mockUser = {
6+
id: 'user-123',
7+
email: 'test@example.com',
8+
username: 'testuser',
9+
displayName: 'Test User',
10+
bio: null,
11+
pronouns: null,
12+
role: null,
13+
company: null,
14+
avatarUrl: null,
15+
accentColor: '#ffffff',
16+
platformLinks: [],
17+
cards: [],
18+
provider: 'github',
19+
providerId: 'gh-123',
20+
};
21+
22+
const mockPrisma = {
23+
user: {
24+
findUnique: vi.fn(),
25+
findFirst: vi.fn(),
26+
update: vi.fn(),
27+
},
28+
};
29+
30+
async function buildApp() {
31+
const app = Fastify();
32+
app.decorate('prisma', mockPrisma);
33+
app.decorate('authenticate', async (request: any) => {
34+
request.user = { id: 'user-123' };
35+
});
36+
app.register(profileRoutes, { prefix: '/api/profiles' });
37+
await app.ready();
38+
return app;
39+
}
40+
41+
describe('GET /api/profiles/me', () => {
42+
beforeEach(() => vi.clearAllMocks());
43+
44+
it('should return user profile with displayName', async () => {
45+
mockPrisma.user.findUnique.mockResolvedValue(mockUser);
46+
const app = await buildApp();
47+
const res = await app.inject({ method: 'GET', url: '/api/profiles/me' });
48+
expect(res.statusCode).toBe(200);
49+
const body = res.json();
50+
expect(body.displayName).toBe('Test User');
51+
expect(body.email).toBe('test@example.com');
52+
expect(body.provider).toBeUndefined();
53+
expect(body.providerId).toBeUndefined();
54+
});
55+
56+
it('should return 404 if user not found', async () => {
57+
mockPrisma.user.findUnique.mockResolvedValue(null);
58+
const app = await buildApp();
59+
const res = await app.inject({ method: 'GET', url: '/api/profiles/me' });
60+
expect(res.statusCode).toBe(404);
61+
expect(res.json().error).toBe('User not found');
62+
});
63+
});
64+
65+
describe('PUT /api/profiles/me', () => {
66+
beforeEach(() => vi.clearAllMocks());
67+
68+
it('should update profile and return updated data', async () => {
69+
mockPrisma.user.findFirst.mockResolvedValue(null);
70+
mockPrisma.user.update.mockResolvedValue({ ...mockUser, displayName: 'Updated Name' });
71+
const app = await buildApp();
72+
const res = await app.inject({
73+
method: 'PUT',
74+
url: '/api/profiles/me',
75+
payload: { displayName: 'Updated Name' },
76+
});
77+
expect(res.statusCode).toBe(200);
78+
expect(res.json().displayName).toBe('Updated Name');
79+
});
80+
81+
it('should return 400 for invalid accentColor', async () => {
82+
const app = await buildApp();
83+
const res = await app.inject({
84+
method: 'PUT',
85+
url: '/api/profiles/me',
86+
payload: { accentColor: 'notacolor' },
87+
});
88+
expect(res.statusCode).toBe(400);
89+
expect(res.json().error).toBe('Validation failed');
3390
});
3491

35-
it('should allow username change when username is available', async () => {
36-
// Expected: 200 OK with updated profile
37-
expect(true).toBe(true);
92+
it('should return 409 if username is already taken', async () => {
93+
mockPrisma.user.findFirst.mockResolvedValue({ id: 'other-user' });
94+
const app = await buildApp();
95+
const res = await app.inject({
96+
method: 'PUT',
97+
url: '/api/profiles/me',
98+
payload: { username: 'takenuser' },
99+
});
100+
expect(res.statusCode).toBe(409);
101+
expect(res.json().error).toBe('Username already taken');
38102
});
39103
});

apps/backend/src/app.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,22 @@ export async function buildApp() {
3737
credentials: true,
3838
});
3939

40-
await app.register(helmet, { contentSecurityPolicy: false });
40+
await app.register(helmet, {
41+
contentSecurityPolicy: {
42+
directives: {
43+
defaultSrc: ["'self'"],
44+
baseUri: ["'self'"],
45+
fontSrc: ["'self'", 'https:', 'data:', 'https://fonts.gstatic.com'],
46+
frameAncestors: ["'self'"],
47+
imgSrc: ["'self'", 'data:', 'https:'],
48+
objectSrc: ["'none'"],
49+
scriptSrc: ["'self'"],
50+
scriptSrcAttr: ["'none'"],
51+
styleSrc: ["'self'", 'https:', "'unsafe-inline'", 'https://fonts.googleapis.com'],
52+
upgradeInsecureRequests: [],
53+
},
54+
},
55+
});
4156

4257
await app.register(jwt, {
4358
secret: process.env.JWT_SECRET || 'dev-secret-change-me',

0 commit comments

Comments
 (0)