@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextsparkjs/theme-default",
3
- "version": "0.1.0-beta.100",
3
+ "version": "0.1.0-beta.101",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./config/theme.config.ts",
@@ -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
+ })