@nextsparkjs/theme-crm 0.1.0-beta.19 → 0.1.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +2 -2
  2. package/tests/cypress/e2e/api/activities/activities-crud.cy.ts +686 -0
  3. package/tests/cypress/e2e/api/campaigns/campaigns-crud.cy.ts +592 -0
  4. package/tests/cypress/e2e/api/companies/companies-crud.cy.ts +682 -0
  5. package/tests/cypress/e2e/api/contacts/contacts-crud.cy.ts +668 -0
  6. package/tests/cypress/e2e/api/leads/leads-crud.cy.ts +648 -0
  7. package/tests/cypress/e2e/api/notes/notes-crud.cy.ts +424 -0
  8. package/tests/cypress/e2e/api/opportunities/opportunities-crud.cy.ts +865 -0
  9. package/tests/cypress/e2e/api/pipelines/pipelines-crud.cy.ts +545 -0
  10. package/tests/cypress/e2e/api/products/products-crud.cy.ts +447 -0
  11. package/tests/cypress/e2e/ui/activities/activities-admin.cy.ts +268 -0
  12. package/tests/cypress/e2e/ui/activities/activities-member.cy.ts +257 -0
  13. package/tests/cypress/e2e/ui/activities/activities-owner.cy.ts +268 -0
  14. package/tests/cypress/e2e/ui/companies/companies-admin.cy.ts +188 -0
  15. package/tests/cypress/e2e/ui/companies/companies-member.cy.ts +166 -0
  16. package/tests/cypress/e2e/ui/companies/companies-owner.cy.ts +189 -0
  17. package/tests/cypress/e2e/ui/contacts/contacts-admin.cy.ts +252 -0
  18. package/tests/cypress/e2e/ui/contacts/contacts-member.cy.ts +224 -0
  19. package/tests/cypress/e2e/ui/contacts/contacts-owner.cy.ts +236 -0
  20. package/tests/cypress/e2e/ui/leads/leads-admin.cy.ts +286 -0
  21. package/tests/cypress/e2e/ui/leads/leads-member.cy.ts +193 -0
  22. package/tests/cypress/e2e/ui/leads/leads-owner.cy.ts +210 -0
  23. package/tests/cypress/e2e/ui/opportunities/opportunities-admin.cy.ts +197 -0
  24. package/tests/cypress/e2e/ui/opportunities/opportunities-member.cy.ts +229 -0
  25. package/tests/cypress/e2e/ui/opportunities/opportunities-owner.cy.ts +196 -0
  26. package/tests/cypress/e2e/ui/pipelines/pipelines-admin.cy.ts +320 -0
  27. package/tests/cypress/e2e/ui/pipelines/pipelines-member.cy.ts +262 -0
  28. package/tests/cypress/e2e/ui/pipelines/pipelines-owner.cy.ts +282 -0
  29. package/tests/cypress/fixtures/blocks.json +9 -0
  30. package/tests/cypress/fixtures/entities.json +240 -0
  31. package/tests/cypress/src/components/CRMDataTable.js +223 -0
  32. package/tests/cypress/src/components/CRMMobileNav.js +138 -0
  33. package/tests/cypress/src/components/CRMSidebar.js +145 -0
  34. package/tests/cypress/src/components/CRMTopBar.js +194 -0
  35. package/tests/cypress/src/components/DealCard.js +197 -0
  36. package/tests/cypress/src/components/EntityDetail.ts +290 -0
  37. package/tests/cypress/src/components/EntityForm.ts +357 -0
  38. package/tests/cypress/src/components/EntityList.ts +360 -0
  39. package/tests/cypress/src/components/PipelineKanban.js +204 -0
  40. package/tests/cypress/src/components/StageColumn.js +196 -0
  41. package/tests/cypress/src/components/index.js +13 -0
  42. package/tests/cypress/src/components/index.ts +22 -0
  43. package/tests/cypress/src/controllers/ActivityAPIController.ts +113 -0
  44. package/tests/cypress/src/controllers/BaseAPIController.ts +307 -0
  45. package/tests/cypress/src/controllers/CampaignAPIController.ts +114 -0
  46. package/tests/cypress/src/controllers/CompanyAPIController.ts +112 -0
  47. package/tests/cypress/src/controllers/ContactAPIController.ts +104 -0
  48. package/tests/cypress/src/controllers/LeadAPIController.ts +96 -0
  49. package/tests/cypress/src/controllers/NoteAPIController.ts +130 -0
  50. package/tests/cypress/src/controllers/OpportunityAPIController.ts +134 -0
  51. package/tests/cypress/src/controllers/PipelineAPIController.ts +116 -0
  52. package/tests/cypress/src/controllers/ProductAPIController.ts +113 -0
  53. package/tests/cypress/src/controllers/index.ts +35 -0
  54. package/tests/cypress/src/entities/ActivitiesPOM.ts +130 -0
  55. package/tests/cypress/src/entities/CompaniesPOM.ts +117 -0
  56. package/tests/cypress/src/entities/ContactsPOM.ts +117 -0
  57. package/tests/cypress/src/entities/LeadsPOM.ts +129 -0
  58. package/tests/cypress/src/entities/OpportunitiesPOM.ts +178 -0
  59. package/tests/cypress/src/entities/PipelinesPOM.ts +341 -0
  60. package/tests/cypress/src/entities/index.ts +31 -0
  61. package/tests/cypress/src/forms/OpportunityForm.js +316 -0
  62. package/tests/cypress/src/forms/PipelineForm.js +243 -0
  63. package/tests/cypress/src/forms/index.js +8 -0
  64. package/tests/cypress/src/index.js +22 -0
  65. package/tests/cypress/src/index.ts +68 -0
  66. package/tests/cypress/src/selectors.ts +50 -0
  67. package/tests/cypress/src/session-helpers.ts +94 -0
  68. package/tests/cypress/support/e2e.ts +89 -0
  69. package/tests/cypress.config.ts +165 -0
  70. package/tests/tsconfig.json +15 -0
@@ -0,0 +1,668 @@
1
+ /**
2
+ * Contacts API - CRUD Tests
3
+ *
4
+ * Comprehensive test suite for Contact API endpoints.
5
+ * Tests GET, POST, PATCH, DELETE operations.
6
+ *
7
+ * Entity characteristics:
8
+ * - Required fields: firstName, lastName, email
9
+ * - Optional fields: phone, mobile, companyId, position, department, isPrimary, birthDate, linkedin, twitter, preferredChannel, timezone
10
+ * - Access: shared within team (all team members see all contacts)
11
+ * - Team context: required (x-team-id header)
12
+ * - Special: Can be linked to companies via companyId
13
+ */
14
+
15
+ /// <reference types="cypress" />
16
+
17
+ import { ContactAPIController } from '../../../src/controllers'
18
+
19
+ describe('Contacts API - CRUD Operations', () => {
20
+ // Test constants
21
+ const SUPERADMIN_API_KEY = 'test_api_key_for_testing_purposes_only_not_a_real_secret_key_abc123'
22
+ const TEAM_ID = 'team-tmt-001'
23
+ const BASE_URL = Cypress.config('baseUrl') || 'http://localhost:5173'
24
+
25
+ // Controller instance
26
+ let contactAPI: InstanceType<typeof ContactAPIController>
27
+
28
+ // Track created contacts for cleanup
29
+ let createdContacts: any[] = []
30
+
31
+ before(() => {
32
+ // Initialize controller with superadmin credentials
33
+ contactAPI = new ContactAPIController(BASE_URL, SUPERADMIN_API_KEY, TEAM_ID)
34
+ })
35
+
36
+ afterEach(() => {
37
+ // Cleanup created contacts after each test
38
+ createdContacts.forEach((contact) => {
39
+ if (contact?.id) {
40
+ contactAPI.delete(contact.id)
41
+ }
42
+ })
43
+ createdContacts = []
44
+ })
45
+
46
+ // ============================================
47
+ // GET /api/v1/contacts - List Contacts
48
+ // ============================================
49
+ describe('GET /api/v1/contacts - List Contacts', () => {
50
+ it('CONT_API_001: Should list contacts with valid API key', () => {
51
+ contactAPI.getAll().then((response: any) => {
52
+ contactAPI.validateSuccessResponse(response, 200)
53
+ expect(response.body.data).to.be.an('array')
54
+ expect(response.body.info).to.have.property('page')
55
+ expect(response.body.info).to.have.property('limit')
56
+ expect(response.body.info).to.have.property('total')
57
+ expect(response.body.info).to.have.property('totalPages')
58
+
59
+ cy.log(`Found ${response.body.data.length} contacts`)
60
+ })
61
+ })
62
+
63
+ it('CONT_API_002: Should list contacts with pagination', () => {
64
+ contactAPI.getAll({ page: 1, limit: 5 }).then((response: any) => {
65
+ contactAPI.validateSuccessResponse(response, 200)
66
+ expect(response.body.info.page).to.eq(1)
67
+ expect(response.body.info.limit).to.eq(5)
68
+ expect(response.body.data.length).to.be.at.most(5)
69
+
70
+ cy.log(`Page 1 with limit 5: ${response.body.data.length} contacts`)
71
+ })
72
+ })
73
+
74
+ it('CONT_API_003: Should filter contacts by companyId', () => {
75
+ // First create a contact with a specific companyId
76
+ const testCompanyId = `company-${Date.now()}`
77
+ const contactData = contactAPI.generateRandomData({ companyId: testCompanyId })
78
+
79
+ contactAPI.create(contactData).then((createResponse: any) => {
80
+ expect(createResponse.status).to.eq(201)
81
+ createdContacts.push(createResponse.body.data)
82
+
83
+ // Now filter by that companyId
84
+ contactAPI.getAll({ companyId: testCompanyId }).then((response: any) => {
85
+ contactAPI.validateSuccessResponse(response, 200)
86
+ expect(response.body.data).to.be.an('array')
87
+
88
+ // All returned contacts should have the specified companyId
89
+ response.body.data.forEach((contact: any) => {
90
+ expect(contact.companyId).to.eq(testCompanyId)
91
+ })
92
+
93
+ cy.log(`Found ${response.body.data.length} contacts with companyId '${testCompanyId}'`)
94
+ })
95
+ })
96
+ })
97
+
98
+ it('CONT_API_004: Should filter contacts by position', () => {
99
+ // First create a contact with a specific position
100
+ const testPosition = 'CEO'
101
+ const contactData = contactAPI.generateRandomData({ position: testPosition })
102
+
103
+ contactAPI.create(contactData).then((createResponse: any) => {
104
+ expect(createResponse.status).to.eq(201)
105
+ createdContacts.push(createResponse.body.data)
106
+
107
+ // Now filter by that position
108
+ contactAPI.getAll({ position: testPosition }).then((response: any) => {
109
+ contactAPI.validateSuccessResponse(response, 200)
110
+ expect(response.body.data).to.be.an('array')
111
+
112
+ // All returned contacts should have the specified position
113
+ response.body.data.forEach((contact: any) => {
114
+ expect(contact.position).to.eq(testPosition)
115
+ })
116
+
117
+ cy.log(`Found ${response.body.data.length} contacts with position '${testPosition}'`)
118
+ })
119
+ })
120
+ })
121
+
122
+ it('CONT_API_005: Should search contacts by name/email', () => {
123
+ // Create a contact with a unique searchable term
124
+ const uniqueTerm = `SearchContact${Date.now()}`
125
+ const contactData = contactAPI.generateRandomData({
126
+ firstName: `Contact${uniqueTerm}`,
127
+ email: `${uniqueTerm.toLowerCase()}@test.com`
128
+ })
129
+
130
+ contactAPI.create(contactData).then((createResponse: any) => {
131
+ expect(createResponse.status).to.eq(201)
132
+ createdContacts.push(createResponse.body.data)
133
+
134
+ // Search for the unique term
135
+ contactAPI.getAll({ search: uniqueTerm }).then((response: any) => {
136
+ contactAPI.validateSuccessResponse(response, 200)
137
+ expect(response.body.data).to.be.an('array')
138
+ expect(response.body.data.length).to.be.greaterThan(0)
139
+
140
+ // Verify the found contact contains our search term
141
+ const foundContact = response.body.data.find(
142
+ (c: any) => c.id === createResponse.body.data.id
143
+ )
144
+ expect(foundContact).to.exist
145
+ expect(foundContact.firstName + foundContact.email).to.include(uniqueTerm)
146
+
147
+ cy.log(`Search found ${response.body.data.length} contacts matching '${uniqueTerm}'`)
148
+ })
149
+ })
150
+ })
151
+
152
+ it('CONT_API_006: Should return results or empty array for search', () => {
153
+ // Generate a truly unique search term unlikely to match anything
154
+ const nonExistentTerm = `ZZZYYY${Date.now()}XXXNONEXISTENT`
155
+
156
+ contactAPI.getAll({ search: nonExistentTerm }).then((response: any) => {
157
+ contactAPI.validateSuccessResponse(response, 200)
158
+ expect(response.body.data).to.be.an('array')
159
+ // Search should work correctly - total can be string or number
160
+ expect(Number(response.body.info.total)).to.be.at.least(0)
161
+
162
+ cy.log(`Search returned ${response.body.data.length} contacts for term '${nonExistentTerm}'`)
163
+ })
164
+ })
165
+
166
+ it('CONT_API_007: Should reject request without API key', () => {
167
+ const noAuthAPI = new ContactAPIController(BASE_URL, null, TEAM_ID)
168
+
169
+ noAuthAPI.getAll().then((response: any) => {
170
+ expect(response.status).to.eq(401)
171
+ expect(response.body).to.have.property('success', false)
172
+
173
+ cy.log('Request without API key rejected with 401')
174
+ })
175
+ })
176
+
177
+ it('CONT_API_008: Should reject request without x-team-id', () => {
178
+ const noTeamAPI = new ContactAPIController(BASE_URL, SUPERADMIN_API_KEY, null)
179
+
180
+ noTeamAPI.getAll().then((response: any) => {
181
+ expect(response.status).to.eq(400)
182
+ expect(response.body).to.have.property('success', false)
183
+ expect(response.body).to.have.property('code', 'TEAM_CONTEXT_REQUIRED')
184
+
185
+ cy.log('Request without x-team-id rejected with TEAM_CONTEXT_REQUIRED')
186
+ })
187
+ })
188
+ })
189
+
190
+ // ============================================
191
+ // POST /api/v1/contacts - Create Contact
192
+ // ============================================
193
+ describe('POST /api/v1/contacts - Create Contact', () => {
194
+ it('CONT_API_010: Should create contact with valid data', () => {
195
+ const contactData = contactAPI.generateRandomData({
196
+ phone: '+1-555-1234',
197
+ position: 'VP Sales',
198
+ department: 'Sales',
199
+ preferredChannel: 'email',
200
+ isPrimary: false
201
+ })
202
+
203
+ contactAPI.create(contactData).then((response: any) => {
204
+ contactAPI.validateSuccessResponse(response, 201)
205
+ createdContacts.push(response.body.data)
206
+
207
+ const contact = response.body.data
208
+ contactAPI.validateObject(contact)
209
+
210
+ // Verify provided data
211
+ expect(contact.firstName).to.eq(contactData.firstName)
212
+ expect(contact.email).to.eq(contactData.email)
213
+ expect(contact.lastName).to.eq(contactData.lastName)
214
+ expect(contact.phone).to.eq(contactData.phone)
215
+ expect(contact.position).to.eq(contactData.position)
216
+ expect(contact.department).to.eq(contactData.department)
217
+ expect(contact.preferredChannel).to.eq(contactData.preferredChannel)
218
+
219
+ cy.log(`Created contact: ${contact.firstName} ${contact.lastName} (ID: ${contact.id})`)
220
+ })
221
+ })
222
+
223
+ it('CONT_API_011: Should create contact with minimal data (firstName, lastName, email)', () => {
224
+ const minimalData = {
225
+ firstName: `MinimalContact${Date.now()}`,
226
+ lastName: `LastName${Date.now()}`,
227
+ email: `minimal-${Date.now()}@test.com`
228
+ }
229
+
230
+ contactAPI.create(minimalData).then((response: any) => {
231
+ contactAPI.validateSuccessResponse(response, 201)
232
+ createdContacts.push(response.body.data)
233
+
234
+ const contact = response.body.data
235
+ contactAPI.validateObject(contact)
236
+
237
+ // Verify required fields
238
+ expect(contact.firstName).to.eq(minimalData.firstName)
239
+ expect(contact.lastName).to.eq(minimalData.lastName)
240
+ expect(contact.email).to.eq(minimalData.email)
241
+
242
+ // Verify optional fields are null or undefined
243
+ const optionalFields = ['phone', 'mobile', 'position', 'department', 'companyId', 'preferredChannel']
244
+ optionalFields.forEach(field => {
245
+ if (contact[field] !== null && contact[field] !== undefined) {
246
+ expect(contact[field]).to.be.a('string')
247
+ }
248
+ })
249
+
250
+ cy.log(`Created contact with minimal data: ${contact.id}`)
251
+ })
252
+ })
253
+
254
+ it('CONT_API_012: Should create contact with all optional fields', () => {
255
+ const contactData = contactAPI.generateRandomData({
256
+ firstName: 'John',
257
+ lastName: 'Doe',
258
+ email: `complete-${Date.now()}@test.com`,
259
+ phone: '+1-555-9999',
260
+ mobile: '+1-555-8888',
261
+ position: 'CEO',
262
+ department: 'Executive',
263
+ isPrimary: true,
264
+ preferredChannel: 'email',
265
+ linkedin: 'https://linkedin.com/in/johndoe',
266
+ twitter: '@johndoe',
267
+ timezone: 'America/New_York'
268
+ })
269
+
270
+ contactAPI.create(contactData).then((response: any) => {
271
+ contactAPI.validateSuccessResponse(response, 201)
272
+ createdContacts.push(response.body.data)
273
+
274
+ const contact = response.body.data
275
+ expect(contact.firstName).to.eq(contactData.firstName)
276
+ expect(contact.lastName).to.eq(contactData.lastName)
277
+ expect(contact.email).to.eq(contactData.email)
278
+ expect(contact.phone).to.eq(contactData.phone)
279
+ expect(contact.mobile).to.eq(contactData.mobile)
280
+ expect(contact.position).to.eq(contactData.position)
281
+ expect(contact.department).to.eq(contactData.department)
282
+ expect(contact.preferredChannel).to.eq(contactData.preferredChannel)
283
+
284
+ cy.log(`Created contact with all optional fields: ${JSON.stringify(contact, null, 2)}`)
285
+ })
286
+ })
287
+
288
+ it('CONT_API_013: Should reject creation without firstName', () => {
289
+ const invalidData = {
290
+ lastName: 'Test',
291
+ email: `nofirstname-${Date.now()}@test.com`
292
+ // Missing: firstName
293
+ }
294
+
295
+ contactAPI.create(invalidData).then((response: any) => {
296
+ contactAPI.validateErrorResponse(response, 400, 'VALIDATION_ERROR')
297
+
298
+ cy.log('Creation without firstName rejected with VALIDATION_ERROR')
299
+ })
300
+ })
301
+
302
+ it('CONT_API_014: Should reject creation without email', () => {
303
+ const invalidData = {
304
+ firstName: 'NoEmail',
305
+ lastName: 'Test'
306
+ // Missing: email
307
+ }
308
+
309
+ contactAPI.create(invalidData).then((response: any) => {
310
+ contactAPI.validateErrorResponse(response, 400, 'VALIDATION_ERROR')
311
+
312
+ cy.log('Creation without email rejected with VALIDATION_ERROR')
313
+ })
314
+ })
315
+
316
+ it('CONT_API_015: Should reject creation with invalid email format', () => {
317
+ const invalidData = {
318
+ firstName: 'InvalidEmail',
319
+ lastName: 'Test',
320
+ email: 'not-a-valid-email'
321
+ }
322
+
323
+ contactAPI.create(invalidData).then((response: any) => {
324
+ contactAPI.validateErrorResponse(response, 400, 'VALIDATION_ERROR')
325
+
326
+ cy.log('Creation with invalid email format rejected with VALIDATION_ERROR')
327
+ })
328
+ })
329
+
330
+ it('CONT_API_016: Should reject duplicate email', () => {
331
+ // First create a contact
332
+ const contactData = contactAPI.generateRandomData()
333
+
334
+ contactAPI.create(contactData).then((createResponse: any) => {
335
+ expect(createResponse.status).to.eq(201)
336
+ createdContacts.push(createResponse.body.data)
337
+
338
+ // Try to create another contact with the same email
339
+ const duplicateData = contactAPI.generateRandomData({
340
+ email: contactData.email // Same email
341
+ })
342
+
343
+ contactAPI.create(duplicateData).then((response: any) => {
344
+ // Should fail due to UNIQUE constraint
345
+ expect(response.status).to.be.oneOf([400, 409, 500])
346
+ expect(response.body).to.have.property('success', false)
347
+
348
+ cy.log('Duplicate email rejected')
349
+ })
350
+ })
351
+ })
352
+
353
+ it('CONT_API_017: Should reject creation without x-team-id', () => {
354
+ const noTeamAPI = new ContactAPIController(BASE_URL, SUPERADMIN_API_KEY, null)
355
+ const contactData = noTeamAPI.generateRandomData()
356
+
357
+ noTeamAPI.create(contactData).then((response: any) => {
358
+ expect(response.status).to.eq(400)
359
+ expect(response.body).to.have.property('success', false)
360
+ expect(response.body).to.have.property('code', 'TEAM_CONTEXT_REQUIRED')
361
+
362
+ cy.log('Creation without x-team-id rejected with TEAM_CONTEXT_REQUIRED')
363
+ })
364
+ })
365
+ })
366
+
367
+ // ============================================
368
+ // GET /api/v1/contacts/{id} - Get Contact by ID
369
+ // ============================================
370
+ describe('GET /api/v1/contacts/{id} - Get Contact by ID', () => {
371
+ it('CONT_API_020: Should get contact by valid ID', () => {
372
+ // First create a contact
373
+ const contactData = contactAPI.generateRandomData()
374
+
375
+ contactAPI.create(contactData).then((createResponse: any) => {
376
+ expect(createResponse.status).to.eq(201)
377
+ createdContacts.push(createResponse.body.data)
378
+
379
+ const contactId = createResponse.body.data.id
380
+
381
+ // Get the contact by ID
382
+ contactAPI.getById(contactId).then((response: any) => {
383
+ contactAPI.validateSuccessResponse(response, 200)
384
+
385
+ const contact = response.body.data
386
+ contactAPI.validateObject(contact)
387
+ expect(contact.id).to.eq(contactId)
388
+ expect(contact.firstName).to.eq(contactData.firstName)
389
+ expect(contact.email).to.eq(contactData.email)
390
+
391
+ cy.log(`Retrieved contact: ${contact.firstName} ${contact.lastName}`)
392
+ })
393
+ })
394
+ })
395
+
396
+ it('CONT_API_021: Should return 404 for non-existent contact', () => {
397
+ const fakeId = 'non-existent-contact-id-12345'
398
+
399
+ contactAPI.getById(fakeId).then((response: any) => {
400
+ expect(response.status).to.eq(404)
401
+ expect(response.body).to.have.property('success', false)
402
+
403
+ cy.log('Non-existent contact returns 404')
404
+ })
405
+ })
406
+
407
+ // Note: No CONT_API_022 test for "access other user's record" because
408
+ // contacts entity has shared: true - all team members can see all contacts
409
+ })
410
+
411
+ // ============================================
412
+ // PATCH /api/v1/contacts/{id} - Update Contact
413
+ // ============================================
414
+ describe('PATCH /api/v1/contacts/{id} - Update Contact', () => {
415
+ it('CONT_API_030: Should update contact with multiple fields', () => {
416
+ // First create a contact
417
+ contactAPI.createTestRecord().then((testContact: any) => {
418
+ createdContacts.push(testContact)
419
+
420
+ const updateData = {
421
+ firstName: 'UpdatedFirstName',
422
+ lastName: 'UpdatedLastName',
423
+ phone: '+1-555-9999',
424
+ position: 'Updated Position'
425
+ }
426
+
427
+ contactAPI.update(testContact.id, updateData).then((response: any) => {
428
+ contactAPI.validateSuccessResponse(response, 200)
429
+
430
+ const contact = response.body.data
431
+ expect(contact.firstName).to.eq(updateData.firstName)
432
+ expect(contact.lastName).to.eq(updateData.lastName)
433
+ expect(contact.phone).to.eq(updateData.phone)
434
+ expect(contact.position).to.eq(updateData.position)
435
+ // Original email should be preserved
436
+ expect(contact.email).to.eq(testContact.email)
437
+
438
+ cy.log(`Updated contact: ${contact.firstName} ${contact.lastName}`)
439
+ })
440
+ })
441
+ })
442
+
443
+ it('CONT_API_031: Should update contact firstName', () => {
444
+ contactAPI.createTestRecord().then((testContact: any) => {
445
+ createdContacts.push(testContact)
446
+
447
+ const newFirstName = 'NewFirstName'
448
+
449
+ contactAPI.update(testContact.id, { firstName: newFirstName }).then((response: any) => {
450
+ contactAPI.validateSuccessResponse(response, 200)
451
+ expect(response.body.data.firstName).to.eq(newFirstName)
452
+
453
+ cy.log(`Updated firstName to: ${newFirstName}`)
454
+ })
455
+ })
456
+ })
457
+
458
+ it('CONT_API_032: Should update contact companyId (link to company)', () => {
459
+ contactAPI.createTestRecord().then((testContact: any) => {
460
+ createdContacts.push(testContact)
461
+
462
+ const newCompanyId = `company-${Date.now()}`
463
+
464
+ contactAPI.update(testContact.id, { companyId: newCompanyId }).then((response: any) => {
465
+ contactAPI.validateSuccessResponse(response, 200)
466
+ expect(response.body.data.companyId).to.eq(newCompanyId)
467
+
468
+ cy.log(`Linked contact to company: ${newCompanyId}`)
469
+ })
470
+ })
471
+ })
472
+
473
+ it('CONT_API_033: Should update contact phone and department', () => {
474
+ contactAPI.createTestRecord().then((testContact: any) => {
475
+ createdContacts.push(testContact)
476
+
477
+ const updateData = {
478
+ phone: '+1-555-7777',
479
+ mobile: '+1-555-8888',
480
+ department: 'Engineering',
481
+ preferredChannel: 'phone'
482
+ }
483
+
484
+ contactAPI.update(testContact.id, updateData).then((response: any) => {
485
+ contactAPI.validateSuccessResponse(response, 200)
486
+ expect(response.body.data.phone).to.eq(updateData.phone)
487
+ expect(response.body.data.mobile).to.eq(updateData.mobile)
488
+ expect(response.body.data.department).to.eq(updateData.department)
489
+ expect(response.body.data.preferredChannel).to.eq(updateData.preferredChannel)
490
+
491
+ cy.log(`Updated phone and department`)
492
+ })
493
+ })
494
+ })
495
+
496
+ it('CONT_API_034: Should reject update to duplicate email', () => {
497
+ // Create two contacts
498
+ contactAPI.createTestRecord().then((contact1: any) => {
499
+ createdContacts.push(contact1)
500
+
501
+ contactAPI.createTestRecord().then((contact2: any) => {
502
+ createdContacts.push(contact2)
503
+
504
+ // Try to update contact2's email to contact1's email
505
+ contactAPI
506
+ .update(contact2.id, { email: contact1.email })
507
+ .then((response: any) => {
508
+ // Should fail due to UNIQUE constraint
509
+ expect(response.status).to.be.oneOf([400, 409, 500])
510
+ expect(response.body).to.have.property('success', false)
511
+
512
+ cy.log('Update to duplicate email rejected')
513
+ })
514
+ })
515
+ })
516
+ })
517
+
518
+ it('CONT_API_035: Should return 404 for non-existent contact', () => {
519
+ const fakeId = 'non-existent-contact-id-12345'
520
+
521
+ contactAPI.update(fakeId, { firstName: 'NewName' }).then((response: any) => {
522
+ expect(response.status).to.eq(404)
523
+ expect(response.body).to.have.property('success', false)
524
+
525
+ cy.log('Update non-existent contact returns 404')
526
+ })
527
+ })
528
+
529
+ it('CONT_API_036: Should reject empty update body', () => {
530
+ contactAPI.createTestRecord().then((testContact: any) => {
531
+ createdContacts.push(testContact)
532
+
533
+ contactAPI.update(testContact.id, {}).then((response: any) => {
534
+ expect(response.status).to.eq(400)
535
+ expect(response.body).to.have.property('success', false)
536
+ // Error code can be NO_FIELDS or HTTP_400 depending on validation layer
537
+ expect(response.body.code).to.be.oneOf(['NO_FIELDS', 'HTTP_400'])
538
+
539
+ cy.log('Empty update body rejected')
540
+ })
541
+ })
542
+ })
543
+ })
544
+
545
+ // ============================================
546
+ // DELETE /api/v1/contacts/{id} - Delete Contact
547
+ // ============================================
548
+ describe('DELETE /api/v1/contacts/{id} - Delete Contact', () => {
549
+ it('CONT_API_040: Should delete contact by valid ID', () => {
550
+ // Create a contact to delete
551
+ const contactData = contactAPI.generateRandomData()
552
+
553
+ contactAPI.create(contactData).then((createResponse: any) => {
554
+ expect(createResponse.status).to.eq(201)
555
+ const contactId = createResponse.body.data.id
556
+
557
+ // Delete the contact
558
+ contactAPI.delete(contactId).then((response: any) => {
559
+ contactAPI.validateSuccessResponse(response, 200)
560
+ expect(response.body.data).to.have.property('success', true)
561
+ expect(response.body.data).to.have.property('id', contactId)
562
+
563
+ cy.log(`Deleted contact: ${contactId}`)
564
+ })
565
+ })
566
+ })
567
+
568
+ it('CONT_API_041: Should return 404 for non-existent contact', () => {
569
+ const fakeId = 'non-existent-contact-id-12345'
570
+
571
+ contactAPI.delete(fakeId).then((response: any) => {
572
+ expect(response.status).to.eq(404)
573
+ expect(response.body).to.have.property('success', false)
574
+
575
+ cy.log('Delete non-existent contact returns 404')
576
+ })
577
+ })
578
+
579
+ it('CONT_API_042: Should verify deletion persists', () => {
580
+ // Create a contact
581
+ const contactData = contactAPI.generateRandomData()
582
+
583
+ contactAPI.create(contactData).then((createResponse: any) => {
584
+ expect(createResponse.status).to.eq(201)
585
+ const contactId = createResponse.body.data.id
586
+
587
+ // Delete it
588
+ contactAPI.delete(contactId).then((deleteResponse: any) => {
589
+ expect(deleteResponse.status).to.eq(200)
590
+
591
+ // Verify it's gone
592
+ contactAPI.getById(contactId).then((getResponse: any) => {
593
+ expect(getResponse.status).to.eq(404)
594
+ expect(getResponse.body).to.have.property('success', false)
595
+
596
+ cy.log('Deletion verified - contact no longer exists')
597
+ })
598
+ })
599
+ })
600
+ })
601
+ })
602
+
603
+ // ============================================
604
+ // Integration - Complete CRUD Lifecycle
605
+ // ============================================
606
+ describe('Integration - Complete CRUD Lifecycle', () => {
607
+ it('CONT_API_100: Should complete full lifecycle: Create -> Read -> Update -> Delete', () => {
608
+ // 1. CREATE
609
+ const contactData = contactAPI.generateRandomData({
610
+ phone: '+1-555-0000',
611
+ position: 'Initial Position',
612
+ department: 'Sales',
613
+ preferredChannel: 'email'
614
+ })
615
+
616
+ contactAPI.create(contactData).then((createResponse: any) => {
617
+ contactAPI.validateSuccessResponse(createResponse, 201)
618
+ const contactId = createResponse.body.data.id
619
+
620
+ cy.log(`1. Created contact: ${contactId}`)
621
+
622
+ // 2. READ
623
+ contactAPI.getById(contactId).then((readResponse: any) => {
624
+ contactAPI.validateSuccessResponse(readResponse, 200)
625
+ expect(readResponse.body.data.firstName).to.eq(contactData.firstName)
626
+ expect(readResponse.body.data.email).to.eq(contactData.email)
627
+
628
+ cy.log(`2. Read contact: ${readResponse.body.data.firstName} ${readResponse.body.data.lastName}`)
629
+
630
+ // 3. UPDATE
631
+ const updateData = {
632
+ firstName: 'UpdatedLifecycle',
633
+ lastName: 'Contact',
634
+ phone: '+1-555-9999',
635
+ position: 'Updated Position',
636
+ department: 'Engineering',
637
+ preferredChannel: 'phone'
638
+ }
639
+
640
+ contactAPI.update(contactId, updateData).then((updateResponse: any) => {
641
+ contactAPI.validateSuccessResponse(updateResponse, 200)
642
+ expect(updateResponse.body.data.firstName).to.eq(updateData.firstName)
643
+ expect(updateResponse.body.data.phone).to.eq(updateData.phone)
644
+ expect(updateResponse.body.data.position).to.eq(updateData.position)
645
+
646
+ cy.log(`3. Updated contact: ${updateResponse.body.data.firstName} ${updateResponse.body.data.lastName}`)
647
+
648
+ // 4. DELETE
649
+ contactAPI.delete(contactId).then((deleteResponse: any) => {
650
+ contactAPI.validateSuccessResponse(deleteResponse, 200)
651
+ expect(deleteResponse.body.data).to.have.property('success', true)
652
+
653
+ cy.log(`4. Deleted contact: ${contactId}`)
654
+
655
+ // 5. VERIFY DELETION
656
+ contactAPI.getById(contactId).then((verifyResponse: any) => {
657
+ expect(verifyResponse.status).to.eq(404)
658
+
659
+ cy.log('5. Verified deletion - contact no longer exists')
660
+ cy.log('Full CRUD lifecycle completed successfully!')
661
+ })
662
+ })
663
+ })
664
+ })
665
+ })
666
+ })
667
+ })
668
+ })