From db9c9768161e2265464e719b28345ed69b830518 Mon Sep 17 00:00:00 2001 From: Fred Adeniyi Date: Wed, 23 Jul 2025 11:55:00 +0800 Subject: [PATCH] feat: Implement user sign-up functionality and add corresponding tests --- .github/workflows/deploy_note_app.yml | 41 ++++++++--- part2/notes_frontend/src/App.jsx | 35 ++++++++-- part2/notes_frontend/src/components/Forms.jsx | 69 ++++++++++++++++++- part2/notes_frontend/src/services/users.js | 9 +++ part2/notes_frontend/tests/note_app.spec.js | 19 ++++- part3/notes_fullstack/tests/note_app.spec.js | 19 +++++ 6 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 part2/notes_frontend/src/services/users.js diff --git a/.github/workflows/deploy_note_app.yml b/.github/workflows/deploy_note_app.yml index ab3a86b..48b928e 100644 --- a/.github/workflows/deploy_note_app.yml +++ b/.github/workflows/deploy_note_app.yml @@ -20,10 +20,10 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v4.1.7 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v4.0.3 with: node-version: 20 cache: 'npm' @@ -71,16 +71,16 @@ jobs: SECRET: ${{ secrets.SECRET }} - name: Upload Playwright report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.3.4 if: always() with: name: playwright-report path: part3/notes_fullstack/playwright-report/ - retention-days: 30 + retention-days: 10 - name: Upload build artifact for deployment if: github.event_name == 'push' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v4.3.4 with: name: production-build path: part3/notes_fullstack/dist @@ -95,17 +95,17 @@ jobs: id-token: 'write' # Required for Workload Identity Federation steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v4.1.7 - name: Download build artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v4.1.8 with: name: production-build path: part3/notes_fullstack/dist - name: Authenticate to Google Cloud id: auth - uses: 'google-github-actions/auth@v2' + uses: 'google-github-actions/auth@v2.1.3' with: workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }} service_account: ${{ secrets.GCP_SA_EMAIL }} @@ -124,4 +124,27 @@ jobs: --image ${{ env.GAR_LOCATION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.SERVICE_NAME }}/${{ env.SERVICE_NAME }}:${{ github.sha }} \ --region ${{ env.REGION }} \ --allow-unauthenticated \ - --set-env-vars "MONGODB_URI=${{ secrets.MONGODB_URI }},SECRET=${{ secrets.SECRET }}" \ No newline at end of file + --set-env-vars "MONGODB_URI=${{ secrets.MONGODB_URI }},SECRET=${{ secrets.SECRET }}" + + tag_release: + name: Tag Release + needs: [deploy] + runs-on: ubuntu-24.04 + permissions: + contents: 'write' # Required to push new tags + # Skip tagging if the commit message contains #skip for push events on the target branch + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '#skip') }} + steps: + - name: Checkout repository + uses: actions/checkout@v4.1.7 + + - name: Bump version and push tag + id: tag_version + uses: anothrNick/github-tag-action@1.73.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_PREFIX: v + DEFAULT_BUMP: patch + + - name: Log new tag + run: echo "New tag is ${{ steps.tag_version.outputs.tag }}" \ No newline at end of file diff --git a/part2/notes_frontend/src/App.jsx b/part2/notes_frontend/src/App.jsx index 5574e2d..3a3139b 100644 --- a/part2/notes_frontend/src/App.jsx +++ b/part2/notes_frontend/src/App.jsx @@ -2,11 +2,12 @@ import { useState, useEffect, useRef } from 'react' // Services import noteService from './services/notes' import loginService from './services/login' +import userService from './services/users' // Ui components import Notification from './components/Notification' import Footer from './components/Footer' -import { LoginForm, NoteForm } from './components/Forms' +import { LoginForm, NoteForm, SignUpForm } from './components/Forms' import NotesList from './components/NoteList' import Togglable from './components/Togglable' @@ -59,6 +60,21 @@ const App = () => { } } + const signUpApp = async (userObject) => { + try { + await userService.create(userObject) + setErrorMessage(`User ${userObject.username} created successfully! Please log in.`) + setTimeout(() => { + setErrorMessage(null) + }, 5000) + } catch (error) { + setErrorMessage(error.response.data.error) + setTimeout(() => { + setErrorMessage(null) + }, 5000) + } + } + const toggleImportanceOf = (id) => { const note = notes.find(n => n.id === id) const changedNote = { ...note, important: !note.important } @@ -102,11 +118,18 @@ const App = () => { {user === null ? - - - +
+ + + + + + +
: <>

{user.username} logged-in


diff --git a/part2/notes_frontend/src/components/Forms.jsx b/part2/notes_frontend/src/components/Forms.jsx index b36688d..def6662 100644 --- a/part2/notes_frontend/src/components/Forms.jsx +++ b/part2/notes_frontend/src/components/Forms.jsx @@ -48,6 +48,69 @@ const LoginForm = ({ loginApp }) => { ) } +const SignUpForm = ({ signUpApp }) => { + const [name, setName] = useState('') + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + + const handleSignUp = (event) => { + event.preventDefault() + signUpApp({ name, username, password }) + setName('') + setUsername('') + setPassword('') + } + + return ( +
+

Sign Up

+
+
+ + setName(target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500" + placeholder="Enter your name" + /> +
+
+ + setUsername(target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500" + placeholder="Enter username" + /> +
+
+ + setPassword(target.value)} + className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500" + placeholder="Enter password" + /> +
+ +
+
+ ) +} + const NoteForm = ({ createNote }) => { const [newNote, setNewNote] = useState('') @@ -91,8 +154,12 @@ LoginForm.propTypes = { loginApp: PropTypes.func.isRequired, } +SignUpForm.propTypes = { + signUpApp: PropTypes.func.isRequired, +} + NoteForm.propTypes = { createNote: PropTypes.func.isRequired, } -export { LoginForm, NoteForm } +export { LoginForm, NoteForm, SignUpForm } diff --git a/part2/notes_frontend/src/services/users.js b/part2/notes_frontend/src/services/users.js new file mode 100644 index 0000000..a850388 --- /dev/null +++ b/part2/notes_frontend/src/services/users.js @@ -0,0 +1,9 @@ +import axios from 'axios' +const baseUrl = '/api/users' + +const create = async (newObject) => { + const response = await axios.post(baseUrl, newObject) + return response.data +} + +export default { create } \ No newline at end of file diff --git a/part2/notes_frontend/tests/note_app.spec.js b/part2/notes_frontend/tests/note_app.spec.js index d27098a..4c354bc 100644 --- a/part2/notes_frontend/tests/note_app.spec.js +++ b/part2/notes_frontend/tests/note_app.spec.js @@ -32,6 +32,23 @@ test.describe('Note App', () => { await expect(errDiv).toContainText('wrong credentials') await expect(page.getByText('mluukkai logged-in')).not.toBeVisible() }) + + test('a new user can sign up and log in', async ({ page }) => { + await page.getByRole('button', { name: 'sign up' }).click() + + await page.getByTestId('name').fill('Test User') + await page.getByTestId('username-signup').fill('testuser') + await page.getByTestId('password-signup').fill('password') + + await page.getByRole('button', { name: 'Sign Up' }).click() + + const successNotification = await page.locator('.error') + await expect(successNotification).toContainText('User testuser created successfully! Please log in.') + await expect(successNotification).toBeVisible() + + await helpers.loginWith(page, 'testuser', 'password') + await expect(page.getByText('testuser logged-in')).toBeVisible() + }) test.describe('when logged in', () => { test.beforeEach(async ({ page }) => { await helpers.loginWith(page, 'mluukkai', 'salainen') @@ -45,7 +62,7 @@ test.describe('Note App', () => { test.beforeEach(async ({ page }) => { await helpers.createNote(page, 'first note by Playwright') await helpers.createNote(page, 'second note by Playwright') - await helpers.createNote(page, 'another note by Playwright') + // await helpers.createNote(page, 'another note by Playwright') }) test('importance can be changed', async ({ page }) => { await page.getByRole('button', { name: 'make not important' }).click() diff --git a/part3/notes_fullstack/tests/note_app.spec.js b/part3/notes_fullstack/tests/note_app.spec.js index dc9f802..d86fb38 100644 --- a/part3/notes_fullstack/tests/note_app.spec.js +++ b/part3/notes_fullstack/tests/note_app.spec.js @@ -25,6 +25,7 @@ test.describe('Note App', () => { await helpers.loginWith(page, 'mluukkai', 'salainen') await expect(page.getByText('mluukkai logged-in')).toBeVisible() }) + test('login fails with wrong password', async ({ page }) => { await helpers.loginWith(page, 'mluukkai', 'wrong') @@ -32,6 +33,24 @@ test.describe('Note App', () => { await expect(errDiv).toContainText('wrong credentials') await expect(page.getByText('mluukkai logged-in')).not.toBeVisible() }) + + test('a new user can sign up and log in', async ({ page }) => { + await page.getByRole('button', { name: 'sign up' }).click() + + await page.getByTestId('name').fill('Test User') + await page.getByTestId('username-signup').fill('testuser') + await page.getByTestId('password-signup').fill('password') + + await page.getByRole('button', { name: 'Sign Up' }).click() + + const successNotification = await page.locator('.error') + await expect(successNotification).toContainText('User testuser created successfully! Please log in.') + await expect(successNotification).toBeVisible() + + await helpers.loginWith(page, 'testuser', 'password') + await expect(page.getByText('testuser logged-in')).toBeVisible() + }) + test.describe('when logged in', () => { test.beforeEach(async ({ page }) => { await helpers.loginWith(page, 'mluukkai', 'salainen')