@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.
- package/package.json +2 -2
- package/tests/cypress/e2e/api/activities/activities-crud.cy.ts +686 -0
- package/tests/cypress/e2e/api/campaigns/campaigns-crud.cy.ts +592 -0
- package/tests/cypress/e2e/api/companies/companies-crud.cy.ts +682 -0
- package/tests/cypress/e2e/api/contacts/contacts-crud.cy.ts +668 -0
- package/tests/cypress/e2e/api/leads/leads-crud.cy.ts +648 -0
- package/tests/cypress/e2e/api/notes/notes-crud.cy.ts +424 -0
- package/tests/cypress/e2e/api/opportunities/opportunities-crud.cy.ts +865 -0
- package/tests/cypress/e2e/api/pipelines/pipelines-crud.cy.ts +545 -0
- package/tests/cypress/e2e/api/products/products-crud.cy.ts +447 -0
- package/tests/cypress/e2e/ui/activities/activities-admin.cy.ts +268 -0
- package/tests/cypress/e2e/ui/activities/activities-member.cy.ts +257 -0
- package/tests/cypress/e2e/ui/activities/activities-owner.cy.ts +268 -0
- package/tests/cypress/e2e/ui/companies/companies-admin.cy.ts +188 -0
- package/tests/cypress/e2e/ui/companies/companies-member.cy.ts +166 -0
- package/tests/cypress/e2e/ui/companies/companies-owner.cy.ts +189 -0
- package/tests/cypress/e2e/ui/contacts/contacts-admin.cy.ts +252 -0
- package/tests/cypress/e2e/ui/contacts/contacts-member.cy.ts +224 -0
- package/tests/cypress/e2e/ui/contacts/contacts-owner.cy.ts +236 -0
- package/tests/cypress/e2e/ui/leads/leads-admin.cy.ts +286 -0
- package/tests/cypress/e2e/ui/leads/leads-member.cy.ts +193 -0
- package/tests/cypress/e2e/ui/leads/leads-owner.cy.ts +210 -0
- package/tests/cypress/e2e/ui/opportunities/opportunities-admin.cy.ts +197 -0
- package/tests/cypress/e2e/ui/opportunities/opportunities-member.cy.ts +229 -0
- package/tests/cypress/e2e/ui/opportunities/opportunities-owner.cy.ts +196 -0
- package/tests/cypress/e2e/ui/pipelines/pipelines-admin.cy.ts +320 -0
- package/tests/cypress/e2e/ui/pipelines/pipelines-member.cy.ts +262 -0
- package/tests/cypress/e2e/ui/pipelines/pipelines-owner.cy.ts +282 -0
- package/tests/cypress/fixtures/blocks.json +9 -0
- package/tests/cypress/fixtures/entities.json +240 -0
- package/tests/cypress/src/components/CRMDataTable.js +223 -0
- package/tests/cypress/src/components/CRMMobileNav.js +138 -0
- package/tests/cypress/src/components/CRMSidebar.js +145 -0
- package/tests/cypress/src/components/CRMTopBar.js +194 -0
- package/tests/cypress/src/components/DealCard.js +197 -0
- package/tests/cypress/src/components/EntityDetail.ts +290 -0
- package/tests/cypress/src/components/EntityForm.ts +357 -0
- package/tests/cypress/src/components/EntityList.ts +360 -0
- package/tests/cypress/src/components/PipelineKanban.js +204 -0
- package/tests/cypress/src/components/StageColumn.js +196 -0
- package/tests/cypress/src/components/index.js +13 -0
- package/tests/cypress/src/components/index.ts +22 -0
- package/tests/cypress/src/controllers/ActivityAPIController.ts +113 -0
- package/tests/cypress/src/controllers/BaseAPIController.ts +307 -0
- package/tests/cypress/src/controllers/CampaignAPIController.ts +114 -0
- package/tests/cypress/src/controllers/CompanyAPIController.ts +112 -0
- package/tests/cypress/src/controllers/ContactAPIController.ts +104 -0
- package/tests/cypress/src/controllers/LeadAPIController.ts +96 -0
- package/tests/cypress/src/controllers/NoteAPIController.ts +130 -0
- package/tests/cypress/src/controllers/OpportunityAPIController.ts +134 -0
- package/tests/cypress/src/controllers/PipelineAPIController.ts +116 -0
- package/tests/cypress/src/controllers/ProductAPIController.ts +113 -0
- package/tests/cypress/src/controllers/index.ts +35 -0
- package/tests/cypress/src/entities/ActivitiesPOM.ts +130 -0
- package/tests/cypress/src/entities/CompaniesPOM.ts +117 -0
- package/tests/cypress/src/entities/ContactsPOM.ts +117 -0
- package/tests/cypress/src/entities/LeadsPOM.ts +129 -0
- package/tests/cypress/src/entities/OpportunitiesPOM.ts +178 -0
- package/tests/cypress/src/entities/PipelinesPOM.ts +341 -0
- package/tests/cypress/src/entities/index.ts +31 -0
- package/tests/cypress/src/forms/OpportunityForm.js +316 -0
- package/tests/cypress/src/forms/PipelineForm.js +243 -0
- package/tests/cypress/src/forms/index.js +8 -0
- package/tests/cypress/src/index.js +22 -0
- package/tests/cypress/src/index.ts +68 -0
- package/tests/cypress/src/selectors.ts +50 -0
- package/tests/cypress/src/session-helpers.ts +94 -0
- package/tests/cypress/support/e2e.ts +89 -0
- package/tests/cypress.config.ts +165 -0
- 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
|
+
})
|