Skip to content

Commit e9e28f5

Browse files
Implement Mock Authentication Service for E2E Tests (#312) (#313)
* Implement mock authentication service for E2E tests (#312) Co-Authored-By: Hal Seki <[email protected]> * Fix Playwright version to v1.52.0-jammy in Dockerfile.test-runner Co-Authored-By: Hal Seki <[email protected]> * Add data-testid attribute to login and signup buttons for E2E tests Co-Authored-By: Hal Seki <[email protected]> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Hal Seki <[email protected]>
1 parent 1d6bc18 commit e9e28f5

File tree

10 files changed

+381
-25
lines changed

10 files changed

+381
-25
lines changed

frontend/src/components/auth/Login.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ const Login: React.FC = () => {
148148
mt={4}
149149
type="submit"
150150
isLoading={loading}
151+
data-testid="email-login-button"
151152
>
152153
Sign In
153154
</Button>

frontend/src/components/auth/SignUp.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ const SignUp: React.FC = () => {
135135
mt={4}
136136
type="submit"
137137
isLoading={loading}
138+
data-testid="email-login-button"
138139
>
139140
Sign Up
140141
</Button>

integration-tests/docker-compose.test.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ services:
6060
condition: service_healthy
6161
environment:
6262
- VITE_API_URL=http://test-backend:8000
63-
- VITE_SUPABASE_URL=${SUPABASE_URL:-http://test-supabase:9000}
63+
- VITE_SUPABASE_URL=${SUPABASE_URL:-http://mock-auth-api:3000}
6464
- VITE_SUPABASE_ANON_KEY=${SUPABASE_ANON_KEY:-test-anon-key}
65+
- VITE_AUTH_REDIRECT_URI=http://test-frontend:5173/auth/callback
6566
ports:
6667
- "5174:5173"
6768
volumes:
@@ -107,6 +108,25 @@ services:
107108
retries: 3
108109
start_period: 30s
109110

111+
# Mock Auth API service
112+
mock-auth-api:
113+
build:
114+
context: ./mocks/auth-api
115+
dockerfile: Dockerfile
116+
ports:
117+
- "3003:3000"
118+
volumes:
119+
- ./mocks/auth-api:/app
120+
- /app/node_modules
121+
environment:
122+
- PORT=3000
123+
healthcheck:
124+
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1))\" || exit 1"]
125+
interval: 10s
126+
timeout: 5s
127+
retries: 3
128+
start_period: 30s
129+
110130
# Test runner container
111131
test-runner:
112132
build:
@@ -121,6 +141,8 @@ services:
121141
condition: service_healthy
122142
mock-openrouter-api:
123143
condition: service_healthy
144+
mock-auth-api:
145+
condition: service_healthy
124146
volumes:
125147
- ./tests:/app/tests
126148
- ./utils:/app/utils
@@ -132,6 +154,7 @@ services:
132154
- FRONTEND_URL=http://test-frontend:5173
133155
- SLACK_API_URL=http://mock-slack-api:3000
134156
- OPENROUTER_API_URL=http://mock-openrouter-api:3000
157+
- AUTH_API_URL=http://mock-auth-api:3000
135158
- ENABLE_HTML_REPORTER=false
136159
command: ["./setup/wait-for-services.sh", "./run-tests.sh"]
137160

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM node:18-alpine
2+
3+
WORKDIR /app
4+
5+
COPY package.json .
6+
RUN npm install
7+
8+
COPY . .
9+
10+
EXPOSE 3000
11+
CMD ["node", "server.js"]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "auth-api-mock",
3+
"version": "1.0.0",
4+
"description": "Mock authentication service for E2E tests",
5+
"main": "server.js",
6+
"scripts": {
7+
"start": "node server.js"
8+
},
9+
"dependencies": {
10+
"express": "^4.18.2",
11+
"body-parser": "^1.20.2",
12+
"jsonwebtoken": "^9.0.2",
13+
"cors": "^2.8.5"
14+
}
15+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
const express = require('express');
2+
const bodyParser = require('body-parser');
3+
const jwt = require('jsonwebtoken');
4+
const cors = require('cors');
5+
6+
const app = express();
7+
const port = process.env.PORT || 3000;
8+
9+
app.use(bodyParser.json());
10+
app.use(cors({ origin: '*' }));
11+
12+
const JWT_SECRET = 'test-jwt-secret';
13+
const TOKEN_EXPIRY = '1h';
14+
15+
const users = {
16+
17+
id: '123e4567-e89b-12d3-a456-426614174000',
18+
19+
password: 'password123',
20+
name: 'Test User'
21+
}
22+
};
23+
24+
const sessions = {};
25+
26+
app.get('/health', (req, res) => {
27+
res.status(200).json({ status: 'ok' });
28+
});
29+
30+
31+
app.post('/auth/v1/token', (req, res) => {
32+
const { email, password } = req.body;
33+
34+
if (req.body.grant_type === 'refresh_token') {
35+
const { refresh_token } = req.body;
36+
37+
if (!refresh_token || !sessions[refresh_token]) {
38+
return res.status(401).json({ error: 'Invalid refresh token' });
39+
}
40+
41+
const user = sessions[refresh_token].user;
42+
const newTokens = generateTokens(user);
43+
44+
delete sessions[refresh_token];
45+
sessions[newTokens.refresh_token] = {
46+
user,
47+
access_token: newTokens.access_token
48+
};
49+
50+
return res.json({
51+
access_token: newTokens.access_token,
52+
refresh_token: newTokens.refresh_token,
53+
user: user,
54+
expires_in: 3600
55+
});
56+
}
57+
58+
if (!email || !password) {
59+
return res.status(400).json({ error: 'Email and password are required' });
60+
}
61+
62+
if (!users[email] || users[email].password !== password) {
63+
return res.status(401).json({ error: 'Invalid login credentials' });
64+
}
65+
66+
const user = { ...users[email] };
67+
delete user.password;
68+
69+
const tokens = generateTokens(user);
70+
71+
sessions[tokens.refresh_token] = {
72+
user,
73+
access_token: tokens.access_token
74+
};
75+
76+
res.json({
77+
access_token: tokens.access_token,
78+
refresh_token: tokens.refresh_token,
79+
user: user,
80+
expires_in: 3600
81+
});
82+
});
83+
84+
app.post('/auth/v1/signup', (req, res) => {
85+
const { email, password, name } = req.body;
86+
87+
if (!email || !password) {
88+
return res.status(400).json({ error: 'Email and password are required' });
89+
}
90+
91+
if (users[email]) {
92+
return res.status(400).json({ error: 'User already exists' });
93+
}
94+
95+
const userId = `user-${Date.now()}`;
96+
users[email] = {
97+
id: userId,
98+
email,
99+
password,
100+
name: name || email.split('@')[0]
101+
};
102+
103+
const user = { ...users[email] };
104+
delete user.password;
105+
106+
const tokens = generateTokens(user);
107+
108+
sessions[tokens.refresh_token] = {
109+
user,
110+
access_token: tokens.access_token
111+
};
112+
113+
res.json({
114+
access_token: tokens.access_token,
115+
refresh_token: tokens.refresh_token,
116+
user: user,
117+
expires_in: 3600
118+
});
119+
});
120+
121+
app.get('/auth/v1/user', (req, res) => {
122+
const authHeader = req.headers.authorization;
123+
124+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
125+
return res.status(401).json({ error: 'Missing or invalid token' });
126+
}
127+
128+
const token = authHeader.split(' ')[1];
129+
130+
try {
131+
const decoded = jwt.verify(token, JWT_SECRET);
132+
const userId = decoded.sub;
133+
134+
let user = null;
135+
for (const email in users) {
136+
if (users[email].id === userId) {
137+
user = { ...users[email] };
138+
delete user.password;
139+
break;
140+
}
141+
}
142+
143+
if (!user) {
144+
return res.status(404).json({ error: 'User not found' });
145+
}
146+
147+
res.json({ user });
148+
} catch (error) {
149+
console.error('Token verification error:', error);
150+
res.status(401).json({ error: 'Invalid token' });
151+
}
152+
});
153+
154+
app.post('/auth/v1/logout', (req, res) => {
155+
const authHeader = req.headers.authorization;
156+
157+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
158+
return res.status(401).json({ error: 'Missing or invalid token' });
159+
}
160+
161+
const token = authHeader.split(' ')[1];
162+
163+
for (const refreshToken in sessions) {
164+
if (sessions[refreshToken].access_token === token) {
165+
delete sessions[refreshToken];
166+
}
167+
}
168+
169+
res.json({ message: 'Signed out successfully' });
170+
});
171+
172+
function generateTokens(user) {
173+
const accessToken = jwt.sign({ sub: user.id, email: user.email }, JWT_SECRET, { expiresIn: TOKEN_EXPIRY });
174+
const refreshToken = `${user.id}-${Date.now()}`;
175+
176+
return {
177+
access_token: accessToken,
178+
refresh_token: refreshToken
179+
};
180+
}
181+
182+
app.post('/reset', (req, res) => {
183+
Object.keys(users).forEach(key => {
184+
if (key !== '[email protected]') {
185+
delete users[key];
186+
}
187+
});
188+
189+
Object.keys(sessions).forEach(key => {
190+
delete sessions[key];
191+
});
192+
193+
res.json({ status: 'ok', message: 'Data reset successful' });
194+
});
195+
196+
app.listen(port, () => {
197+
console.log(`Mock Auth API server running on port ${port}`);
198+
});

integration-tests/tests/e2e/auth.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { test, expect } from '../../utils/test-fixtures';
22

33
test.describe('Authentication Flow', () => {
4+
test.beforeEach(async ({ page, testData }) => {
5+
await testData.resetAuthData(page);
6+
});
47
test('should allow user to register with email', async ({ page, authHelper, testData }) => {
58
const testUser = testData.getTestUser();
69
const email = `register-${Date.now()}@example.com`;

0 commit comments

Comments
 (0)