diff --git a/backend/test/tags.e2e-spec.ts b/backend/test/tags.e2e-spec.ts new file mode 100644 index 0000000..0ab8bd6 --- /dev/null +++ b/backend/test/tags.e2e-spec.ts @@ -0,0 +1,416 @@ +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { createTestApp, createTestUser, generateTokensForUser, cleanupTestData } from './test-utils'; +import { v4 as uuidv4 } from 'uuid'; +import { DRIZZLE } from '../src/database/database.module'; +import * as schema from '../src/database/schema'; +import { eq, and } from 'drizzle-orm'; + +describe('TagsController (e2e)', () => { + let app: INestApplication; + let accessToken: string; + let testUser: any; + let testUserId: string; + let db: any; + + beforeAll(async () => { + app = await createTestApp(); + + // Get the DrizzleORM instance + db = app.get(DRIZZLE); + + // 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('Tag CRUD operations', () => { + let createdTag: any; + const testTagData = { + name: `Test Tag ${uuidv4().substring(0, 8)}`, + color: '#FF5733', + type: 'PERSON' + }; + + // Clean up any test tags after tests + afterAll(async () => { + if (createdTag?.id) { + try { + await db.delete(schema.tags).where(eq(schema.tags.id, createdTag.id)); + } catch (error) { + console.error('Failed to clean up test tag:', error.message); + } + } + }); + + it('should create a new tag', () => { + return request(app.getHttpServer()) + .post('/api/tags') + .set('Authorization', `Bearer ${accessToken}`) + .send(testTagData) + .expect(201) + .expect((res) => { + expect(res.body).toHaveProperty('id'); + expect(res.body.name).toBe(testTagData.name); + expect(res.body.color).toBe(testTagData.color); + expect(res.body.type).toBe(testTagData.type); + createdTag = res.body; + }); + }); + + it('should get all tags', () => { + return request(app.getHttpServer()) + .get('/api/tags') + .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(tag => tag.id === createdTag.id)).toBe(true); + }); + }); + + it('should get tags by type', () => { + return request(app.getHttpServer()) + .get('/api/tags?type=PERSON') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.every(tag => tag.type === 'PERSON')).toBe(true); + expect(res.body.some(tag => tag.id === createdTag.id)).toBe(true); + }); + }); + + it('should get a tag by ID', () => { + return request(app.getHttpServer()) + .get(`/api/tags/${createdTag.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', createdTag.id); + expect(res.body.name).toBe(createdTag.name); + expect(res.body.color).toBe(createdTag.color); + expect(res.body.type).toBe(createdTag.type); + }); + }); + + it('should update a tag', () => { + const updateData = { + name: `Updated Tag ${uuidv4().substring(0, 8)}`, + color: '#33FF57' + }; + + return request(app.getHttpServer()) + .put(`/api/tags/${createdTag.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .send(updateData) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', createdTag.id); + expect(res.body.name).toBe(updateData.name); + expect(res.body.color).toBe(updateData.color); + expect(res.body.type).toBe(createdTag.type); // Type should remain unchanged + + // Update the createdTag reference for subsequent tests + createdTag = res.body; + }); + }); + + it('should return 404 when getting a non-existent tag', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .get(`/api/tags/${nonExistentId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + + it('should return 404 when updating a non-existent tag', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .put(`/api/tags/${nonExistentId}`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ name: 'Updated Tag' }) + .expect(404); + }); + }); + + describe('Tag relations with persons', () => { + let personTag: any; + let testPerson: any; + + beforeAll(async () => { + // Create a test tag for persons + const [tag] = await db + .insert(schema.tags) + .values({ + name: `Person Tag ${uuidv4().substring(0, 8)}`, + color: '#3366FF', + type: 'PERSON' + }) + .returning(); + personTag = tag; + + // Create a test project first (needed for person) + const [project] = await db + .insert(schema.projects) + .values({ + name: `Test Project ${uuidv4().substring(0, 8)}`, + description: 'A test project for e2e tests', + ownerId: testUserId + }) + .returning(); + + // Create a test person + const [person] = await db + .insert(schema.persons) + .values({ + firstName: `Test ${uuidv4().substring(0, 8)}`, + lastName: `Person ${uuidv4().substring(0, 8)}`, + gender: 'MALE', + technicalLevel: 3, + hasTechnicalTraining: true, + frenchSpeakingLevel: 4, + oralEaseLevel: 'COMFORTABLE', + projectId: project.id + }) + .returning(); + testPerson = person; + }); + + afterAll(async () => { + // Clean up test data + if (personTag?.id) { + try { + await db.delete(schema.tags).where(eq(schema.tags.id, personTag.id)); + } catch (error) { + console.error('Failed to clean up test tag:', error.message); + } + } + + if (testPerson?.id) { + try { + await db.delete(schema.persons).where(eq(schema.persons.id, testPerson.id)); + } catch (error) { + console.error('Failed to clean up test person:', error.message); + } + } + }); + + it('should add a tag to a person', () => { + return request(app.getHttpServer()) + .post(`/api/tags/persons/${testPerson.id}/tags/${personTag.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(201) + .expect((res) => { + expect(res.body).toHaveProperty('personId', testPerson.id); + expect(res.body).toHaveProperty('tagId', personTag.id); + }); + }); + + it('should get all tags for a person', () => { + return request(app.getHttpServer()) + .get(`/api/tags/persons/${testPerson.id}/tags`) + .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(item => item.tag.id === personTag.id)).toBe(true); + }); + }); + + it('should remove a tag from a person', () => { + return request(app.getHttpServer()) + .delete(`/api/tags/persons/${testPerson.id}/tags/${personTag.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('personId', testPerson.id); + expect(res.body).toHaveProperty('tagId', personTag.id); + }); + }); + + it('should return 404 when adding a tag to a non-existent person', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .post(`/api/tags/persons/${nonExistentId}/tags/${personTag.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + + it('should return 400 when adding a project tag to a person', async () => { + // Create a project tag + const [projectTag] = await db + .insert(schema.tags) + .values({ + name: `Project Tag ${uuidv4().substring(0, 8)}`, + color: '#FF3366', + type: 'PROJECT' + }) + .returning(); + + const response = await request(app.getHttpServer()) + .post(`/api/tags/persons/${testPerson.id}/tags/${projectTag.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(400); + + // Clean up the project tag + await db.delete(schema.tags).where(eq(schema.tags.id, projectTag.id)); + }); + }); + + describe('Tag relations with projects', () => { + let projectTag: any; + let testProject: any; + + beforeAll(async () => { + // Create a test tag for projects + const [tag] = await db + .insert(schema.tags) + .values({ + name: `Project Tag ${uuidv4().substring(0, 8)}`, + color: '#33FFCC', + type: 'PROJECT' + }) + .returning(); + projectTag = tag; + + // Create a test project + const [project] = await db + .insert(schema.projects) + .values({ + name: `Test Project ${uuidv4().substring(0, 8)}`, + description: 'A test project for e2e tests', + ownerId: testUserId + }) + .returning(); + testProject = project; + }); + + afterAll(async () => { + // Clean up test data + if (projectTag?.id) { + try { + await db.delete(schema.tags).where(eq(schema.tags.id, projectTag.id)); + } catch (error) { + console.error('Failed to clean up test tag:', error.message); + } + } + + if (testProject?.id) { + try { + await db.delete(schema.projects).where(eq(schema.projects.id, testProject.id)); + } catch (error) { + console.error('Failed to clean up test project:', error.message); + } + } + }); + + it('should add a tag to a project', () => { + return request(app.getHttpServer()) + .post(`/api/tags/projects/${testProject.id}/tags/${projectTag.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(201) + .expect((res) => { + expect(res.body).toHaveProperty('projectId', testProject.id); + expect(res.body).toHaveProperty('tagId', projectTag.id); + }); + }); + + it('should get all tags for a project', () => { + return request(app.getHttpServer()) + .get(`/api/tags/projects/${testProject.id}/tags`) + .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(item => item.tag.id === projectTag.id)).toBe(true); + }); + }); + + it('should remove a tag from a project', () => { + return request(app.getHttpServer()) + .delete(`/api/tags/projects/${testProject.id}/tags/${projectTag.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('projectId', testProject.id); + expect(res.body).toHaveProperty('tagId', projectTag.id); + }); + }); + + it('should return 404 when adding a tag to a non-existent project', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .post(`/api/tags/projects/${nonExistentId}/tags/${projectTag.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + + it('should return 400 when adding a person tag to a project', async () => { + // Create a person tag + const [personTag] = await db + .insert(schema.tags) + .values({ + name: `Person Tag ${uuidv4().substring(0, 8)}`, + color: '#CCFF33', + type: 'PERSON' + }) + .returning(); + + const response = await request(app.getHttpServer()) + .post(`/api/tags/projects/${testProject.id}/tags/${personTag.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(400); + + // Clean up the person tag + await db.delete(schema.tags).where(eq(schema.tags.id, personTag.id)); + }); + }); + + describe('Tag deletion', () => { + let tagToDelete: any; + + beforeEach(async () => { + // Create a new tag to delete + const [tag] = await db + .insert(schema.tags) + .values({ + name: `Tag to Delete ${uuidv4().substring(0, 8)}`, + color: '#FF99CC', + type: 'PERSON' + }) + .returning(); + tagToDelete = tag; + }); + + it('should delete a tag', () => { + return request(app.getHttpServer()) + .delete(`/api/tags/${tagToDelete.id}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .expect((res) => { + expect(res.body).toHaveProperty('id', tagToDelete.id); + expect(res.body.name).toBe(tagToDelete.name); + }); + }); + + it('should return 404 when deleting a non-existent tag', () => { + const nonExistentId = uuidv4(); + return request(app.getHttpServer()) + .delete(`/api/tags/${nonExistentId}`) + .set('Authorization', `Bearer ${accessToken}`) + .expect(404); + }); + }); +});