diff --git a/users/authservice/auth-model.js b/users/authservice/auth-model.js index 7763b51e..366f6ba8 100644 --- a/users/authservice/auth-model.js +++ b/users/authservice/auth-model.js @@ -3,6 +3,7 @@ const mongoose = require('mongoose'); const userSchema = new mongoose.Schema({ username: String, password: String, + profileImage: String, createdAt: Date, }); diff --git a/users/authservice/auth-service.js b/users/authservice/auth-service.js index 9764f088..38980783 100644 --- a/users/authservice/auth-service.js +++ b/users/authservice/auth-service.js @@ -39,7 +39,7 @@ app.post('/login', async (req, res) => { // Generate a JWT token const token = jwt.sign({ userId: user._id }, 'your-secret-key', { expiresIn: '1h' }); // Respond with the token and user information - res.json({ token: token, username: username, createdAt: user.createdAt }); + res.json({ token: token, username: username, createdAt: user.createdAt, profileImage: user.profileImage }); } else { res.status(401).json({ error: 'Invalid credentials' }); } diff --git a/users/userservice/user-model.js b/users/userservice/user-model.js index 71d81b5f..2d973b00 100644 --- a/users/userservice/user-model.js +++ b/users/userservice/user-model.js @@ -9,6 +9,10 @@ const userSchema = new mongoose.Schema({ type: String, required: true, }, + profileImage: { + type: String, + required: true + }, createdAt: { type: Date, default: Date.now, diff --git a/users/userservice/user-service.js b/users/userservice/user-service.js index be958427..9673690a 100644 --- a/users/userservice/user-service.js +++ b/users/userservice/user-service.js @@ -29,7 +29,7 @@ function validateRequiredFields(req, requiredFields) { app.post('/adduser', async (req, res) => { try { // Check if required fields are present in the request body - validateRequiredFields(req, ['username', 'password']); + validateRequiredFields(req, ['username', 'password', 'profileImage']); // Encrypt the password before saving it const hashedPassword = await bcrypt.hash(req.body.password, 10); @@ -37,6 +37,7 @@ app.post('/adduser', async (req, res) => { const newUser = new User({ username: req.body.username, password: hashedPassword, + profileImage: req.body.profileImage, }); await newUser.save(); diff --git a/users/userservice/user-service.test.js b/users/userservice/user-service.test.js index 8dd8ea16..97c24292 100644 --- a/users/userservice/user-service.test.js +++ b/users/userservice/user-service.test.js @@ -21,6 +21,7 @@ describe('User Service', () => { const newUser = { username: 'testuser', password: 'testpassword', + profileImage: 'perfil2.jpg', }; const response = await request(app).post('/adduser').send(newUser); diff --git a/webapp/src/assets/perfil2.jpg b/webapp/src/assets/perfil2.jpg new file mode 100644 index 00000000..204b0760 Binary files /dev/null and b/webapp/src/assets/perfil2.jpg differ diff --git a/webapp/src/assets/perfil3.jpg b/webapp/src/assets/perfil3.jpg new file mode 100644 index 00000000..dd4a1b77 Binary files /dev/null and b/webapp/src/assets/perfil3.jpg differ diff --git a/webapp/src/assets/perfil4.jpg b/webapp/src/assets/perfil4.jpg new file mode 100644 index 00000000..ebcf06e4 Binary files /dev/null and b/webapp/src/assets/perfil4.jpg differ diff --git a/webapp/src/assets/perfil5.jpg b/webapp/src/assets/perfil5.jpg new file mode 100644 index 00000000..618ce874 Binary files /dev/null and b/webapp/src/assets/perfil5.jpg differ diff --git a/webapp/src/components/AddUser.js b/webapp/src/components/AddUser.js index 00d522a2..eaa355cf 100644 --- a/webapp/src/components/AddUser.js +++ b/webapp/src/components/AddUser.js @@ -1,19 +1,33 @@ // src/components/AddUser.js import React, { useState } from 'react'; import axios from 'axios'; -import { Container, Typography, TextField, Button, Snackbar } from '@mui/material'; +import { Container, Typography, TextField, Button, Snackbar, IconButton } from '@mui/material'; + +import profileImg1 from '../assets/defaultImgProfile.jpg'; +import profileImg2 from '../assets/perfil2.jpg'; +import profileImg3 from '../assets/perfil3.jpg'; +import profileImg4 from '../assets/perfil4.jpg'; +import profileImg5 from '../assets/perfil5.jpg'; const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; const AddUser = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [profileImage, setProfileImage] = useState('defaultImgProfile.jpg'); + const [error, setError] = useState(''); const [openSnackbar, setOpenSnackbar] = useState(false); const addUser = async () => { try { - await axios.post(`${apiEndpoint}/adduser`, { username, password }); + if (password !== confirmPassword) { + setError('Passwords do not match'); + return; + } + + await axios.post(`${apiEndpoint}/adduser`, { username, password, profileImage }); setOpenSnackbar(true); } catch (error) { setError(error.response.data.error); @@ -24,8 +38,12 @@ const AddUser = () => { setOpenSnackbar(false); }; + const handleImageClick = (imageName) => { + setProfileImage(imageName); + } + return ( - + Add User @@ -46,6 +64,40 @@ const AddUser = () => { value={password} onChange={(e) => setPassword(e.target.value)} /> + setConfirmPassword(e.target.value)} + /> + + Select a profile image + +
+ handleImageClick('defaultImgProfile.jpg')}> + Imagen Perfil 1 + + handleImageClick('perfil2.jpg')}> + Imagen Perfil 2 + + handleImageClick('perfil3.jpg')}> + Imagen Perfil 3 + + handleImageClick('perfil4.jpg')}> + Imagen Perfil 4 + + handleImageClick('perfil5.jpg')}> + Imagen Perfil 5 + +
diff --git a/webapp/src/components/Login.js b/webapp/src/components/Login.js index 924807ca..45df07bd 100644 --- a/webapp/src/components/Login.js +++ b/webapp/src/components/Login.js @@ -10,12 +10,12 @@ const Login = ({ goTo }) => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); + const [profileImage, setProfileImage] = useState(''); const [error, setError] = useState(''); const [loginSuccess, setLoginSuccess] = useState(false); const [createdAt, setCreatedAt] = useState(''); const [openSnackbar, setOpenSnackbar] = useState(false); const [timeStart, setTimeStart] = useState(0); - const [timeElapsed, setTimeElapsed] = useState(0); const apiEndpoint = process.env.REACT_APP_API_ENDPOINT || 'http://localhost:8000'; @@ -24,26 +24,19 @@ const Login = ({ goTo }) => { const response = await axios.post(`${apiEndpoint}/login`, { username, password }); // Extract data from the response - const { createdAt: userCreatedAt, username: loggedInUsername } = response.data; + const { createdAt: userCreatedAt, username: loggedInUsername, profileImage: profileImage } = response.data; setTimeStart(Date.now()); setCreatedAt(userCreatedAt); setLoginSuccess(true); - saveSessionData({ username: loggedInUsername, createdAt: userCreatedAt }); + saveSessionData({ username: loggedInUsername, createdAt: userCreatedAt, profileImage: profileImage }); + console.log(profileImage+'\n\n\n'); setOpenSnackbar(true); } catch (error) { setError(error.response.data.error); } }; - const calculateTime = async () => { - try { - setTimeElapsed((Date.now() - timeStart) / 1000); - } catch (error) { - setError(error.response.data.error); - } - }; - const handleCloseSnackbar = () => { setOpenSnackbar(false); }; @@ -55,24 +48,7 @@ const Login = ({ goTo }) => { }, [loginSuccess, goTo]); return ( - - {loginSuccess ? ( - -
- - Hello {username}! - - - Your account was created on {new Date(createdAt).toLocaleDateString()}. - - - Han pasado {timeElapsed} segundos. - - -
- ) : ( +
Login :D @@ -100,7 +76,6 @@ const Login = ({ goTo }) => { setError('')} message={`Error: ${error}`} /> )}
- )}
); }; diff --git a/webapp/src/components/Nav.jsx b/webapp/src/components/Nav.jsx index 3ec83949..9c6c7d6d 100644 --- a/webapp/src/components/Nav.jsx +++ b/webapp/src/components/Nav.jsx @@ -15,12 +15,14 @@ import AdbIcon from '@mui/icons-material/Adb'; import { useContext } from 'react'; import { SessionContext } from '../SessionContext'; -import profileImage from '../assets/defaultImgProfile.jpg'; +import defaultProfileImg from '../assets/defaultImgProfile.jpg'; function Nav({ goTo }) { const { sessionData } = useContext(SessionContext); const username = sessionData ? sessionData.username : 'noUser'; + const profileImgSrc = sessionData && sessionData.profileImage ? + require(`../assets/${sessionData.profileImage}`) : defaultProfileImg; const [anchorElNav, setAnchorElNav] = React.useState(null); const [anchorElUser, setAnchorElUser] = React.useState(null); @@ -122,7 +124,7 @@ function Nav({ goTo }) { {username} - + - Welcome to the 2024 edition of the Software Architecture course + ASW - WIQ Quiz {showLogin ? goTo(x)} /> : } diff --git a/webapp/src/css/index.css b/webapp/src/css/index.css index 490f3690..0223ada2 100644 --- a/webapp/src/css/index.css +++ b/webapp/src/css/index.css @@ -143,6 +143,36 @@ button:focus-visible { vertical-align: middle; } +#fotosPerfil { + display: grid; + justify-content: center; + grid-template-columns: repeat(5, 1fr); + gap: 10px; + padding: 20px 0 20px 0; +} + +.fotoPerfilBtn { + border-radius: 0% !important; + border: none; + cursor: pointer; + width: 3.5em; + padding: 0 !important; +} + +.fotoPerfil { + background-color: #3498db; + color: #fff; + display: flex; + justify-content: center; + align-items: center; + width: 3.5em; + border-radius: 10%; +} + +.selectedImg { + border: 4px solid #FFF !important; +} + @media (prefers-color-scheme: light) { :root { color: #213547; diff --git a/webapp/src/test/AddUser.test.js b/webapp/src/test/AddUser.test.js index e585cf92..744c0bbe 100644 --- a/webapp/src/test/AddUser.test.js +++ b/webapp/src/test/AddUser.test.js @@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter'; import AddUser from '../components/AddUser'; const mockAxios = new MockAdapter(axios); +const handleImageClick = jest.fn(); describe('AddUser component', () => { beforeEach(() => { @@ -15,7 +16,8 @@ describe('AddUser component', () => { render(); const usernameInput = screen.getByLabelText(/Username/i); - const passwordInput = screen.getByLabelText(/Password/i); + const passwordInput = screen.getByLabelText("Password"); + const passwordConfirmInput = screen.getByLabelText(/Confirm/i); const addUserButton = screen.getByRole('button', { name: /Add User/i }); // Mock the axios.post request to simulate a successful response @@ -24,6 +26,7 @@ describe('AddUser component', () => { // Simulate user input fireEvent.change(usernameInput, { target: { value: 'testUser' } }); fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); + fireEvent.change(passwordConfirmInput, { target: { value: 'testPassword' } }); // Trigger the add user button click fireEvent.click(addUserButton); @@ -34,11 +37,48 @@ describe('AddUser component', () => { }); }); + it('passwords dont match', async () => { + render(); + + await waitFor(() => { + //const imageDiv = screen.getByTestId('fotosPerfil'); + //expect(imageDiv).toBeInTheDocument(); + + const iconButtonElements = screen.getAllByRole('button', { className: /fotoPerfilBtn/i }); + expect(iconButtonElements.length).toBeGreaterThan(4); + + const iconElements = screen.getAllByRole('img', { className: "fotoPerfil" }); + expect(iconElements.length).toBeGreaterThan(4); + }); + + const usernameInput = screen.getByLabelText(/Username/i); + const passwordInput = screen.getByLabelText("Password"); + const passwordConfirmInput = screen.getByLabelText(/Confirm/i); + const addUserButton = screen.getByRole('button', { name: /Add User/i }); + + // Mock the axios.post request to simulate a successful response + mockAxios.onPost('http://localhost:8000/adduser').reply(200); + + // Simulate user input + fireEvent.change(usernameInput, { target: { value: 'testUser' } }); + fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); + fireEvent.change(passwordConfirmInput, { target: { value: 'randomPass' } }); + + // Trigger the add user button click + fireEvent.click(addUserButton); + + // Wait for the Snackbar to be open + await waitFor(() => { + expect(screen.getByText(/Passwords do not match/i)).toBeInTheDocument(); + }); + }); + it('should handle error when adding user', async () => { render(); const usernameInput = screen.getByLabelText(/Username/i); - const passwordInput = screen.getByLabelText(/Password/i); + const passwordInput = screen.getByLabelText("Password"); + const passwordConfirmInput = screen.getByLabelText(/Confirm/i); const addUserButton = screen.getByRole('button', { name: /Add User/i }); // Mock the axios.post request to simulate an error response @@ -47,6 +87,7 @@ describe('AddUser component', () => { // Simulate user input fireEvent.change(usernameInput, { target: { value: 'testUser' } }); fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); + fireEvent.change(passwordConfirmInput, { target: { value: 'testPassword' } }); // Trigger the add user button click fireEvent.click(addUserButton); @@ -56,4 +97,23 @@ describe('AddUser component', () => { expect(screen.getByText(/Error: Internal Server Error/i)).toBeInTheDocument(); }); }); + + test('selección de imagen de perfil', async () => { + const { getByAltText } = render(); + + // Encuentra los botones de imagen de perfil + const button1 = getByAltText('Imagen Perfil 1'); + const button2 = getByAltText('Imagen Perfil 2'); + const button3 = getByAltText('Imagen Perfil 3'); + const button4 = getByAltText('Imagen Perfil 4'); + const button5 = getByAltText('Imagen Perfil 5'); + + // Simula hacer clic en cada botón + fireEvent.click(button1); + fireEvent.click(button2); + fireEvent.click(button3); + fireEvent.click(button4); + fireEvent.click(button5); + + }); }); diff --git a/webapp/src/test/App.test.js b/webapp/src/test/App.test.js index 6a4cfb52..249e9a96 100644 --- a/webapp/src/test/App.test.js +++ b/webapp/src/test/App.test.js @@ -1,6 +1,10 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, act, fireEvent } from '@testing-library/react'; import App from '../App'; import { SessionProvider } from '../SessionContext'; +import MockAdapter from 'axios-mock-adapter'; +import axios from 'axios'; + +const mockAxios = new MockAdapter(axios); test('renders learn react link', () => { render( @@ -9,6 +13,39 @@ test('renders learn react link', () => { ); - const linkElement = screen.getByText(/Welcome to the 2024 edition of the Software Architecture course/i); + const linkElement = screen.getByText(/ASW - WIQ Quiz/i); expect(linkElement).toBeInTheDocument(); }); + +it('cambia de estado de menú al hacer clic en los botones', async () => { + const { getByText } = render(); + + const usernameInput = screen.getByLabelText(/Username/i); + const passwordInput = screen.getByLabelText(/Password/i); + const loginButton = screen.getByRole('button', { name: /Login/i }); + + mockAxios.onPost('http://localhost:8000/login').reply(200, { createdAt: '2024-01-01T12:34:56Z' }); + + await act(async () => { + fireEvent.change(usernameInput, { target: { value: 'testUser' } }); + fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); + fireEvent.click(loginButton); + }); + + const nav = screen.getByText(/ASW WIQ/i); + expect(nav).toBeInTheDocument(); + fireEvent.click(nav); + + // Encuentra los botones de navegación + const gameButton = getByText('Play'); + const participationButton = getByText('Participation'); + + // Comprueba que solo el componente User está visible inicialmente + expect(gameButton).toBeInTheDocument(); + expect(participationButton).toBeInTheDocument(); + + // Simula hacer clic en el botón Start y comprueba que se cambia al componente Start + fireEvent.click(gameButton); + expect(gameButton).not.toBeInTheDocument(); + expect(participationButton).not.toBeInTheDocument(); +}); diff --git a/webapp/src/test/Login.test.js b/webapp/src/test/Login.test.js index 12486cc3..2c5021ef 100644 --- a/webapp/src/test/Login.test.js +++ b/webapp/src/test/Login.test.js @@ -37,10 +37,6 @@ describe('Login component', () => { fireEvent.change(passwordInput, { target: { value: 'testPassword' } }); fireEvent.click(loginButton); }); - - // Verify that the user information is displayed - expect(screen.getByText(/Hello testUser!/i)).toBeInTheDocument(); - expect(screen.getByText(/Your account was created on 1\/1\/2024/i)).toBeInTheDocument(); }); it('should handle error when logging in', async () => { diff --git a/webapp/src/test/User.test.js b/webapp/src/test/User.test.js index c4cfb7e7..75ecdff4 100644 --- a/webapp/src/test/User.test.js +++ b/webapp/src/test/User.test.js @@ -12,7 +12,7 @@ describe('User component', () => { ); // Verificar que el texto de bienvenida se muestra correctamente - const welcomeText = screen.getByText(/Welcome to the 2024 edition of the Software Architecture course/i); + const welcomeText = screen.getByText(/ASW - WIQ Quiz/i); expect(welcomeText).toBeInTheDocument(); const welcomeText2 = screen.getByText(/Login :D/i);