@nextsparkjs/theme-default 0.1.0-beta.100 → 0.1.0-beta.101
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/config/app.config.ts +21 -0
- package/migrations/090_demo_users_teams.sql +11 -11
- package/migrations/091_greek_teams_billing.sql +15 -15
- package/package.json +1 -1
- package/tests/cypress/e2e/uat/_core/auth/registration-control-invitation.cy.ts +176 -0
- package/tests/cypress/e2e/uat/_core/auth/registration-control-open.cy.ts +131 -0
- package/tests/cypress/e2e/uat/_core/auth/registration-control.cy.ts +140 -0
package/config/app.config.ts
CHANGED
|
@@ -34,6 +34,27 @@ export const APP_CONFIG_OVERRIDES = {
|
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
36
|
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// AUTHENTICATION CONFIGURATION
|
|
39
|
+
// =============================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Registration modes:
|
|
42
|
+
* - 'open': Anyone can register (email+password and Google OAuth) - DEFAULT
|
|
43
|
+
* - 'domain-restricted': Only Google OAuth for specific email domains
|
|
44
|
+
* - 'invitation-only': Registration only via invitation link
|
|
45
|
+
*/
|
|
46
|
+
auth: {
|
|
47
|
+
registration: {
|
|
48
|
+
mode: 'domain-restricted' as const,
|
|
49
|
+
allowedDomains: ['nextspark.dev'],
|
|
50
|
+
},
|
|
51
|
+
providers: {
|
|
52
|
+
google: {
|
|
53
|
+
enabled: true,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
37
58
|
// =============================================================================
|
|
38
59
|
// INTERNATIONALIZATION OVERRIDES
|
|
39
60
|
// =============================================================================
|
|
@@ -168,7 +168,7 @@ INSERT INTO "teams" (
|
|
|
168
168
|
'carlos-mendoza-team',
|
|
169
169
|
'Default workspace',
|
|
170
170
|
'usr-carlos-001',
|
|
171
|
-
'{}'::jsonb,
|
|
171
|
+
'{"isSeedData": true}'::jsonb,
|
|
172
172
|
NOW(),
|
|
173
173
|
NOW()
|
|
174
174
|
),
|
|
@@ -178,7 +178,7 @@ INSERT INTO "teams" (
|
|
|
178
178
|
'james-wilson-team',
|
|
179
179
|
'Default workspace',
|
|
180
180
|
'usr-james-002',
|
|
181
|
-
'{}'::jsonb,
|
|
181
|
+
'{"isSeedData": true}'::jsonb,
|
|
182
182
|
NOW(),
|
|
183
183
|
NOW()
|
|
184
184
|
),
|
|
@@ -188,7 +188,7 @@ INSERT INTO "teams" (
|
|
|
188
188
|
'diego-ramirez-team',
|
|
189
189
|
'Default workspace',
|
|
190
190
|
'usr-diego-003',
|
|
191
|
-
'{}'::jsonb,
|
|
191
|
+
'{"isSeedData": true}'::jsonb,
|
|
192
192
|
NOW(),
|
|
193
193
|
NOW()
|
|
194
194
|
),
|
|
@@ -198,7 +198,7 @@ INSERT INTO "teams" (
|
|
|
198
198
|
'michael-brown-team',
|
|
199
199
|
'Default workspace',
|
|
200
200
|
'usr-michael-004',
|
|
201
|
-
'{}'::jsonb,
|
|
201
|
+
'{"isSeedData": true}'::jsonb,
|
|
202
202
|
NOW(),
|
|
203
203
|
NOW()
|
|
204
204
|
),
|
|
@@ -208,7 +208,7 @@ INSERT INTO "teams" (
|
|
|
208
208
|
'ana-garcia-team',
|
|
209
209
|
'Default workspace',
|
|
210
210
|
'usr-ana-005',
|
|
211
|
-
'{}'::jsonb,
|
|
211
|
+
'{"isSeedData": true}'::jsonb,
|
|
212
212
|
NOW(),
|
|
213
213
|
NOW()
|
|
214
214
|
),
|
|
@@ -218,7 +218,7 @@ INSERT INTO "teams" (
|
|
|
218
218
|
'emily-johnson-team',
|
|
219
219
|
'Default workspace',
|
|
220
220
|
'usr-emily-006',
|
|
221
|
-
'{}'::jsonb,
|
|
221
|
+
'{"isSeedData": true}'::jsonb,
|
|
222
222
|
NOW(),
|
|
223
223
|
NOW()
|
|
224
224
|
),
|
|
@@ -228,7 +228,7 @@ INSERT INTO "teams" (
|
|
|
228
228
|
'sofia-lopez-team',
|
|
229
229
|
'Default workspace',
|
|
230
230
|
'usr-sofia-007',
|
|
231
|
-
'{}'::jsonb,
|
|
231
|
+
'{"isSeedData": true}'::jsonb,
|
|
232
232
|
NOW(),
|
|
233
233
|
NOW()
|
|
234
234
|
),
|
|
@@ -238,7 +238,7 @@ INSERT INTO "teams" (
|
|
|
238
238
|
'sarah-davis-team',
|
|
239
239
|
'Default workspace',
|
|
240
240
|
'usr-sarah-008',
|
|
241
|
-
'{}'::jsonb,
|
|
241
|
+
'{"isSeedData": true}'::jsonb,
|
|
242
242
|
NOW(),
|
|
243
243
|
NOW()
|
|
244
244
|
),
|
|
@@ -252,7 +252,7 @@ INSERT INTO "teams" (
|
|
|
252
252
|
'everpoint-labs',
|
|
253
253
|
'Technology Company - Software development and innovation',
|
|
254
254
|
'usr-carlos-001',
|
|
255
|
-
'{"segment": "startup", "industry": "technology"}'::jsonb,
|
|
255
|
+
'{"segment": "startup", "industry": "technology", "isSeedData": true}'::jsonb,
|
|
256
256
|
NOW(),
|
|
257
257
|
NOW()
|
|
258
258
|
),
|
|
@@ -262,7 +262,7 @@ INSERT INTO "teams" (
|
|
|
262
262
|
'ironvale-global',
|
|
263
263
|
'Consulting Firm - Business strategy and management consulting',
|
|
264
264
|
'usr-ana-005',
|
|
265
|
-
'{"segment": "enterprise", "industry": "consulting"}'::jsonb,
|
|
265
|
+
'{"segment": "enterprise", "industry": "consulting", "isSeedData": true}'::jsonb,
|
|
266
266
|
NOW(),
|
|
267
267
|
NOW()
|
|
268
268
|
),
|
|
@@ -272,7 +272,7 @@ INSERT INTO "teams" (
|
|
|
272
272
|
'riverstone-ventures',
|
|
273
273
|
'Investment Fund - Early-stage startup investments',
|
|
274
274
|
'usr-sofia-007',
|
|
275
|
-
'{"segment": "startup", "industry": "finance"}'::jsonb,
|
|
275
|
+
'{"segment": "startup", "industry": "finance", "isSeedData": true}'::jsonb,
|
|
276
276
|
NOW(),
|
|
277
277
|
NOW()
|
|
278
278
|
)
|
|
@@ -154,22 +154,22 @@ INSERT INTO "teams" (
|
|
|
154
154
|
"createdAt",
|
|
155
155
|
"updatedAt"
|
|
156
156
|
) VALUES
|
|
157
|
-
('team-alpha-001', 'Alpha Tech', 'alpha-tech', 'Software startup - Building innovative solutions', 'usr-alpha-01', '{"segment": "startup", "industry": "software", "employeeCount": 15}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
158
|
-
('team-beta-002', 'Beta Solutions', 'beta-solutions', 'Digital agency - Creative marketing and design', 'usr-beta-01', '{"segment": "smb", "industry": "marketing", "employeeCount": 25}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
159
|
-
('team-gamma-003', 'Gamma Industries', 'gamma-industries', 'Manufacturing - Industrial equipment and supplies', 'usr-gamma-01', '{"segment": "enterprise", "industry": "manufacturing", "employeeCount": 150}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
160
|
-
('team-delta-004', 'Delta Dynamics', 'delta-dynamics', 'Engineering - Precision engineering services', 'usr-delta-01', '{"segment": "smb", "industry": "engineering", "employeeCount": 40}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
161
|
-
('team-epsilon-005', 'Epsilon Media', 'epsilon-media', 'Media company - Content creation and distribution', 'usr-epsilon-01', '{"segment": "smb", "industry": "media", "employeeCount": 35}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
162
|
-
('team-zeta-006', 'Zeta Finance', 'zeta-finance', 'Fintech - Financial technology solutions', 'usr-zeta-01', '{"segment": "startup", "industry": "fintech", "employeeCount": 20}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
163
|
-
('team-eta-007', 'Eta Healthcare', 'eta-healthcare', 'Healthcare - Medical technology and services', 'usr-eta-01', '{"segment": "enterprise", "industry": "healthcare", "employeeCount": 200}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
164
|
-
('team-theta-008', 'Theta Enterprises', 'theta-enterprises', 'Large corporation - Enterprise solutions', 'usr-theta-01', '{"segment": "enterprise", "industry": "consulting", "employeeCount": 500}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
165
|
-
('team-iota-009', 'Iota Consulting', 'iota-consulting', 'Consulting - Business strategy and operations', 'usr-iota-01', '{"segment": "smb", "industry": "consulting", "employeeCount": 30, "churned": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
166
|
-
('team-kappa-010', 'Kappa Labs', 'kappa-labs', 'Research lab - R&D and innovation', 'usr-kappa-01', '{"segment": "startup", "industry": "research", "employeeCount": 12, "churned": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
157
|
+
('team-alpha-001', 'Alpha Tech', 'alpha-tech', 'Software startup - Building innovative solutions', 'usr-alpha-01', '{"segment": "startup", "industry": "software", "employeeCount": 15, "isSeedData": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
158
|
+
('team-beta-002', 'Beta Solutions', 'beta-solutions', 'Digital agency - Creative marketing and design', 'usr-beta-01', '{"segment": "smb", "industry": "marketing", "employeeCount": 25, "isSeedData": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
159
|
+
('team-gamma-003', 'Gamma Industries', 'gamma-industries', 'Manufacturing - Industrial equipment and supplies', 'usr-gamma-01', '{"segment": "enterprise", "industry": "manufacturing", "employeeCount": 150, "isSeedData": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
160
|
+
('team-delta-004', 'Delta Dynamics', 'delta-dynamics', 'Engineering - Precision engineering services', 'usr-delta-01', '{"segment": "smb", "industry": "engineering", "employeeCount": 40, "isSeedData": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
161
|
+
('team-epsilon-005', 'Epsilon Media', 'epsilon-media', 'Media company - Content creation and distribution', 'usr-epsilon-01', '{"segment": "smb", "industry": "media", "employeeCount": 35, "isSeedData": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
162
|
+
('team-zeta-006', 'Zeta Finance', 'zeta-finance', 'Fintech - Financial technology solutions', 'usr-zeta-01', '{"segment": "startup", "industry": "fintech", "employeeCount": 20, "isSeedData": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
163
|
+
('team-eta-007', 'Eta Healthcare', 'eta-healthcare', 'Healthcare - Medical technology and services', 'usr-eta-01', '{"segment": "enterprise", "industry": "healthcare", "employeeCount": 200, "isSeedData": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
164
|
+
('team-theta-008', 'Theta Enterprises', 'theta-enterprises', 'Large corporation - Enterprise solutions', 'usr-theta-01', '{"segment": "enterprise", "industry": "consulting", "employeeCount": 500, "isSeedData": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
165
|
+
('team-iota-009', 'Iota Consulting', 'iota-consulting', 'Consulting - Business strategy and operations', 'usr-iota-01', '{"segment": "smb", "industry": "consulting", "employeeCount": 30, "churned": true, "isSeedData": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
166
|
+
('team-kappa-010', 'Kappa Labs', 'kappa-labs', 'Research lab - R&D and innovation', 'usr-kappa-01', '{"segment": "startup", "industry": "research", "employeeCount": 12, "churned": true, "isSeedData": true}'::jsonb, NOW() - INTERVAL '180 days', NOW()),
|
|
167
167
|
-- Annual Subscription Teams (Teams 11-15)
|
|
168
|
-
('team-lambda-011', 'Lambda Corp', 'lambda-corp', 'Enterprise - Global technology corporation', 'usr-lambda-01', '{"segment": "enterprise", "industry": "technology", "employeeCount": 500, "billingCycle": "annual"}'::jsonb, NOW() - INTERVAL '365 days', NOW()),
|
|
169
|
-
('team-mu-012', 'Mu Industries', 'mu-industries', 'Manufacturing - Heavy industry and automation', 'usr-mu-01', '{"segment": "enterprise", "industry": "manufacturing", "employeeCount": 450, "billingCycle": "annual"}'::jsonb, NOW() - INTERVAL '365 days', NOW()),
|
|
170
|
-
('team-nu-013', 'Nu Dynamics', 'nu-dynamics', 'Technology - Advanced systems and AI', 'usr-nu-01', '{"segment": "enterprise", "industry": "technology", "employeeCount": 300, "billingCycle": "annual"}'::jsonb, NOW() - INTERVAL '365 days', NOW()),
|
|
171
|
-
('team-xi-014', 'Xi Solutions', 'xi-solutions', 'Consulting - Strategic advisory services', 'usr-xi-01', '{"segment": "smb", "industry": "consulting", "employeeCount": 50, "billingCycle": "annual"}'::jsonb, NOW() - INTERVAL '365 days', NOW()),
|
|
172
|
-
('team-omicron-015', 'Omicron Labs', 'omicron-labs', 'Research - Scientific research and development', 'usr-omicron-01', '{"segment": "startup", "industry": "research", "employeeCount": 25, "billingCycle": "annual"}'::jsonb, NOW() - INTERVAL '365 days', NOW())
|
|
168
|
+
('team-lambda-011', 'Lambda Corp', 'lambda-corp', 'Enterprise - Global technology corporation', 'usr-lambda-01', '{"segment": "enterprise", "industry": "technology", "employeeCount": 500, "billingCycle": "annual", "isSeedData": true}'::jsonb, NOW() - INTERVAL '365 days', NOW()),
|
|
169
|
+
('team-mu-012', 'Mu Industries', 'mu-industries', 'Manufacturing - Heavy industry and automation', 'usr-mu-01', '{"segment": "enterprise", "industry": "manufacturing", "employeeCount": 450, "billingCycle": "annual", "isSeedData": true}'::jsonb, NOW() - INTERVAL '365 days', NOW()),
|
|
170
|
+
('team-nu-013', 'Nu Dynamics', 'nu-dynamics', 'Technology - Advanced systems and AI', 'usr-nu-01', '{"segment": "enterprise", "industry": "technology", "employeeCount": 300, "billingCycle": "annual", "isSeedData": true}'::jsonb, NOW() - INTERVAL '365 days', NOW()),
|
|
171
|
+
('team-xi-014', 'Xi Solutions', 'xi-solutions', 'Consulting - Strategic advisory services', 'usr-xi-01', '{"segment": "smb", "industry": "consulting", "employeeCount": 50, "billingCycle": "annual", "isSeedData": true}'::jsonb, NOW() - INTERVAL '365 days', NOW()),
|
|
172
|
+
('team-omicron-015', 'Omicron Labs', 'omicron-labs', 'Research - Scientific research and development', 'usr-omicron-01', '{"segment": "startup", "industry": "research", "employeeCount": 25, "billingCycle": "annual", "isSeedData": true}'::jsonb, NOW() - INTERVAL '365 days', NOW())
|
|
173
173
|
ON CONFLICT (id) DO NOTHING;
|
|
174
174
|
|
|
175
175
|
-- NEW TEAM MEMBERSHIPS (50 total - 5 per team)
|
package/package.json
CHANGED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/// <reference types="cypress" />
|
|
2
|
+
|
|
3
|
+
import * as allure from 'allure-cypress'
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_THEME_USERS } from '../../../../src/session-helpers'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registration Control Tests - Invitation-Only Mode
|
|
9
|
+
*
|
|
10
|
+
* Verifies registration mode enforcement when mode is 'invitation-only'.
|
|
11
|
+
* Tests: signup redirect, login page visibility, API blocking, existing user login.
|
|
12
|
+
*
|
|
13
|
+
* These tests detect the current registration mode and skip if not 'invitation-only'.
|
|
14
|
+
* Detection: invitation-only mode redirects /signup AND shows email login on /login
|
|
15
|
+
* (unlike domain-restricted which hides email login).
|
|
16
|
+
*/
|
|
17
|
+
describe('Registration Control - Invitation-Only Mode', {
|
|
18
|
+
tags: ['@uat', '@feat-auth', '@security', '@regression']
|
|
19
|
+
}, () => {
|
|
20
|
+
const TEST_PASSWORD = Cypress.env('TEST_PASSWORD') || 'Test1234'
|
|
21
|
+
|
|
22
|
+
before(() => {
|
|
23
|
+
// Detect invitation-only mode:
|
|
24
|
+
// 1. /signup redirects (shared with domain-restricted)
|
|
25
|
+
// 2. /login shows email form (NOT shared with domain-restricted, which hides it)
|
|
26
|
+
cy.request({
|
|
27
|
+
url: '/signup',
|
|
28
|
+
followRedirect: false,
|
|
29
|
+
failOnStatusCode: false,
|
|
30
|
+
}).then((signupResponse) => {
|
|
31
|
+
if (signupResponse.status < 300 || signupResponse.status >= 400) {
|
|
32
|
+
// /signup is accessible — this is 'open' mode, skip
|
|
33
|
+
Cypress.runner.stop()
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// /signup redirects — could be domain-restricted or invitation-only
|
|
38
|
+
// Check login page for email form to distinguish
|
|
39
|
+
cy.visit('/login')
|
|
40
|
+
cy.get('body').then(($body) => {
|
|
41
|
+
// In domain-restricted mode, email login is hidden (no showEmail toggle, no form)
|
|
42
|
+
// In invitation-only mode, email login IS shown
|
|
43
|
+
const hasEmailForm = $body.find('[data-cy="auth.login.form"]').length > 0
|
|
44
|
+
const hasShowEmail = $body.find('[data-cy="auth.login.showEmail"]').length > 0
|
|
45
|
+
|
|
46
|
+
if (!hasEmailForm && !hasShowEmail) {
|
|
47
|
+
// No email login at all — this is domain-restricted, not invitation-only
|
|
48
|
+
Cypress.runner.stop()
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
allure.epic('UAT')
|
|
56
|
+
allure.feature('Registration Control')
|
|
57
|
+
cy.clearCookies()
|
|
58
|
+
cy.clearLocalStorage()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('REG_INV_001: Signup page redirects in invitation-only mode', () => {
|
|
62
|
+
it('should redirect /signup to /login', () => {
|
|
63
|
+
allure.story('Signup Redirect')
|
|
64
|
+
allure.severity('critical')
|
|
65
|
+
|
|
66
|
+
cy.log('1. Visit /signup')
|
|
67
|
+
cy.visit('/signup', { failOnStatusCode: false })
|
|
68
|
+
|
|
69
|
+
cy.log('2. Should redirect to /login')
|
|
70
|
+
cy.url().should('include', '/login')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('REG_INV_002: Login page shows email login but no signup link', () => {
|
|
75
|
+
it('should show email login and hide signup link', () => {
|
|
76
|
+
allure.story('Login Page Visibility')
|
|
77
|
+
allure.severity('critical')
|
|
78
|
+
|
|
79
|
+
cy.log('1. Visit /login')
|
|
80
|
+
cy.visit('/login')
|
|
81
|
+
|
|
82
|
+
cy.log('2. Email login should be available')
|
|
83
|
+
cy.get('body').then(($body) => {
|
|
84
|
+
if ($body.find('[data-cy="auth.login.showEmail"]').length) {
|
|
85
|
+
cy.get('[data-cy="auth.login.showEmail"]').click()
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
cy.get('[data-cy="auth.login.form"]').should('exist')
|
|
89
|
+
|
|
90
|
+
cy.log('3. Signup link should NOT exist')
|
|
91
|
+
cy.get('[data-cy="auth.login.signupLink"]').should('not.exist')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
describe('REG_INV_003: API blocks email signup in invitation-only mode', () => {
|
|
96
|
+
it('should return 403 for email signup attempt', () => {
|
|
97
|
+
allure.story('API Signup Blocking')
|
|
98
|
+
allure.severity('critical')
|
|
99
|
+
|
|
100
|
+
cy.log('1. POST /api/auth/sign-up/email with new user data')
|
|
101
|
+
cy.request({
|
|
102
|
+
method: 'POST',
|
|
103
|
+
url: '/api/auth/sign-up/email',
|
|
104
|
+
body: {
|
|
105
|
+
name: 'Test Uninvited User',
|
|
106
|
+
email: 'uninvited@some-domain.com',
|
|
107
|
+
password: 'TestPassword123',
|
|
108
|
+
},
|
|
109
|
+
failOnStatusCode: false,
|
|
110
|
+
}).then((response) => {
|
|
111
|
+
cy.log(`Response status: ${response.status}`)
|
|
112
|
+
// Should be blocked — 403 or 500 (from SIGNUP_RESTRICTED error)
|
|
113
|
+
expect(response.status).to.be.oneOf([403, 500])
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('REG_INV_004: API blocks alternative signup endpoints', () => {
|
|
119
|
+
it('should block signup via alternative endpoints', () => {
|
|
120
|
+
allure.story('API Signup Blocking')
|
|
121
|
+
allure.severity('normal')
|
|
122
|
+
|
|
123
|
+
const signupPayload = {
|
|
124
|
+
name: 'Test Uninvited User',
|
|
125
|
+
email: 'uninvited@some-domain.com',
|
|
126
|
+
password: 'TestPassword123',
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const endpoints = [
|
|
130
|
+
'/api/auth/sign-up/email',
|
|
131
|
+
'/api/auth/sign-up/credentials',
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
endpoints.forEach((endpoint) => {
|
|
135
|
+
cy.log(`Testing: POST ${endpoint}`)
|
|
136
|
+
cy.request({
|
|
137
|
+
method: 'POST',
|
|
138
|
+
url: endpoint,
|
|
139
|
+
body: signupPayload,
|
|
140
|
+
failOnStatusCode: false,
|
|
141
|
+
}).then((response) => {
|
|
142
|
+
cy.log(`${endpoint} → ${response.status}`)
|
|
143
|
+
// Should be blocked (403/500 for blocked signup, 404 for non-existent)
|
|
144
|
+
expect(response.status).to.be.oneOf([403, 404, 422, 500])
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('REG_INV_005: Existing user can still login with email+password', () => {
|
|
151
|
+
it('should allow existing user to sign in via API', () => {
|
|
152
|
+
allure.story('Existing User Login')
|
|
153
|
+
allure.severity('critical')
|
|
154
|
+
|
|
155
|
+
const existingUser = DEFAULT_THEME_USERS.OWNER
|
|
156
|
+
|
|
157
|
+
cy.log(`1. POST /api/auth/sign-in/email with existing user: ${existingUser}`)
|
|
158
|
+
cy.request({
|
|
159
|
+
method: 'POST',
|
|
160
|
+
url: '/api/auth/sign-in/email',
|
|
161
|
+
body: {
|
|
162
|
+
email: existingUser,
|
|
163
|
+
password: TEST_PASSWORD,
|
|
164
|
+
},
|
|
165
|
+
failOnStatusCode: false,
|
|
166
|
+
}).then((response) => {
|
|
167
|
+
cy.log(`Response status: ${response.status}`)
|
|
168
|
+
expect(response.status).to.eq(200)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
after(() => {
|
|
174
|
+
cy.log('Registration control (invitation-only mode) tests completed')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/// <reference types="cypress" />
|
|
2
|
+
|
|
3
|
+
import * as allure from 'allure-cypress'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registration Control Tests - Open Mode
|
|
7
|
+
*
|
|
8
|
+
* Verifies registration mode enforcement when mode is 'open'.
|
|
9
|
+
* Tests: signup accessibility, login page elements, API signup allowed.
|
|
10
|
+
*
|
|
11
|
+
* These tests detect the current registration mode and skip if not 'open'.
|
|
12
|
+
*/
|
|
13
|
+
describe('Registration Control - Open Mode', {
|
|
14
|
+
tags: ['@uat', '@feat-auth', '@security', '@regression']
|
|
15
|
+
}, () => {
|
|
16
|
+
const TEST_PASSWORD = Cypress.env('TEST_PASSWORD') || 'Test1234'
|
|
17
|
+
|
|
18
|
+
before(() => {
|
|
19
|
+
// Detect registration mode by checking if /signup is accessible (not redirected)
|
|
20
|
+
cy.request({
|
|
21
|
+
url: '/signup',
|
|
22
|
+
followRedirect: false,
|
|
23
|
+
failOnStatusCode: false,
|
|
24
|
+
}).then((response) => {
|
|
25
|
+
// In open mode, /signup returns 200 (not a redirect)
|
|
26
|
+
// In domain-restricted or invitation-only (with existing team), it redirects (307/308)
|
|
27
|
+
if (response.status >= 300 && response.status < 400) {
|
|
28
|
+
// Not open mode — skip all tests in this suite
|
|
29
|
+
Cypress.runner.stop()
|
|
30
|
+
}
|
|
31
|
+
// Additionally verify that the login page shows a signup link (confirms open mode)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
allure.epic('UAT')
|
|
37
|
+
allure.feature('Registration Control')
|
|
38
|
+
cy.clearCookies()
|
|
39
|
+
cy.clearLocalStorage()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('REG_OPEN_001: Signup page is accessible in open mode', () => {
|
|
43
|
+
it('should show the signup form without redirecting', () => {
|
|
44
|
+
allure.story('Signup Accessibility')
|
|
45
|
+
allure.severity('critical')
|
|
46
|
+
|
|
47
|
+
cy.log('1. Visit /signup')
|
|
48
|
+
cy.visit('/signup')
|
|
49
|
+
|
|
50
|
+
cy.log('2. Should stay on /signup (no redirect)')
|
|
51
|
+
cy.url().should('include', '/signup')
|
|
52
|
+
|
|
53
|
+
cy.log('3. Signup form should be visible')
|
|
54
|
+
cy.get('form').should('exist')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('REG_OPEN_002: Login page shows email login and signup link', () => {
|
|
59
|
+
it('should show email login form and signup link', () => {
|
|
60
|
+
allure.story('Login Page Visibility')
|
|
61
|
+
allure.severity('critical')
|
|
62
|
+
|
|
63
|
+
cy.log('1. Visit /login')
|
|
64
|
+
cy.visit('/login')
|
|
65
|
+
|
|
66
|
+
cy.log('2. Email login should be available')
|
|
67
|
+
// Either the form is visible directly, or a "show email" toggle exists
|
|
68
|
+
cy.get('body').then(($body) => {
|
|
69
|
+
if ($body.find('[data-cy="auth.login.showEmail"]').length) {
|
|
70
|
+
cy.get('[data-cy="auth.login.showEmail"]').click()
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
cy.get('[data-cy="auth.login.form"]').should('exist')
|
|
74
|
+
|
|
75
|
+
cy.log('3. Signup link should be visible')
|
|
76
|
+
cy.get('[data-cy="auth.login.signupLink"]').should('be.visible')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('REG_OPEN_003: API allows email signup in open mode', () => {
|
|
81
|
+
it('should allow new user registration via email', () => {
|
|
82
|
+
allure.story('API Signup Allowed')
|
|
83
|
+
allure.severity('critical')
|
|
84
|
+
|
|
85
|
+
const uniqueEmail = `test-open-${Date.now()}@test-cypress.dev`
|
|
86
|
+
|
|
87
|
+
cy.log(`1. POST /api/auth/sign-up/email with new user: ${uniqueEmail}`)
|
|
88
|
+
cy.request({
|
|
89
|
+
method: 'POST',
|
|
90
|
+
url: '/api/auth/sign-up/email',
|
|
91
|
+
body: {
|
|
92
|
+
name: 'Test Open Mode User',
|
|
93
|
+
email: uniqueEmail,
|
|
94
|
+
password: TEST_PASSWORD,
|
|
95
|
+
},
|
|
96
|
+
failOnStatusCode: false,
|
|
97
|
+
}).then((response) => {
|
|
98
|
+
cy.log(`Response status: ${response.status}`)
|
|
99
|
+
// Open mode should allow signup (200) or require email verification (200 with token)
|
|
100
|
+
// Should NOT be 403 (blocked)
|
|
101
|
+
expect(response.status).to.not.eq(403)
|
|
102
|
+
expect(response.status).to.be.oneOf([200, 201])
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('REG_OPEN_004: Existing user can login with email+password', () => {
|
|
108
|
+
it('should allow existing user to sign in via API', () => {
|
|
109
|
+
allure.story('Existing User Login')
|
|
110
|
+
allure.severity('critical')
|
|
111
|
+
|
|
112
|
+
cy.log('1. POST /api/auth/sign-in/email with existing user')
|
|
113
|
+
cy.request({
|
|
114
|
+
method: 'POST',
|
|
115
|
+
url: '/api/auth/sign-in/email',
|
|
116
|
+
body: {
|
|
117
|
+
email: Cypress.env('OWNER_EMAIL') || 'carlos.mendoza@nextspark.dev',
|
|
118
|
+
password: TEST_PASSWORD,
|
|
119
|
+
},
|
|
120
|
+
failOnStatusCode: false,
|
|
121
|
+
}).then((response) => {
|
|
122
|
+
cy.log(`Response status: ${response.status}`)
|
|
123
|
+
expect(response.status).to.eq(200)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
after(() => {
|
|
129
|
+
cy.log('Registration control (open mode) tests completed')
|
|
130
|
+
})
|
|
131
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/// <reference types="cypress" />
|
|
2
|
+
|
|
3
|
+
import * as allure from 'allure-cypress'
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_THEME_USERS } from '../../../../src/session-helpers'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registration Control Tests
|
|
9
|
+
*
|
|
10
|
+
* Verifies registration mode enforcement in domain-restricted mode (default theme).
|
|
11
|
+
* Tests: signup redirect, login page visibility, API blocking, existing user login.
|
|
12
|
+
*/
|
|
13
|
+
describe('Registration Control - Domain Restricted Mode', {
|
|
14
|
+
tags: ['@uat', '@feat-auth', '@security', '@regression']
|
|
15
|
+
}, () => {
|
|
16
|
+
const TEST_PASSWORD = Cypress.env('TEST_PASSWORD') || 'Test1234'
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
allure.epic('UAT')
|
|
20
|
+
allure.feature('Registration Control')
|
|
21
|
+
cy.clearCookies()
|
|
22
|
+
cy.clearLocalStorage()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('REG_001: Signup page redirects in domain-restricted mode', () => {
|
|
26
|
+
it('should redirect /signup to /login', () => {
|
|
27
|
+
allure.story('Signup Redirect')
|
|
28
|
+
allure.severity('critical')
|
|
29
|
+
|
|
30
|
+
cy.log('1. Visit /signup')
|
|
31
|
+
cy.visit('/signup', { failOnStatusCode: false })
|
|
32
|
+
|
|
33
|
+
cy.log('2. Should redirect to /login')
|
|
34
|
+
cy.url().should('include', '/login')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('REG_002: Login page hides email login in domain-restricted mode', () => {
|
|
39
|
+
it('should show Google button but hide email login options', () => {
|
|
40
|
+
allure.story('Login Page Visibility')
|
|
41
|
+
allure.severity('critical')
|
|
42
|
+
|
|
43
|
+
cy.log('1. Visit /login')
|
|
44
|
+
cy.visit('/login')
|
|
45
|
+
|
|
46
|
+
cy.log('2. Google sign-in button should be visible')
|
|
47
|
+
cy.get('[data-cy="auth.login.googleSignin"]').should('be.visible')
|
|
48
|
+
|
|
49
|
+
cy.log('3. "Show email" link should NOT exist')
|
|
50
|
+
cy.get('[data-cy="auth.login.showEmail"]').should('not.exist')
|
|
51
|
+
|
|
52
|
+
cy.log('4. Email form should NOT exist')
|
|
53
|
+
cy.get('[data-cy="auth.login.form"]').should('not.exist')
|
|
54
|
+
|
|
55
|
+
cy.log('5. Signup link should NOT exist')
|
|
56
|
+
cy.get('[data-cy="auth.login.signupLink"]').should('not.exist')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('REG_003: API blocks email signup in domain-restricted mode', () => {
|
|
61
|
+
it('should return 403 for email signup attempt', () => {
|
|
62
|
+
allure.story('API Signup Blocking')
|
|
63
|
+
allure.severity('critical')
|
|
64
|
+
|
|
65
|
+
cy.log('1. POST /api/auth/sign-up/email with new user data')
|
|
66
|
+
cy.request({
|
|
67
|
+
method: 'POST',
|
|
68
|
+
url: '/api/auth/sign-up/email',
|
|
69
|
+
body: {
|
|
70
|
+
name: 'Test New User',
|
|
71
|
+
email: 'newuser@unauthorized-domain.com',
|
|
72
|
+
password: 'TestPassword123',
|
|
73
|
+
},
|
|
74
|
+
failOnStatusCode: false,
|
|
75
|
+
}).then((response) => {
|
|
76
|
+
cy.log(`Response status: ${response.status}`)
|
|
77
|
+
expect(response.status).to.eq(403)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('REG_004: API blocks alternative signup endpoints', () => {
|
|
83
|
+
it('should block signup via alternative endpoints', () => {
|
|
84
|
+
allure.story('API Signup Blocking')
|
|
85
|
+
allure.severity('normal')
|
|
86
|
+
|
|
87
|
+
const signupPayload = {
|
|
88
|
+
name: 'Test New User',
|
|
89
|
+
email: 'newuser@unauthorized-domain.com',
|
|
90
|
+
password: 'TestPassword123',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const endpoints = [
|
|
94
|
+
'/api/auth/sign-up/email',
|
|
95
|
+
'/api/auth/sign-up/credentials',
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
endpoints.forEach((endpoint) => {
|
|
99
|
+
cy.log(`Testing: POST ${endpoint}`)
|
|
100
|
+
cy.request({
|
|
101
|
+
method: 'POST',
|
|
102
|
+
url: endpoint,
|
|
103
|
+
body: signupPayload,
|
|
104
|
+
failOnStatusCode: false,
|
|
105
|
+
}).then((response) => {
|
|
106
|
+
cy.log(`${endpoint} → ${response.status}`)
|
|
107
|
+
// Should be blocked (403 or 404 for non-existent endpoints)
|
|
108
|
+
expect(response.status).to.be.oneOf([403, 404, 422])
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('REG_005: Existing user can still login with email+password', () => {
|
|
115
|
+
it('should allow existing user to sign in via API', () => {
|
|
116
|
+
allure.story('Existing User Login')
|
|
117
|
+
allure.severity('critical')
|
|
118
|
+
|
|
119
|
+
const existingUser = DEFAULT_THEME_USERS.OWNER
|
|
120
|
+
|
|
121
|
+
cy.log(`1. POST /api/auth/sign-in/email with existing user: ${existingUser}`)
|
|
122
|
+
cy.request({
|
|
123
|
+
method: 'POST',
|
|
124
|
+
url: '/api/auth/sign-in/email',
|
|
125
|
+
body: {
|
|
126
|
+
email: existingUser,
|
|
127
|
+
password: TEST_PASSWORD,
|
|
128
|
+
},
|
|
129
|
+
failOnStatusCode: false,
|
|
130
|
+
}).then((response) => {
|
|
131
|
+
cy.log(`Response status: ${response.status}`)
|
|
132
|
+
expect(response.status).to.eq(200)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
after(() => {
|
|
138
|
+
cy.log('Registration control tests completed')
|
|
139
|
+
})
|
|
140
|
+
})
|