Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions .github/workflows/deploy_note_app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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 }}
Expand All @@ -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 }}"
--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 }}"
35 changes: 29 additions & 6 deletions part2/notes_frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -102,11 +118,18 @@ const App = () => {

<Notification message={errorMessage} />
{user === null ?
<Togglable buttonLabel="login">
<LoginForm
loginApp={loginApp}
/>
</Togglable>
<div>
<Togglable buttonLabel="login">
<LoginForm
loginApp={loginApp}
/>
</Togglable>
<Togglable buttonLabel="sign up">
<SignUpForm
signUpApp={signUpApp}
/>
</Togglable>
</div>
: <>
<p>{user.username} logged-in</p> <button onClick={logoutApp}>logout</button>
<br />
Expand Down
69 changes: 68 additions & 1 deletion part2/notes_frontend/src/components/Forms.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="max-w-md mx-auto bg-white p-6 mb-5 rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-4">Sign Up</h2>
<form className="flex flex-col gap-4" onSubmit={handleSignUp}>
<div>
<label className="block text-gray-700 mb-2 font-semibold">name</label>
<input
data-testid="name"
type="text"
value={name}
name="name"
onChange={({ target }) => 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"
/>
</div>
<div>
<label className="block text-gray-700 mb-2 font-semibold">username</label>
<input
data-testid="username-signup"
type="text"
value={username}
name="username"
onChange={({ target }) => setUsername(target.value)}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
placeholder="Enter username"
/>
</div>
<div>
<label className="block text-gray-700 mb-2 font-semibold">password</label>
<input
data-testid="password-signup"
type="password"
value={password}
name="password"
onChange={({ target }) => setPassword(target.value)}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
placeholder="Enter password"
/>
</div>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md transition">
Sign Up
</button>
</form>
</div>
)
}

const NoteForm = ({ createNote }) => {
const [newNote, setNewNote] = useState('')

Expand Down Expand Up @@ -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 }
9 changes: 9 additions & 0 deletions part2/notes_frontend/src/services/users.js
Original file line number Diff line number Diff line change
@@ -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 }
19 changes: 18 additions & 1 deletion part2/notes_frontend/tests/note_app.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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()
Expand Down
19 changes: 19 additions & 0 deletions part3/notes_fullstack/tests/note_app.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,32 @@ 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')

const errDiv = await page.locator('.error')
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')
Expand Down
Loading