Skip to content

Commit 50e8801

Browse files
authored
Merge pull request #15 from AET-DevOps25/feat/client-v1
First draft if client
2 parents 7fe7105 + 680c24f commit 50e8801

File tree

128 files changed

+18524
-382
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+18524
-382
lines changed

client/angular.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
{
4848
"type": "anyComponentStyle",
4949
"maximumWarning": "4kB",
50-
"maximumError": "8kB"
50+
"maximumError": "12kB"
5151
}
5252
],
5353
"outputHashing": "all"
@@ -93,7 +93,10 @@
9393
"styles": [
9494
"src/styles.scss"
9595
],
96-
"scripts": []
96+
"scripts": [],
97+
"codeCoverageExclude": [
98+
"src/app/mocks/**/*"
99+
]
97100
}
98101
}
99102
}

client/cypress.config.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { defineConfig } from 'cypress';
2+
3+
export default defineConfig({
4+
e2e: {
5+
baseUrl: 'http://localhost:4200',
6+
viewportWidth: 1280,
7+
viewportHeight: 720,
8+
video: false,
9+
screenshotOnRunFailure: false,
10+
setupNodeEvents(on, config) {
11+
// implement node event listeners here
12+
},
13+
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
14+
supportFile: 'cypress/support/e2e.ts'
15+
},
16+
component: {
17+
devServer: {
18+
framework: 'angular',
19+
bundler: 'webpack',
20+
},
21+
specPattern: '**/*.cy.ts'
22+
}
23+
});

client/cypress/e2e/auth-flow.cy.ts

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
/// <reference types="cypress" />
2+
3+
describe('Authentication Flow', () => {
4+
beforeEach(() => {
5+
// Clear any existing authentication state
6+
cy.clearLocalStorage();
7+
cy.clearCookies();
8+
});
9+
10+
describe('Login Page', () => {
11+
beforeEach(() => {
12+
cy.visit('/auth/login');
13+
});
14+
15+
it('should display login form', () => {
16+
cy.get('[data-cy=login-form]').should('be.visible');
17+
cy.get('[data-cy=email-input]').should('be.visible');
18+
cy.get('[data-cy=password-input]').should('be.visible');
19+
cy.get('[data-cy=login-button]').should('be.visible');
20+
});
21+
22+
it('should show validation errors for empty form submission', () => {
23+
cy.get('[data-cy=login-button]').click();
24+
25+
cy.get('[data-cy=email-error]').should('contain', 'Email is required');
26+
cy.get('[data-cy=password-error]').should('contain', 'Password is required');
27+
});
28+
29+
it('should show validation error for invalid email format', () => {
30+
cy.get('[data-cy=email-input]').type('invalid-email');
31+
cy.get('[data-cy=email-input]').blur();
32+
33+
cy.get('[data-cy=email-error]').should('contain', 'Please enter a valid email');
34+
});
35+
36+
it('should show validation error for short password', () => {
37+
cy.get('[data-cy=password-input]').type('123');
38+
cy.get('[data-cy=password-input]').blur();
39+
40+
cy.get('[data-cy=password-error]').should('contain', 'Password must be at least 6 characters');
41+
});
42+
43+
it('should toggle password visibility', () => {
44+
cy.get('[data-cy=password-input]').should('have.attr', 'type', 'password');
45+
46+
cy.get('[data-cy=password-toggle]').click();
47+
cy.get('[data-cy=password-input]').should('have.attr', 'type', 'text');
48+
49+
cy.get('[data-cy=password-toggle]').click();
50+
cy.get('[data-cy=password-input]').should('have.attr', 'type', 'password');
51+
});
52+
53+
it('should login successfully with valid credentials', () => {
54+
cy.get('[data-cy=email-input]').type('demo@concepter.com');
55+
cy.get('[data-cy=password-input]').type('demo123');
56+
cy.get('[data-cy=login-button]').click();
57+
58+
// Should redirect to dashboard
59+
cy.url().should('include', '/dashboard');
60+
61+
// Should show user info in header
62+
cy.get('[data-cy=user-menu]').should('be.visible');
63+
});
64+
65+
it('should show error message for invalid credentials', () => {
66+
cy.get('[data-cy=email-input]').type('wrong@example.com');
67+
cy.get('[data-cy=password-input]').type('wrongpassword');
68+
cy.get('[data-cy=login-button]').click();
69+
70+
cy.get('[data-cy=error-message]').should('be.visible');
71+
cy.get('[data-cy=error-message]').should('contain', 'Invalid credentials');
72+
});
73+
74+
it('should remember user preference', () => {
75+
cy.get('[data-cy=email-input]').type('demo@concepter.com');
76+
cy.get('[data-cy=password-input]').type('demo123');
77+
cy.get('[data-cy=remember-me]').check();
78+
cy.get('[data-cy=login-button]').click();
79+
80+
// Verify remember me is checked
81+
cy.get('[data-cy=remember-me]').should('be.checked');
82+
});
83+
84+
it('should navigate to register page', () => {
85+
cy.get('[data-cy=register-link]').click();
86+
cy.url().should('include', '/auth/register');
87+
});
88+
});
89+
90+
describe('Registration Page', () => {
91+
beforeEach(() => {
92+
cy.visit('/auth/register');
93+
});
94+
95+
it('should display registration form', () => {
96+
cy.get('[data-cy=register-form]').should('be.visible');
97+
cy.get('[data-cy=first-name-input]').should('be.visible');
98+
cy.get('[data-cy=last-name-input]').should('be.visible');
99+
cy.get('[data-cy=email-input]').should('be.visible');
100+
cy.get('[data-cy=password-input]').should('be.visible');
101+
cy.get('[data-cy=register-button]').should('be.visible');
102+
});
103+
104+
it('should complete registration flow', () => {
105+
// Fill basic information
106+
cy.get('[data-cy=first-name-input]').type('John');
107+
cy.get('[data-cy=last-name-input]').type('Doe');
108+
cy.get('[data-cy=email-input]').type('john.doe@example.com');
109+
cy.get('[data-cy=password-input]').type('password123');
110+
111+
// Fill preferences if using stepper
112+
cy.get('[data-cy=next-button]').click();
113+
cy.get('[data-cy=event-format-select]').click();
114+
cy.get('[data-cy=hybrid-option]').click();
115+
cy.get('[data-cy=industry-select]').click();
116+
cy.get('[data-cy=technology-option]').click();
117+
118+
cy.get('[data-cy=register-button]').click();
119+
120+
// Should redirect to dashboard after successful registration
121+
cy.url().should('include', '/dashboard');
122+
cy.get('[data-cy=user-menu]').should('contain', 'John Doe');
123+
});
124+
125+
it('should navigate back to login page', () => {
126+
cy.get('[data-cy=login-link]').click();
127+
cy.url().should('include', '/auth/login');
128+
});
129+
});
130+
131+
describe('Authentication State', () => {
132+
it('should redirect unauthenticated users to login', () => {
133+
cy.visit('/dashboard');
134+
cy.url().should('include', '/auth/login');
135+
136+
cy.visit('/concepts');
137+
cy.url().should('include', '/auth/login');
138+
139+
cy.visit('/profile');
140+
cy.url().should('include', '/auth/login');
141+
});
142+
143+
it('should allow access to protected routes after login', () => {
144+
// Login first
145+
cy.visit('/auth/login');
146+
cy.get('[data-cy=email-input]').type('demo@concepter.com');
147+
cy.get('[data-cy=password-input]').type('demo123');
148+
cy.get('[data-cy=login-button]').click();
149+
150+
// Should be able to access protected routes
151+
cy.visit('/dashboard');
152+
cy.url().should('include', '/dashboard');
153+
154+
cy.visit('/concepts');
155+
cy.url().should('include', '/concepts');
156+
157+
cy.visit('/profile');
158+
cy.url().should('include', '/profile');
159+
});
160+
161+
it('should persist authentication across browser refresh', () => {
162+
// Login
163+
cy.visit('/auth/login');
164+
cy.get('[data-cy=email-input]').type('demo@concepter.com');
165+
cy.get('[data-cy=password-input]').type('demo123');
166+
cy.get('[data-cy=login-button]').click();
167+
168+
// Refresh the page
169+
cy.reload();
170+
171+
// Should still be authenticated
172+
cy.url().should('include', '/dashboard');
173+
cy.get('[data-cy=user-menu]').should('be.visible');
174+
});
175+
});
176+
177+
describe('Logout', () => {
178+
beforeEach(() => {
179+
// Login before each logout test
180+
cy.visit('/auth/login');
181+
cy.get('[data-cy=email-input]').type('demo@concepter.com');
182+
cy.get('[data-cy=password-input]').type('demo123');
183+
cy.get('[data-cy=login-button]').click();
184+
cy.url().should('include', '/dashboard');
185+
});
186+
187+
it('should logout successfully', () => {
188+
cy.get('[data-cy=user-menu]').click();
189+
cy.get('[data-cy=logout-button]').click();
190+
191+
// Should redirect to login page
192+
cy.url().should('include', '/auth/login');
193+
194+
// Should not be able to access protected routes
195+
cy.visit('/dashboard');
196+
cy.url().should('include', '/auth/login');
197+
});
198+
199+
it('should clear authentication state after logout', () => {
200+
cy.get('[data-cy=user-menu]').click();
201+
cy.get('[data-cy=logout-button]').click();
202+
203+
// Check that localStorage is cleared
204+
cy.window().then((window) => {
205+
expect(window.localStorage.getItem('access_token')).to.be.null;
206+
expect(window.localStorage.getItem('current_user')).to.be.null;
207+
});
208+
});
209+
});
210+
211+
describe('Loading States', () => {
212+
it('should show loading spinner during login', () => {
213+
cy.visit('/auth/login');
214+
215+
// Intercept the login API call to add delay
216+
cy.intercept('POST', '/api/auth/login', { delay: 1000 }).as('loginRequest');
217+
218+
cy.get('[data-cy=email-input]').type('demo@concepter.com');
219+
cy.get('[data-cy=password-input]').type('demo123');
220+
cy.get('[data-cy=login-button]').click();
221+
222+
// Should show loading spinner
223+
cy.get('[data-cy=loading-spinner]').should('be.visible');
224+
225+
cy.wait('@loginRequest');
226+
227+
// Loading spinner should disappear
228+
cy.get('[data-cy=loading-spinner]').should('not.exist');
229+
});
230+
});
231+
232+
describe('Error Handling', () => {
233+
it('should handle network errors gracefully', () => {
234+
cy.visit('/auth/login');
235+
236+
// Simulate network error
237+
cy.intercept('POST', '/api/auth/login', { forceNetworkError: true }).as('loginError');
238+
239+
cy.get('[data-cy=email-input]').type('demo@concepter.com');
240+
cy.get('[data-cy=password-input]').type('demo123');
241+
cy.get('[data-cy=login-button]').click();
242+
243+
// Should show error message
244+
cy.get('[data-cy=error-message]').should('be.visible');
245+
cy.get('[data-cy=error-message]').should('contain', 'Network error');
246+
});
247+
248+
it('should handle server errors gracefully', () => {
249+
cy.visit('/auth/login');
250+
251+
// Simulate server error
252+
cy.intercept('POST', '/api/auth/login', {
253+
statusCode: 500,
254+
body: { message: 'Internal server error' }
255+
}).as('serverError');
256+
257+
cy.get('[data-cy=email-input]').type('demo@concepter.com');
258+
cy.get('[data-cy=password-input]').type('demo123');
259+
cy.get('[data-cy=login-button]').click();
260+
261+
// Should show error message
262+
cy.get('[data-cy=error-message]').should('be.visible');
263+
cy.get('[data-cy=error-message]').should('contain', 'server error');
264+
});
265+
});
266+
267+
describe('Accessibility', () => {
268+
it('should be keyboard navigable', () => {
269+
cy.visit('/auth/login');
270+
271+
// Tab through form elements
272+
cy.get('body').type('{tab}');
273+
cy.focused().should('have.attr', 'formControlName', 'email');
274+
275+
cy.focused().type('{tab}');
276+
cy.focused().should('have.attr', 'formControlName', 'password');
277+
278+
cy.focused().type('{tab}');
279+
cy.focused().should('have.attr', 'type', 'submit');
280+
});
281+
282+
it('should have proper ARIA labels', () => {
283+
cy.visit('/auth/login');
284+
285+
cy.get('[data-cy=email-input]').should('have.attr', 'aria-label');
286+
cy.get('[data-cy=password-input]').should('have.attr', 'aria-label');
287+
cy.get('[data-cy=login-button]').should('have.attr', 'aria-label');
288+
});
289+
});
290+
});

client/nginx.conf

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,19 @@ server {
1515
add_header Cache-Control "no-cache";
1616
}
1717

18-
# Cache static assets
19-
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
18+
# Cache static assets (Angular generates unique filenames with hashes)
19+
location ~* \.(jpg|jpeg|png|gif|ico)$ {
2020
root /usr/share/nginx/html;
2121
expires 1y;
2222
add_header Cache-Control "public, no-transform";
2323
}
24+
25+
# Cache JS/CSS with shorter duration for development
26+
location ~* \.(css|js)$ {
27+
root /usr/share/nginx/html;
28+
expires 1h;
29+
add_header Cache-Control "public, no-transform";
30+
}
2431

2532
# Error handling
2633
error_page 404 /index.html;

0 commit comments

Comments
 (0)