@nextsparkjs/theme-crm 0.1.0-beta.18 → 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,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leads API - CRUD Tests
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive test suite for Lead API endpoints.
|
|
5
|
+
* Tests GET, POST, PATCH, DELETE operations.
|
|
6
|
+
*
|
|
7
|
+
* Entity characteristics:
|
|
8
|
+
* - Required fields: companyName, contactName, email
|
|
9
|
+
* - Access: shared within team (all team members see all leads)
|
|
10
|
+
* - Team context: required (x-team-id header)
|
|
11
|
+
* - Special: score field, source enum, status enum
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/// <reference types="cypress" />
|
|
15
|
+
|
|
16
|
+
import { LeadAPIController } from '../../../src/controllers'
|
|
17
|
+
|
|
18
|
+
describe('Leads API - CRUD Operations', () => {
|
|
19
|
+
// Test constants
|
|
20
|
+
const SUPERADMIN_API_KEY = 'test_api_key_for_testing_purposes_only_not_a_real_secret_key_abc123'
|
|
21
|
+
const TEAM_ID = 'team-tmt-001'
|
|
22
|
+
const BASE_URL = Cypress.config('baseUrl') || 'http://localhost:5173'
|
|
23
|
+
|
|
24
|
+
// Controller instance
|
|
25
|
+
let leadAPI: InstanceType<typeof LeadAPIController>
|
|
26
|
+
|
|
27
|
+
// Track created leads for cleanup
|
|
28
|
+
let createdLeads: any[] = []
|
|
29
|
+
|
|
30
|
+
before(() => {
|
|
31
|
+
// Initialize controller with superadmin credentials
|
|
32
|
+
leadAPI = new LeadAPIController(BASE_URL, SUPERADMIN_API_KEY, TEAM_ID)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
// Cleanup created leads after each test
|
|
37
|
+
createdLeads.forEach((lead) => {
|
|
38
|
+
if (lead?.id) {
|
|
39
|
+
leadAPI.delete(lead.id)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
createdLeads = []
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// GET /api/v1/leads - List Leads
|
|
47
|
+
// ============================================
|
|
48
|
+
describe('GET /api/v1/leads - List Leads', () => {
|
|
49
|
+
it('LEAD_API_001: Should list leads with valid API key', () => {
|
|
50
|
+
leadAPI.getAll().then((response: any) => {
|
|
51
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
52
|
+
expect(response.body.data).to.be.an('array')
|
|
53
|
+
expect(response.body.info).to.have.property('page')
|
|
54
|
+
expect(response.body.info).to.have.property('limit')
|
|
55
|
+
expect(response.body.info).to.have.property('total')
|
|
56
|
+
expect(response.body.info).to.have.property('totalPages')
|
|
57
|
+
|
|
58
|
+
cy.log(`Found ${response.body.data.length} leads`)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('LEAD_API_002: Should list leads with pagination', () => {
|
|
63
|
+
leadAPI.getAll({ page: 1, limit: 5 }).then((response: any) => {
|
|
64
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
65
|
+
expect(response.body.info.page).to.eq(1)
|
|
66
|
+
expect(response.body.info.limit).to.eq(5)
|
|
67
|
+
expect(response.body.data.length).to.be.at.most(5)
|
|
68
|
+
|
|
69
|
+
cy.log(`Page 1 with limit 5: ${response.body.data.length} leads`)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('LEAD_API_003: Should filter leads by status', () => {
|
|
74
|
+
// First create a lead with a specific status
|
|
75
|
+
const testStatus = 'new'
|
|
76
|
+
const leadData = leadAPI.generateRandomData({ status: testStatus })
|
|
77
|
+
|
|
78
|
+
leadAPI.create(leadData).then((createResponse: any) => {
|
|
79
|
+
expect(createResponse.status).to.eq(201)
|
|
80
|
+
createdLeads.push(createResponse.body.data)
|
|
81
|
+
|
|
82
|
+
// Now filter by that status
|
|
83
|
+
leadAPI.getAll({ status: testStatus }).then((response: any) => {
|
|
84
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
85
|
+
expect(response.body.data).to.be.an('array')
|
|
86
|
+
|
|
87
|
+
// All returned leads should have the specified status
|
|
88
|
+
response.body.data.forEach((lead: any) => {
|
|
89
|
+
expect(lead.status).to.eq(testStatus)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
cy.log(`Found ${response.body.data.length} leads with status '${testStatus}'`)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('LEAD_API_004: Should filter leads by source', () => {
|
|
98
|
+
// First create a lead with a specific source (valid value from entity config)
|
|
99
|
+
const testSource = 'web'
|
|
100
|
+
const leadData = leadAPI.generateRandomData({ source: testSource })
|
|
101
|
+
|
|
102
|
+
leadAPI.create(leadData).then((createResponse: any) => {
|
|
103
|
+
expect(createResponse.status).to.eq(201)
|
|
104
|
+
createdLeads.push(createResponse.body.data)
|
|
105
|
+
|
|
106
|
+
// Now filter by that source
|
|
107
|
+
leadAPI.getAll({ source: testSource }).then((response: any) => {
|
|
108
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
109
|
+
expect(response.body.data).to.be.an('array')
|
|
110
|
+
|
|
111
|
+
// All returned leads should have the specified source
|
|
112
|
+
response.body.data.forEach((lead: any) => {
|
|
113
|
+
expect(lead.source).to.eq(testSource)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
cy.log(`Found ${response.body.data.length} leads with source '${testSource}'`)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('LEAD_API_005: Should search leads by name/email', () => {
|
|
122
|
+
// Create a lead with a unique searchable term
|
|
123
|
+
const uniqueTerm = `SearchLead${Date.now()}`
|
|
124
|
+
const leadData = leadAPI.generateRandomData({
|
|
125
|
+
companyName: `${uniqueTerm} Corporation`
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
leadAPI.create(leadData).then((createResponse: any) => {
|
|
129
|
+
expect(createResponse.status).to.eq(201)
|
|
130
|
+
createdLeads.push(createResponse.body.data)
|
|
131
|
+
|
|
132
|
+
// Search for the unique term
|
|
133
|
+
leadAPI.getAll({ search: uniqueTerm }).then((response: any) => {
|
|
134
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
135
|
+
expect(response.body.data).to.be.an('array')
|
|
136
|
+
expect(response.body.data.length).to.be.greaterThan(0)
|
|
137
|
+
|
|
138
|
+
// Verify the found lead contains our search term
|
|
139
|
+
const foundLead = response.body.data.find(
|
|
140
|
+
(l: any) => l.id === createResponse.body.data.id
|
|
141
|
+
)
|
|
142
|
+
expect(foundLead).to.exist
|
|
143
|
+
expect(foundLead.companyName).to.include(uniqueTerm)
|
|
144
|
+
|
|
145
|
+
cy.log(`Search found ${response.body.data.length} leads matching '${uniqueTerm}'`)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('LEAD_API_006: Should return empty array for non-matching search', () => {
|
|
151
|
+
const nonExistentTerm = 'NonExistentLeadSearchTerm123456789'
|
|
152
|
+
|
|
153
|
+
leadAPI.getAll({ search: nonExistentTerm }).then((response: any) => {
|
|
154
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
155
|
+
expect(response.body.data).to.be.an('array')
|
|
156
|
+
expect(response.body.data.length).to.eq(0)
|
|
157
|
+
|
|
158
|
+
cy.log('Search with non-matching term returns empty array')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('LEAD_API_007: Should reject request without API key', () => {
|
|
163
|
+
const noAuthAPI = new LeadAPIController(BASE_URL, null, TEAM_ID)
|
|
164
|
+
|
|
165
|
+
noAuthAPI.getAll().then((response: any) => {
|
|
166
|
+
expect(response.status).to.eq(401)
|
|
167
|
+
expect(response.body).to.have.property('success', false)
|
|
168
|
+
|
|
169
|
+
cy.log('Request without API key rejected with 401')
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('LEAD_API_008: Should reject request without x-team-id', () => {
|
|
174
|
+
const noTeamAPI = new LeadAPIController(BASE_URL, SUPERADMIN_API_KEY, null)
|
|
175
|
+
|
|
176
|
+
noTeamAPI.getAll().then((response: any) => {
|
|
177
|
+
expect(response.status).to.eq(400)
|
|
178
|
+
expect(response.body).to.have.property('success', false)
|
|
179
|
+
expect(response.body).to.have.property('code', 'TEAM_CONTEXT_REQUIRED')
|
|
180
|
+
|
|
181
|
+
cy.log('Request without x-team-id rejected with TEAM_CONTEXT_REQUIRED')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// ============================================
|
|
187
|
+
// POST /api/v1/leads - Create Lead
|
|
188
|
+
// ============================================
|
|
189
|
+
describe('POST /api/v1/leads - Create Lead', () => {
|
|
190
|
+
it('LEAD_API_010: Should create lead with valid data', () => {
|
|
191
|
+
const leadData = leadAPI.generateRandomData({
|
|
192
|
+
phone: '+1-555-1234',
|
|
193
|
+
website: 'https://www.testcompany.com',
|
|
194
|
+
score: 85,
|
|
195
|
+
industry: 'Technology',
|
|
196
|
+
companySize: '51-200',
|
|
197
|
+
budget: 50000,
|
|
198
|
+
notes: 'High quality lead from tech conference'
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
leadAPI.create(leadData).then((response: any) => {
|
|
202
|
+
leadAPI.validateSuccessResponse(response, 201)
|
|
203
|
+
createdLeads.push(response.body.data)
|
|
204
|
+
|
|
205
|
+
const lead = response.body.data
|
|
206
|
+
leadAPI.validateObject(lead)
|
|
207
|
+
|
|
208
|
+
// Verify provided data
|
|
209
|
+
expect(lead.companyName).to.eq(leadData.companyName)
|
|
210
|
+
expect(lead.contactName).to.eq(leadData.contactName)
|
|
211
|
+
expect(lead.email).to.eq(leadData.email)
|
|
212
|
+
expect(lead.phone).to.eq(leadData.phone)
|
|
213
|
+
expect(lead.website).to.eq(leadData.website)
|
|
214
|
+
expect(Number(lead.score)).to.eq(leadData.score)
|
|
215
|
+
expect(lead.industry).to.eq(leadData.industry)
|
|
216
|
+
expect(lead.companySize).to.eq(leadData.companySize)
|
|
217
|
+
expect(Number(lead.budget)).to.eq(leadData.budget)
|
|
218
|
+
expect(lead.notes).to.eq(leadData.notes)
|
|
219
|
+
|
|
220
|
+
cy.log(`Created lead: ${lead.companyName} (ID: ${lead.id})`)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('LEAD_API_011: Should create lead with minimal data', () => {
|
|
225
|
+
const minimalData = {
|
|
226
|
+
companyName: `Minimal Company ${Date.now()}`,
|
|
227
|
+
contactName: `Minimal Contact ${Date.now()}`,
|
|
228
|
+
email: `minimal-${Date.now()}@test.com`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
leadAPI.create(minimalData).then((response: any) => {
|
|
232
|
+
leadAPI.validateSuccessResponse(response, 201)
|
|
233
|
+
createdLeads.push(response.body.data)
|
|
234
|
+
|
|
235
|
+
const lead = response.body.data
|
|
236
|
+
leadAPI.validateObject(lead)
|
|
237
|
+
|
|
238
|
+
// Verify required fields
|
|
239
|
+
expect(lead.companyName).to.eq(minimalData.companyName)
|
|
240
|
+
expect(lead.contactName).to.eq(minimalData.contactName)
|
|
241
|
+
expect(lead.email).to.eq(minimalData.email)
|
|
242
|
+
|
|
243
|
+
cy.log(`Created lead with minimal data: ${lead.id}`)
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('LEAD_API_012: Should create lead with score and all optional fields', () => {
|
|
248
|
+
const leadData = leadAPI.generateRandomData({
|
|
249
|
+
score: 100,
|
|
250
|
+
source: 'social_media',
|
|
251
|
+
status: 'qualified',
|
|
252
|
+
industry: 'Healthcare',
|
|
253
|
+
companySize: '500+',
|
|
254
|
+
budget: 100000,
|
|
255
|
+
notes: 'Top priority lead'
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
leadAPI.create(leadData).then((response: any) => {
|
|
259
|
+
leadAPI.validateSuccessResponse(response, 201)
|
|
260
|
+
createdLeads.push(response.body.data)
|
|
261
|
+
|
|
262
|
+
const lead = response.body.data
|
|
263
|
+
expect(Number(lead.score)).to.eq(100)
|
|
264
|
+
expect(lead.source).to.eq('social_media')
|
|
265
|
+
expect(lead.status).to.eq('qualified')
|
|
266
|
+
expect(lead.industry).to.eq('Healthcare')
|
|
267
|
+
expect(lead.companySize).to.eq('500+')
|
|
268
|
+
expect(Number(lead.budget)).to.eq(100000)
|
|
269
|
+
|
|
270
|
+
cy.log(`Created lead with score: ${lead.score}`)
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('LEAD_API_013: Should reject creation without companyName', () => {
|
|
275
|
+
const invalidData = {
|
|
276
|
+
contactName: 'Test Contact',
|
|
277
|
+
email: `test-${Date.now()}@test.com`
|
|
278
|
+
// Missing: companyName
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
leadAPI.create(invalidData).then((response: any) => {
|
|
282
|
+
leadAPI.validateErrorResponse(response, 400, 'VALIDATION_ERROR')
|
|
283
|
+
|
|
284
|
+
cy.log('Creation without companyName rejected with VALIDATION_ERROR')
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('LEAD_API_014: Should reject creation without contactName', () => {
|
|
289
|
+
const invalidData = {
|
|
290
|
+
companyName: 'Test Company',
|
|
291
|
+
email: `test-${Date.now()}@test.com`
|
|
292
|
+
// Missing: contactName
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
leadAPI.create(invalidData).then((response: any) => {
|
|
296
|
+
leadAPI.validateErrorResponse(response, 400, 'VALIDATION_ERROR')
|
|
297
|
+
|
|
298
|
+
cy.log('Creation without contactName rejected with VALIDATION_ERROR')
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('LEAD_API_015: Should reject creation without email', () => {
|
|
303
|
+
const invalidData = {
|
|
304
|
+
companyName: 'Test Company',
|
|
305
|
+
contactName: 'Test Contact'
|
|
306
|
+
// Missing: email
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
leadAPI.create(invalidData).then((response: any) => {
|
|
310
|
+
leadAPI.validateErrorResponse(response, 400, 'VALIDATION_ERROR')
|
|
311
|
+
|
|
312
|
+
cy.log('Creation without email rejected with VALIDATION_ERROR')
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('LEAD_API_016: Should reject duplicate email', () => {
|
|
317
|
+
// First create a lead
|
|
318
|
+
const leadData = leadAPI.generateRandomData()
|
|
319
|
+
|
|
320
|
+
leadAPI.create(leadData).then((createResponse: any) => {
|
|
321
|
+
expect(createResponse.status).to.eq(201)
|
|
322
|
+
createdLeads.push(createResponse.body.data)
|
|
323
|
+
|
|
324
|
+
// Try to create another lead with the same email
|
|
325
|
+
const duplicateData = leadAPI.generateRandomData({
|
|
326
|
+
email: leadData.email // Same email
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
leadAPI.create(duplicateData).then((response: any) => {
|
|
330
|
+
// Should fail due to UNIQUE constraint
|
|
331
|
+
expect(response.status).to.be.oneOf([400, 409, 500])
|
|
332
|
+
expect(response.body).to.have.property('success', false)
|
|
333
|
+
|
|
334
|
+
cy.log('Duplicate email rejected')
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('LEAD_API_017: Should reject creation without x-team-id', () => {
|
|
340
|
+
const noTeamAPI = new LeadAPIController(BASE_URL, SUPERADMIN_API_KEY, null)
|
|
341
|
+
const leadData = noTeamAPI.generateRandomData()
|
|
342
|
+
|
|
343
|
+
noTeamAPI.create(leadData).then((response: any) => {
|
|
344
|
+
expect(response.status).to.eq(400)
|
|
345
|
+
expect(response.body).to.have.property('success', false)
|
|
346
|
+
expect(response.body).to.have.property('code', 'TEAM_CONTEXT_REQUIRED')
|
|
347
|
+
|
|
348
|
+
cy.log('Creation without x-team-id rejected with TEAM_CONTEXT_REQUIRED')
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
// ============================================
|
|
354
|
+
// GET /api/v1/leads/{id} - Get Lead by ID
|
|
355
|
+
// ============================================
|
|
356
|
+
describe('GET /api/v1/leads/{id} - Get Lead by ID', () => {
|
|
357
|
+
it('LEAD_API_020: Should get lead by valid ID', () => {
|
|
358
|
+
// First create a lead
|
|
359
|
+
const leadData = leadAPI.generateRandomData()
|
|
360
|
+
|
|
361
|
+
leadAPI.create(leadData).then((createResponse: any) => {
|
|
362
|
+
expect(createResponse.status).to.eq(201)
|
|
363
|
+
createdLeads.push(createResponse.body.data)
|
|
364
|
+
|
|
365
|
+
const leadId = createResponse.body.data.id
|
|
366
|
+
|
|
367
|
+
// Get the lead by ID
|
|
368
|
+
leadAPI.getById(leadId).then((response: any) => {
|
|
369
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
370
|
+
|
|
371
|
+
const lead = response.body.data
|
|
372
|
+
leadAPI.validateObject(lead)
|
|
373
|
+
expect(lead.id).to.eq(leadId)
|
|
374
|
+
expect(lead.companyName).to.eq(leadData.companyName)
|
|
375
|
+
expect(lead.email).to.eq(leadData.email)
|
|
376
|
+
|
|
377
|
+
cy.log(`Retrieved lead: ${lead.companyName}`)
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('LEAD_API_021: Should return 404 for non-existent lead', () => {
|
|
383
|
+
const fakeId = 'non-existent-lead-id-12345'
|
|
384
|
+
|
|
385
|
+
leadAPI.getById(fakeId).then((response: any) => {
|
|
386
|
+
expect(response.status).to.eq(404)
|
|
387
|
+
expect(response.body).to.have.property('success', false)
|
|
388
|
+
|
|
389
|
+
cy.log('Non-existent lead returns 404')
|
|
390
|
+
})
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
// Note: No LEAD_API_022 test for "access other user's record" because
|
|
394
|
+
// leads entity has shared: true - all team members can see all leads
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// ============================================
|
|
398
|
+
// PATCH /api/v1/leads/{id} - Update Lead
|
|
399
|
+
// ============================================
|
|
400
|
+
describe('PATCH /api/v1/leads/{id} - Update Lead', () => {
|
|
401
|
+
it('LEAD_API_030: Should update lead with valid data', () => {
|
|
402
|
+
// First create a lead
|
|
403
|
+
leadAPI.createTestRecord().then((testLead: any) => {
|
|
404
|
+
createdLeads.push(testLead)
|
|
405
|
+
|
|
406
|
+
const updateData = {
|
|
407
|
+
companyName: 'Updated Company Name',
|
|
408
|
+
contactName: 'Updated Contact Name',
|
|
409
|
+
phone: '+1-555-9999'
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
leadAPI.update(testLead.id, updateData).then((response: any) => {
|
|
413
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
414
|
+
|
|
415
|
+
const lead = response.body.data
|
|
416
|
+
expect(lead.companyName).to.eq(updateData.companyName)
|
|
417
|
+
expect(lead.contactName).to.eq(updateData.contactName)
|
|
418
|
+
expect(lead.phone).to.eq(updateData.phone)
|
|
419
|
+
// Original email should be preserved
|
|
420
|
+
expect(lead.email).to.eq(testLead.email)
|
|
421
|
+
|
|
422
|
+
cy.log(`Updated lead: ${lead.companyName}`)
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it('LEAD_API_031: Should update lead status', () => {
|
|
428
|
+
leadAPI.createTestRecord().then((testLead: any) => {
|
|
429
|
+
createdLeads.push(testLead)
|
|
430
|
+
|
|
431
|
+
const newStatus = 'qualified'
|
|
432
|
+
|
|
433
|
+
leadAPI.update(testLead.id, { status: newStatus }).then((response: any) => {
|
|
434
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
435
|
+
expect(response.body.data.status).to.eq(newStatus)
|
|
436
|
+
|
|
437
|
+
cy.log(`Updated status to: ${newStatus}`)
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('LEAD_API_032: Should update lead score', () => {
|
|
443
|
+
leadAPI.createTestRecord().then((testLead: any) => {
|
|
444
|
+
createdLeads.push(testLead)
|
|
445
|
+
|
|
446
|
+
const newScore = 95
|
|
447
|
+
|
|
448
|
+
leadAPI.update(testLead.id, { score: newScore }).then((response: any) => {
|
|
449
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
450
|
+
expect(Number(response.body.data.score)).to.eq(newScore)
|
|
451
|
+
|
|
452
|
+
cy.log(`Updated score to: ${newScore}`)
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('LEAD_API_033: Should update lead source', () => {
|
|
458
|
+
leadAPI.createTestRecord().then((testLead: any) => {
|
|
459
|
+
createdLeads.push(testLead)
|
|
460
|
+
|
|
461
|
+
// Valid source value from entity config
|
|
462
|
+
const newSource = 'referral'
|
|
463
|
+
|
|
464
|
+
leadAPI.update(testLead.id, { source: newSource }).then((response: any) => {
|
|
465
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
466
|
+
expect(response.body.data.source).to.eq(newSource)
|
|
467
|
+
|
|
468
|
+
cy.log(`Updated source to: ${newSource}`)
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it('LEAD_API_034: Should reject update to duplicate email', () => {
|
|
474
|
+
// Create two leads
|
|
475
|
+
leadAPI.createTestRecord().then((lead1: any) => {
|
|
476
|
+
createdLeads.push(lead1)
|
|
477
|
+
|
|
478
|
+
leadAPI.createTestRecord().then((lead2: any) => {
|
|
479
|
+
createdLeads.push(lead2)
|
|
480
|
+
|
|
481
|
+
// Try to update lead2's email to lead1's email
|
|
482
|
+
leadAPI.update(lead2.id, { email: lead1.email }).then((response: any) => {
|
|
483
|
+
// Should fail due to UNIQUE constraint
|
|
484
|
+
expect(response.status).to.be.oneOf([400, 409, 500])
|
|
485
|
+
expect(response.body).to.have.property('success', false)
|
|
486
|
+
|
|
487
|
+
cy.log('Update to duplicate email rejected')
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it('LEAD_API_035: Should return 404 for non-existent lead', () => {
|
|
494
|
+
const fakeId = 'non-existent-lead-id-12345'
|
|
495
|
+
|
|
496
|
+
leadAPI.update(fakeId, { companyName: 'New Name' }).then((response: any) => {
|
|
497
|
+
expect(response.status).to.eq(404)
|
|
498
|
+
expect(response.body).to.have.property('success', false)
|
|
499
|
+
|
|
500
|
+
cy.log('Update non-existent lead returns 404')
|
|
501
|
+
})
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('LEAD_API_036: Should reject empty update body', () => {
|
|
505
|
+
leadAPI.createTestRecord().then((testLead: any) => {
|
|
506
|
+
createdLeads.push(testLead)
|
|
507
|
+
|
|
508
|
+
leadAPI.update(testLead.id, {}).then((response: any) => {
|
|
509
|
+
expect(response.status).to.eq(400)
|
|
510
|
+
expect(response.body).to.have.property('success', false)
|
|
511
|
+
// Error code can be NO_FIELDS or HTTP_400 depending on validation layer
|
|
512
|
+
expect(response.body.code).to.be.oneOf(['NO_FIELDS', 'HTTP_400'])
|
|
513
|
+
|
|
514
|
+
cy.log('Empty update body rejected')
|
|
515
|
+
})
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
// ============================================
|
|
521
|
+
// DELETE /api/v1/leads/{id} - Delete Lead
|
|
522
|
+
// ============================================
|
|
523
|
+
describe('DELETE /api/v1/leads/{id} - Delete Lead', () => {
|
|
524
|
+
it('LEAD_API_040: Should delete lead by valid ID', () => {
|
|
525
|
+
// Create a lead to delete
|
|
526
|
+
const leadData = leadAPI.generateRandomData()
|
|
527
|
+
|
|
528
|
+
leadAPI.create(leadData).then((createResponse: any) => {
|
|
529
|
+
expect(createResponse.status).to.eq(201)
|
|
530
|
+
const leadId = createResponse.body.data.id
|
|
531
|
+
|
|
532
|
+
// Delete the lead
|
|
533
|
+
leadAPI.delete(leadId).then((response: any) => {
|
|
534
|
+
leadAPI.validateSuccessResponse(response, 200)
|
|
535
|
+
expect(response.body.data).to.have.property('success', true)
|
|
536
|
+
expect(response.body.data).to.have.property('id', leadId)
|
|
537
|
+
|
|
538
|
+
cy.log(`Deleted lead: ${leadId}`)
|
|
539
|
+
})
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('LEAD_API_041: Should return 404 for non-existent lead', () => {
|
|
544
|
+
const fakeId = 'non-existent-lead-id-12345'
|
|
545
|
+
|
|
546
|
+
leadAPI.delete(fakeId).then((response: any) => {
|
|
547
|
+
expect(response.status).to.eq(404)
|
|
548
|
+
expect(response.body).to.have.property('success', false)
|
|
549
|
+
|
|
550
|
+
cy.log('Delete non-existent lead returns 404')
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('LEAD_API_042: Should verify deletion persists', () => {
|
|
555
|
+
// Create a lead
|
|
556
|
+
const leadData = leadAPI.generateRandomData()
|
|
557
|
+
|
|
558
|
+
leadAPI.create(leadData).then((createResponse: any) => {
|
|
559
|
+
expect(createResponse.status).to.eq(201)
|
|
560
|
+
const leadId = createResponse.body.data.id
|
|
561
|
+
|
|
562
|
+
// Delete it
|
|
563
|
+
leadAPI.delete(leadId).then((deleteResponse: any) => {
|
|
564
|
+
expect(deleteResponse.status).to.eq(200)
|
|
565
|
+
|
|
566
|
+
// Verify it's gone
|
|
567
|
+
leadAPI.getById(leadId).then((getResponse: any) => {
|
|
568
|
+
expect(getResponse.status).to.eq(404)
|
|
569
|
+
expect(getResponse.body).to.have.property('success', false)
|
|
570
|
+
|
|
571
|
+
cy.log('Deletion verified - lead no longer exists')
|
|
572
|
+
})
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
})
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
// ============================================
|
|
579
|
+
// Integration - Complete CRUD Lifecycle
|
|
580
|
+
// ============================================
|
|
581
|
+
describe('Integration - Complete CRUD Lifecycle', () => {
|
|
582
|
+
it('LEAD_API_100: Should complete full lifecycle: Create -> Read -> Update -> Delete', () => {
|
|
583
|
+
// 1. CREATE - Use valid enum values from entity config
|
|
584
|
+
const leadData = leadAPI.generateRandomData({
|
|
585
|
+
phone: '+1-555-0000',
|
|
586
|
+
website: 'https://www.initialcompany.com',
|
|
587
|
+
source: 'web',
|
|
588
|
+
status: 'new',
|
|
589
|
+
score: 50,
|
|
590
|
+
industry: 'Technology',
|
|
591
|
+
companySize: '11-50',
|
|
592
|
+
budget: 25000,
|
|
593
|
+
notes: 'Initial lead notes'
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
leadAPI.create(leadData).then((createResponse: any) => {
|
|
597
|
+
leadAPI.validateSuccessResponse(createResponse, 201)
|
|
598
|
+
const leadId = createResponse.body.data.id
|
|
599
|
+
|
|
600
|
+
cy.log(`1. Created lead: ${leadId}`)
|
|
601
|
+
|
|
602
|
+
// 2. READ
|
|
603
|
+
leadAPI.getById(leadId).then((readResponse: any) => {
|
|
604
|
+
leadAPI.validateSuccessResponse(readResponse, 200)
|
|
605
|
+
expect(readResponse.body.data.companyName).to.eq(leadData.companyName)
|
|
606
|
+
expect(readResponse.body.data.email).to.eq(leadData.email)
|
|
607
|
+
|
|
608
|
+
cy.log(`2. Read lead: ${readResponse.body.data.companyName}`)
|
|
609
|
+
|
|
610
|
+
// 3. UPDATE
|
|
611
|
+
const updateData = {
|
|
612
|
+
companyName: 'Updated Lifecycle Company',
|
|
613
|
+
contactName: 'Updated Contact',
|
|
614
|
+
phone: '+1-555-9999',
|
|
615
|
+
status: 'qualified',
|
|
616
|
+
score: 95,
|
|
617
|
+
notes: 'Updated lead notes - now qualified'
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
leadAPI.update(leadId, updateData).then((updateResponse: any) => {
|
|
621
|
+
leadAPI.validateSuccessResponse(updateResponse, 200)
|
|
622
|
+
expect(updateResponse.body.data.companyName).to.eq(updateData.companyName)
|
|
623
|
+
expect(updateResponse.body.data.status).to.eq(updateData.status)
|
|
624
|
+
expect(Number(updateResponse.body.data.score)).to.eq(updateData.score)
|
|
625
|
+
|
|
626
|
+
cy.log(`3. Updated lead: ${updateResponse.body.data.companyName}`)
|
|
627
|
+
|
|
628
|
+
// 4. DELETE
|
|
629
|
+
leadAPI.delete(leadId).then((deleteResponse: any) => {
|
|
630
|
+
leadAPI.validateSuccessResponse(deleteResponse, 200)
|
|
631
|
+
expect(deleteResponse.body.data).to.have.property('success', true)
|
|
632
|
+
|
|
633
|
+
cy.log(`4. Deleted lead: ${leadId}`)
|
|
634
|
+
|
|
635
|
+
// 5. VERIFY DELETION
|
|
636
|
+
leadAPI.getById(leadId).then((verifyResponse: any) => {
|
|
637
|
+
expect(verifyResponse.status).to.eq(404)
|
|
638
|
+
|
|
639
|
+
cy.log('5. Verified deletion - lead no longer exists')
|
|
640
|
+
cy.log('Full CRUD lifecycle completed successfully!')
|
|
641
|
+
})
|
|
642
|
+
})
|
|
643
|
+
})
|
|
644
|
+
})
|
|
645
|
+
})
|
|
646
|
+
})
|
|
647
|
+
})
|
|
648
|
+
})
|