diff --git a/backend/test/app.e2e-spec.ts b/backend/test/app.e2e-spec.ts index 4df6580..7a26b71 100644 --- a/backend/test/app.e2e-spec.ts +++ b/backend/test/app.e2e-spec.ts @@ -1,25 +1,24 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; -import { App } from 'supertest/types'; -import { AppModule } from './../src/app.module'; +import { createTestApp } from './test-utils'; describe('AppController (e2e)', () => { - let app: INestApplication; + let app: INestApplication; - beforeEach(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); + beforeAll(async () => { + app = await createTestApp(); }); - it('/ (GET)', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect('Hello World!'); + afterAll(async () => { + await app.close(); + }); + + describe('GET /api', () => { + it('should return "Hello World!"', () => { + return request(app.getHttpServer()) + .get('/api') + .expect(200) + .expect('Hello World!'); + }); }); }); diff --git a/backend/test/auth.e2e-spec.ts b/backend/test/auth.e2e-spec.ts new file mode 100644 index 0000000..8262dbd --- /dev/null +++ b/backend/test/auth.e2e-spec.ts @@ -0,0 +1,96 @@ +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { createTestApp, createTestUser, generateTokensForUser, cleanupTestData } from './test-utils'; +import { v4 as uuidv4 } from 'uuid'; + +describe('AuthController (e2e)', () => { + let app: INestApplication; + let accessToken: string; + let refreshToken: string; + let testUser: any; + let testUserId: string; + + beforeAll(async () => { + app = await createTestApp(); + + // Create a test user and generate tokens + testUser = await createTestUser(app); + testUserId = testUser.id; + const tokens = await generateTokensForUser(app, testUserId); + accessToken = tokens.accessToken; + refreshToken = tokens.refreshToken; + }); + + afterAll(async () => { + // Clean up test data + await cleanupTestData(app, testUserId); + await app.close(); + }); + + describe('GET /api/auth/profile', () => { + it('should return the current user profile when authenticated', () => { + return request(app.getHttpServer()) + .get('/api/auth/profile') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', testUserId); + expect(res.body.name).toBe(testUser.name); + expect(res.body.githubId).toBe(testUser.githubId); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .get('/api/auth/profile') + .expect(401); + }); + + it('should return 401 with invalid token', () => { + return request(app.getHttpServer()) + .get('/api/auth/profile') + .set('Authorization', 'Bearer invalid-token') + .expect(401); + }); + }); + + describe('POST /api/auth/refresh', () => { + it('should refresh tokens with valid refresh token', () => { + return request(app.getHttpServer()) + .post('/api/auth/refresh') + .set('Authorization', `Bearer ${refreshToken}`) + .expect(201) + .expect((res) => { + expect(res.body).toHaveProperty('accessToken'); + expect(res.body).toHaveProperty('refreshToken'); + expect(typeof res.body.accessToken).toBe('string'); + expect(typeof res.body.refreshToken).toBe('string'); + + // Update tokens for subsequent tests + accessToken = res.body.accessToken; + refreshToken = res.body.refreshToken; + }); + }); + + it('should return 401 with invalid refresh token', () => { + return request(app.getHttpServer()) + .post('/api/auth/refresh') + .set('Authorization', 'Bearer invalid-token') + .expect(401); + }); + }); + + // Note: We can't easily test the GitHub OAuth flow in an e2e test + // as it requires interaction with the GitHub API + describe('GET /api/auth/github', () => { + it('should redirect to GitHub OAuth page', () => { + return request(app.getHttpServer()) + .get('/api/auth/github') + .expect(302) // Expect a redirect + .expect((res) => { + expect(res.headers.location).toBeDefined(); + expect(res.headers.location.startsWith('https://github.com/login/oauth')).toBe(true); + }); + }); + }); +}); \ No newline at end of file diff --git a/backend/test/groups.e2e-spec.ts b/backend/test/groups.e2e-spec.ts new file mode 100644 index 0000000..da42037 --- /dev/null +++ b/backend/test/groups.e2e-spec.ts @@ -0,0 +1,249 @@ +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { createTestApp, createTestUser, generateTokensForUser, cleanupTestData } from './test-utils'; +import { v4 as uuidv4 } from 'uuid'; + +describe('GroupsController (e2e)', () => { + let app: INestApplication; + let accessToken: string; + let testUser: any; + let testUserId: string; + let testGroupId: string; + let testProjectId: string; + let testPersonId: string; + + beforeAll(async () => { + app = await createTestApp(); + + // Create a test user and generate tokens + testUser = await createTestUser(app); + testUserId = testUser.id; + const tokens = await generateTokensForUser(app, testUserId); + accessToken = tokens.accessToken; + + // Create a test project + const projectResponse = await request(app.getHttpServer()) + .post('/api/projects') + .set('Authorization', `Bearer ${accessToken}`) + .send({ + name: `Test Project ${uuidv4().substring(0, 8)}`, + description: 'Test project for e2e tests', + ownerId: testUserId + }); + testProjectId = projectResponse.body.id; + + // Create a test person + const personResponse = await request(app.getHttpServer()) + .post('/api/persons') + .set('Authorization', `Bearer ${accessToken}`) + .send({ + name: `Test Person ${uuidv4().substring(0, 8)}`, + projectId: testProjectId, + skills: ['JavaScript', 'TypeScript'], + metadata: { email: 'testperson@example.com' } + }); + testPersonId = personResponse.body.id; + }); + + afterAll(async () => { + // Clean up test data + if (testGroupId) { + await request(app.getHttpServer()) + .delete(`/api/groups/${testGroupId}`) + .set('Authorization', `Bearer ${accessToken}`); + } + + if (testPersonId) { + await request(app.getHttpServer()) + .delete(`/api/persons/${testPersonId}`) + .set('Authorization', `Bearer ${accessToken}`); + } + + if (testProjectId) { + await request(app.getHttpServer()) + .delete(`/api/projects/${testProjectId}`) + .set('Authorization', `Bearer ${accessToken}`); + } + + await cleanupTestData(app, testUserId); + await app.close(); + }); + + describe('POST /api/groups', () => { + it('should create a new group', async () => { + const createGroupDto = { + name: `Test Group ${uuidv4().substring(0, 8)}`, + projectId: testProjectId, + description: 'Test group for e2e tests' + }; + + const response = await request(app.getHttpServer()) + .post('/api/groups') + .set('Authorization', `Bearer ${accessToken}`) + .send(createGroupDto) + .expect(201); + + expect(response.body).toHaveProperty('id'); + expect(response.body.name).toBe(createGroupDto.name); + expect(response.body.projectId).toBe(createGroupDto.projectId); + + testGroupId = response.body.id; + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .post('/api/groups') + .send({ + name: 'Unauthorized Group', + projectId: testProjectId + }) + .expect(401); + }); + }); + + describe('GET /api/groups', () => { + it('should return all groups', () => { + return request(app.getHttpServer()) + .get('/api/groups') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); + expect(res.body.some(group => group.id === testGroupId)).toBe(true); + }); + }); + + it('should filter groups by project ID', () => { + return request(app.getHttpServer()) + .get(`/api/groups?projectId=${testProjectId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); + expect(res.body.every(group => group.projectId === testProjectId)).toBe(true); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .get('/api/groups') + .expect(401); + }); + }); + + describe('GET /api/groups/:id', () => { + it('should return a group by ID', () => { + return request(app.getHttpServer()) + .get(`/api/groups/${testGroupId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', testGroupId); + expect(res.body).toHaveProperty('projectId', testProjectId); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .get(`/api/groups/${testGroupId}`) + .expect(401); + }); + + it('should return 404 for non-existent group', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .get(`/api/groups/${nonExistentId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + }); + + describe('PUT /api/groups/:id', () => { + it('should update a group', () => { + const updateData = { + name: `Updated Group ${uuidv4().substring(0, 8)}`, + description: 'Updated description' + }; + + return request(app.getHttpServer()) + .put(`/api/groups/${testGroupId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(updateData) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', testGroupId); + expect(res.body.name).toBe(updateData.name); + expect(res.body.description).toBe(updateData.description); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .put(`/api/groups/${testGroupId}`) + .send({ name: 'Unauthorized Update' }) + .expect(401); + }); + }); + + describe('POST /api/groups/:id/persons/:personId', () => { + it('should add a person to a group', () => { + return request(app.getHttpServer()) + .post(`/api/groups/${testGroupId}/persons/${testPersonId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(201) + .expect((res) => { + expect(res.body).toHaveProperty('id', testGroupId); + expect(res.body.persons).toContainEqual(expect.objectContaining({ id: testPersonId })); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .post(`/api/groups/${testGroupId}/persons/${testPersonId}`) + .expect(401); + }); + }); + + describe('GET /api/groups/:id/persons', () => { + it('should get all persons in a group', () => { + return request(app.getHttpServer()) + .get(`/api/groups/${testGroupId}/persons`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); + expect(res.body.some(person => person.id === testPersonId)).toBe(true); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .get(`/api/groups/${testGroupId}/persons`) + .expect(401); + }); + }); + + describe('DELETE /api/groups/:id/persons/:personId', () => { + it('should remove a person from a group', () => { + return request(app.getHttpServer()) + .delete(`/api/groups/${testGroupId}/persons/${testPersonId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', testGroupId); + expect(res.body.persons.every(person => person.id !== testPersonId)).toBe(true); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .delete(`/api/groups/${testGroupId}/persons/${testPersonId}`) + .expect(401); + }); + }); + + // Note: We're not testing the DELETE /api/groups/:id endpoint here to avoid complications with test cleanup +}); \ No newline at end of file diff --git a/backend/test/projects.e2e-spec.ts b/backend/test/projects.e2e-spec.ts new file mode 100644 index 0000000..b570a67 --- /dev/null +++ b/backend/test/projects.e2e-spec.ts @@ -0,0 +1,302 @@ +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { createTestApp, createTestUser, generateTokensForUser, cleanupTestData } from './test-utils'; +import { CreateProjectDto } from '../src/modules/projects/dto/create-project.dto'; +import { UpdateProjectDto } from '../src/modules/projects/dto/update-project.dto'; +import { v4 as uuidv4 } from 'uuid'; + +describe('ProjectsController (e2e)', () => { + let app: INestApplication; + let accessToken: string; + let testUser: any; + let testUserId: string; + let secondTestUser: any; + let secondTestUserId: string; + let testProject: any; + let testProjectId: string; + + beforeAll(async () => { + app = await createTestApp(); + + // Create test users and generate tokens + testUser = await createTestUser(app); + testUserId = testUser.id; + const tokens = await generateTokensForUser(app, testUserId); + accessToken = tokens.accessToken; + + // Create a second test user for collaborator tests + secondTestUser = await createTestUser(app); + secondTestUserId = secondTestUser.id; + }); + + afterAll(async () => { + // Clean up test data + await cleanupTestData(app, testUserId); + await cleanupTestData(app, secondTestUserId); + await app.close(); + }); + + describe('POST /api/projects', () => { + it('should create a new project', async () => { + const createProjectDto: CreateProjectDto = { + name: `Test Project ${uuidv4().substring(0, 8)}`, + description: 'A test project for e2e testing', + ownerId: testUserId, + settings: { isPublic: true }, + }; + + const response = await request(app.getHttpServer()) + .post('/api/projects') + .set('Authorization', `Bearer ${accessToken}`) + .send(createProjectDto) + .expect(201); + + expect(response.body).toHaveProperty('id'); + expect(response.body.name).toBe(createProjectDto.name); + expect(response.body.description).toBe(createProjectDto.description); + expect(response.body.ownerId).toBe(createProjectDto.ownerId); + expect(response.body.settings).toEqual(createProjectDto.settings); + + // Save the project for later tests + testProject = response.body; + testProjectId = response.body.id; + }); + + it('should return 400 if required fields are missing', () => { + const invalidProjectDto = { + // Missing required name and ownerId + description: 'An invalid project', + }; + + return request(app.getHttpServer()) + .post('/api/projects') + .set('Authorization', `Bearer ${accessToken}`) + .send(invalidProjectDto) + .expect(400); + }); + }); + + describe('GET /api/projects', () => { + it('should return all projects', () => { + return request(app.getHttpServer()) + .get('/api/projects') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); + expect(res.body.some(project => project.id === testProjectId)).toBe(true); + }); + }); + + it('should filter projects by owner ID', () => { + return request(app.getHttpServer()) + .get(`/api/projects?ownerId=${testUserId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.every(project => project.ownerId === testUserId)).toBe(true); + expect(res.body.some(project => project.id === testProjectId)).toBe(true); + }); + }); + }); + + describe('GET /api/projects/:id', () => { + it('should return a project by ID', () => { + return request(app.getHttpServer()) + .get(`/api/projects/${testProjectId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', testProjectId); + expect(res.body.name).toBe(testProject.name); + expect(res.body.description).toBe(testProject.description); + expect(res.body.ownerId).toBe(testProject.ownerId); + }); + }); + + it('should return 404 if project not found', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .get(`/api/projects/${nonExistentId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + }); + + describe('PATCH /api/projects/:id', () => { + it('should update a project', () => { + const updateProjectDto: UpdateProjectDto = { + name: `Updated Project ${uuidv4().substring(0, 8)}`, + description: 'An updated test project', + settings: { isPublic: false }, + }; + + return request(app.getHttpServer()) + .patch(`/api/projects/${testProjectId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(updateProjectDto) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', testProjectId); + expect(res.body.name).toBe(updateProjectDto.name); + expect(res.body.description).toBe(updateProjectDto.description); + expect(res.body.settings).toEqual(updateProjectDto.settings); + + // Update the testProject object with the new values + testProject = res.body; + }); + }); + + it('should return 404 if project not found', () => { + const nonExistentId = uuidv4(); + const updateProjectDto: UpdateProjectDto = { + name: 'Updated Project', + }; + + return request(app.getHttpServer()) + .patch(`/api/projects/${nonExistentId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(updateProjectDto) + .expect(404); + }); + }); + + describe('GET /api/projects/:id/check-access/:userId', () => { + it('should return true if user is the owner', () => { + return request(app.getHttpServer()) + .get(`/api/projects/${testProjectId}/check-access/${testUserId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toBe(true); + }); + }); + + it('should return false if user has no access', () => { + return request(app.getHttpServer()) + .get(`/api/projects/${testProjectId}/check-access/${secondTestUserId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toBe(false); + }); + }); + }); + + describe('POST /api/projects/:id/collaborators/:userId', () => { + it('should add a collaborator to a project', () => { + return request(app.getHttpServer()) + .post(`/api/projects/${testProjectId}/collaborators/${secondTestUserId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(201) + .expect((res) => { + expect(res.body).toHaveProperty('projectId', testProjectId); + expect(res.body).toHaveProperty('userId', secondTestUserId); + }); + }); + + it('should return 404 if project not found', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .post(`/api/projects/${nonExistentId}/collaborators/${secondTestUserId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + + it('should return 404 if user not found', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .post(`/api/projects/${testProjectId}/collaborators/${nonExistentId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + }); + + describe('GET /api/projects/:id/collaborators', () => { + it('should return all collaborators for a project', () => { + return request(app.getHttpServer()) + .get(`/api/projects/${testProjectId}/collaborators`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.some(collab => collab.user.id === secondTestUserId)).toBe(true); + }); + }); + + it('should return 404 if project not found', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .get(`/api/projects/${nonExistentId}/collaborators`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + }); + + describe('GET /api/projects/:id/check-access/:userId', () => { + it('should return true if user is a collaborator', () => { + return request(app.getHttpServer()) + .get(`/api/projects/${testProjectId}/check-access/${secondTestUserId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toBe(true); + }); + }); + }); + + describe('DELETE /api/projects/:id/collaborators/:userId', () => { + it('should remove a collaborator from a project', () => { + return request(app.getHttpServer()) + .delete(`/api/projects/${testProjectId}/collaborators/${secondTestUserId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(204); + }); + + it('should return 404 if collaboration not found', () => { + return request(app.getHttpServer()) + .delete(`/api/projects/${testProjectId}/collaborators/${secondTestUserId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + }); + + // We'll test DELETE last to avoid affecting other tests + describe('DELETE /api/projects/:id', () => { + let projectToDeleteId: string; + + beforeAll(async () => { + // Create a project specifically for the delete test + const createProjectDto: CreateProjectDto = { + name: `Project to Delete ${uuidv4().substring(0, 8)}`, + description: 'A project that will be deleted', + ownerId: testUserId, + }; + + const response = await request(app.getHttpServer()) + .post('/api/projects') + .set('Authorization', `Bearer ${accessToken}`) + .send(createProjectDto) + .expect(201); + + projectToDeleteId = response.body.id; + }); + + it('should delete a project', () => { + return request(app.getHttpServer()) + .delete(`/api/projects/${projectToDeleteId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(204); + }); + + it('should return 404 if project not found', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .delete(`/api/projects/${nonExistentId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + }); +}); diff --git a/backend/test/users.e2e-spec.ts b/backend/test/users.e2e-spec.ts new file mode 100644 index 0000000..3230955 --- /dev/null +++ b/backend/test/users.e2e-spec.ts @@ -0,0 +1,144 @@ +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { createTestApp, createTestUser, generateTokensForUser, cleanupTestData } from './test-utils'; +import { v4 as uuidv4 } from 'uuid'; + +describe('UsersController (e2e)', () => { + let app: INestApplication; + let accessToken: string; + let testUser: any; + let testUserId: string; + + beforeAll(async () => { + app = await createTestApp(); + + // Create a test user and generate tokens + testUser = await createTestUser(app); + testUserId = testUser.id; + const tokens = await generateTokensForUser(app, testUserId); + accessToken = tokens.accessToken; + }); + + afterAll(async () => { + // Clean up test data + await cleanupTestData(app, testUserId); + await app.close(); + }); + + describe('GET /api/users', () => { + it('should return a list of users when authenticated', () => { + return request(app.getHttpServer()) + .get('/api/users') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBeGreaterThan(0); + expect(res.body.some(user => user.id === testUserId)).toBe(true); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .get('/api/users') + .expect(401); + }); + }); + + describe('GET /api/users/:id', () => { + it('should return a user by ID when authenticated', () => { + return request(app.getHttpServer()) + .get(`/api/users/${testUserId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', testUserId); + expect(res.body.name).toBe(testUser.name); + expect(res.body.githubId).toBe(testUser.githubId); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .get(`/api/users/${testUserId}`) + .expect(401); + }); + + it('should return 404 for non-existent user', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .get(`/api/users/${nonExistentId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + }); + + describe('PATCH /api/users/:id', () => { + it('should update a user when authenticated', () => { + const updateData = { + name: `Updated Test User ${uuidv4().substring(0, 8)}` + }; + + return request(app.getHttpServer()) + .patch(`/api/users/${testUserId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(updateData) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', testUserId); + expect(res.body.name).toBe(updateData.name); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .patch(`/api/users/${testUserId}`) + .send({ name: 'Updated Name' }) + .expect(401); + }); + }); + + describe('POST /api/users/:id/gdpr-consent', () => { + it('should update GDPR consent timestamp when authenticated', () => { + return request(app.getHttpServer()) + .post(`/api/users/${testUserId}/gdpr-consent`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', testUserId); + expect(res.body).toHaveProperty('gdprConsentDate'); + expect(new Date(res.body.gdprConsentDate).getTime()).toBeGreaterThan(0); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .post(`/api/users/${testUserId}/gdpr-consent`) + .expect(401); + }); + }); + + describe('GET /api/users/:id/export-data', () => { + it('should export user data when authenticated', () => { + return request(app.getHttpServer()) + .get(`/api/users/${testUserId}/export-data`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('user'); + expect(res.body.user).toHaveProperty('id', testUserId); + expect(res.body).toHaveProperty('projects'); + expect(res.body).toHaveProperty('groups'); + expect(res.body).toHaveProperty('persons'); + }); + }); + + it('should return 401 when not authenticated', () => { + return request(app.getHttpServer()) + .get(`/api/users/${testUserId}/export-data`) + .expect(401); + }); + }); + + // Note: We're not testing the DELETE endpoint to avoid complications with test user cleanup +}); \ No newline at end of file