Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth smoke tests #4673

Merged
merged 48 commits into from
Mar 15, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
fc59cb7
Add profile page generation
dac09 Mar 6, 2022
fd0acee
Fix profile page codemods
dac09 Mar 7, 2022
9ed479d
Merge branch 'main' of github.com:redwoodjs/redwood into feat/auth-ch…
dac09 Mar 7, 2022
71c8b98
Add codemod for profilePageTest so we check mockCurrentUser too
dac09 Mar 7, 2022
ee16ee4
Fix: stop interval when server is up
dac09 Mar 7, 2022
4779f1d
Use the same port for all workers
dac09 Mar 7, 2022
efd4d35
Add initial auth checks specc
dac09 Mar 7, 2022
b37dd81
Add authChecks with signup
dac09 Mar 8, 2022
428e567
Update test-project fixture
dac09 Mar 8, 2022
91d29e7
Merge branch 'main' of github.com:redwoodjs/redwood into feat/auth-ch…
dac09 Mar 8, 2022
dcc4c6b
Small tweaks to smoke test because we edit the title
dac09 Mar 8, 2022
883180d
Update authProvider to support string as role | Handle loading state …
dac09 Mar 10, 2022
90f35c3
Undo auth provider type change for now, handle it in separate PR
dac09 Mar 10, 2022
ace4b34
Add rbac checks
dac09 Mar 10, 2022
3ef5127
Update fixture
dac09 Mar 10, 2022
c517852
Re-enable admin checks (will cause failure)
dac09 Mar 10, 2022
1c1d0fe
Merge branch 'main' into feat/auth-checks-smoke-test
thedavidprice Mar 10, 2022
7f0c0e2
Merge branch 'main' into feat/auth-checks-smoke-test
thedavidprice Mar 10, 2022
1d30f45
Merge branch 'feat/auth-checks-smoke-test' of github.com:dac09/redwoo…
dac09 Mar 11, 2022
338e95f
Update fixture
dac09 Mar 11, 2022
c22755c
Update migrations
dac09 Mar 11, 2022
9a03437
Make the tests a little more resilient
dac09 Mar 12, 2022
2fe5e6d
Scaffold contacts in fixture, regenerate
dac09 Mar 13, 2022
05b0533
Add contacts scaffold to fixture
dac09 Mar 13, 2022
2594bd4
Change yarn.lock to clear workflow cache
dac09 Mar 13, 2022
dbf7273
Restore lockfile
dac09 Mar 13, 2022
085ebfc
Update tests to use contacts instead
dac09 Mar 13, 2022
f04ed82
Switch smoke test to check for blogpost body instead
dac09 Mar 13, 2022
5e1a12a
Merge branch 'main' of github.com:redwoodjs/redwood into feat/auth-ch…
dac09 Mar 13, 2022
8f66711
Fix auth secret generation on windows
dac09 Mar 14, 2022
46c3049
Set cwd for execa
dac09 Mar 14, 2022
92395db
Merge branch 'main' into feat/auth-checks-smoke-test
dac09 Mar 14, 2022
c7e155c
Regenerate fixture just to be sure
dac09 Mar 15, 2022
90b637c
Merge branch 'main' into feat/auth-checks-smoke-test
dac09 Mar 15, 2022
fd819cb
Ooops, forgot to commit RBAC checks
dac09 Mar 15, 2022
23ecbe3
Merge branch 'feat/auth-checks-smoke-test' of github.com:dac09/redwoo…
dac09 Mar 15, 2022
4dd0eb0
Use beforeAll instead of beforeEach in rbac
dac09 Mar 15, 2022
8bc2a61
Parallelize rbac signups
dac09 Mar 15, 2022
b69909b
Merge branch 'main' into feat/auth-checks-smoke-test
dac09 Mar 15, 2022
07bb80e
Update auth checks so that it actually verifies if the post has been …
dac09 Mar 15, 2022
e525796
Merge branch 'feat/auth-checks-smoke-test' of github.com:dac09/redwoo…
dac09 Mar 15, 2022
22392c6
dunno what happened with the auth selectors
dac09 Mar 15, 2022
59ceda7
Use first selector for article
dac09 Mar 15, 2022
123c6b2
Merge branch 'main' of github.com:redwoodjs/redwood into feat/auth-ch…
dac09 Mar 15, 2022
f561e06
Make it resilient on faster machines i.e. macs
dac09 Mar 15, 2022
e2dff3e
Merge branch 'main' into feat/auth-checks-smoke-test
thedavidprice Mar 15, 2022
e4dde2d
Merge branch 'main' into feat/auth-checks-smoke-test
thedavidprice Mar 15, 2022
5c077f7
update fixture api/src/functions/auth.ts
thedavidprice Mar 15, 2022
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
1 change: 0 additions & 1 deletion __fixtures__/test-project/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"recommendations": [
"redwoodjs.redwood",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
"ofhumanbondage.react-proptypes-intellisense",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('requireAuth directive', () => {
// If you want to set values in context, pass it through e.g.
// mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
const mockExecution = mockRedwoodDirective(requireAuth, {
context: { currentUser: { id: 1, roles: 'ADMIN' } },
context: { currentUser: { id: 1, roles: 'ADMIN', email: '[email protected]' } },
})

expect(mockExecution).not.toThrowError()
Expand Down
13 changes: 13 additions & 0 deletions __fixtures__/test-project/api/src/functions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ export const handler = async (event, context) => {
resetTokenExpiresAt: 'resetTokenExpiresAt',
},

// Specifies attributes on the cookie that dbAuth sets in order to remember
// who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
cookie: {
HttpOnly: true,
Path: '/',
SameSite: 'Strict',
Secure: true,

// If you need to allow other domains (besides the api side) access to
// the dbAuth session cookie:
// Domain: 'example.com',
},

forgotPassword: forgotPasswordOptions,
login: loginOptions,
resetPassword: resetPasswordOptions,
Expand Down
2 changes: 1 addition & 1 deletion __fixtures__/test-project/api/src/graphql/posts.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const schema = gql`
}

type Mutation {
createPost(input: CreatePostInput!): Post! @requireAuth
createPost(input: CreatePostInput!): Post! @requireAuth(roles: ["ADMIN"])
updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth
deletePost(id: Int!): Post! @requireAuth
}
Expand Down
2 changes: 1 addition & 1 deletion __fixtures__/test-project/api/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { db } from './db'
export const getCurrentUser = async (session) => {
return await db.user.findUnique({
where: { id: session.id },
select: { id: true, roles: true },
select: { id: true, roles: true, email: true },
})
}

Expand Down
10 changes: 5 additions & 5 deletions __fixtures__/test-project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
},
"devDependencies": {
"@redwoodjs/core": "0.49.0",
"autoprefixer": "10.4.2",
"postcss": "8.4.7",
"postcss-loader": "6.2.1",
"tailwindcss": "3.0.23"
"autoprefixer": "^10.4.2",
"postcss": "^8.4.8",
"postcss-loader": "^6.2.1",
"tailwindcss": "^3.0.23"
},
"eslintConfig": {
"extends": "@redwoodjs/eslint-config",
Expand All @@ -28,4 +28,4 @@
"scripts": {
"postinstall": ""
}
}
}
5 changes: 4 additions & 1 deletion __fixtures__/test-project/web/src/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// 'src/pages/HomePage/HomePage.js' -> HomePage
// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage

import { Router, Route, Set } from '@redwoodjs/router'
import { Router, Route, Private, Set } from '@redwoodjs/router'
import PostsLayout from 'src/layouts/PostsLayout'

import BlogLayout from 'src/layouts/BlogLayout'
Expand All @@ -26,6 +26,9 @@ const Routes = () => {
<Route path="/posts" page={PostPostsPage} name="posts" />
</Set>
<Set wrap={BlogLayout}>
<Private unauthenticated="login">
<Route path="/profile" page={ProfilePage} name="profile" />
</Private>
<Route path="/blog-post/{id:Int}" page={BlogPostPage} name="blogPost" />
<Route path="/contact" page={ContactPage} name="contact" />
<Route path="/about" page={AboutPage} name="about" prerender />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import ProfilePage from './ProfilePage'

export const generated = () => {
return <ProfilePage />
}

export default { title: 'Pages/ProfilePage' }
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { render, waitFor, screen } from '@redwoodjs/testing/web'

import ProfilePage from './ProfilePage'

describe('ProfilePage', () => {
it('renders successfully', async () => {
mockCurrentUser({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lets us verify that mockCurrentUser is working

email: '[email protected]',
id: 84849020,
roles: 'BAZINGA',
})

await waitFor(async () => {
expect(() => {
render(<ProfilePage />)
}).not.toThrow()
})

expect(await screen.findByText('[email protected]')).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useAuth } from '@redwoodjs/auth'
import { Link, routes } from '@redwoodjs/router'
import { MetaTags } from '@redwoodjs/web'

const ProfilePage = () => {
const { currentUser, isAuthenticated, hasRole, loading } = useAuth()

if (loading) {
return <p>Loading...</p>
}

return (
<>
<MetaTags title="Profile" description="Profile page" />

<h1 className="text-2xl">Profile</h1>

<table className="rw-table">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{Object.keys(currentUser).map((key) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just prints the keys from currentUser in useAuth.

return (
<tr key={key}>
<td>{key.toUpperCase()}</td>
<td>{currentUser[key]}</td>
</tr>
)
})}

<tr key="isAuthenticated">
<td>isAuthenticated</td>
<td>{JSON.stringify(isAuthenticated)}</td>
</tr>

<tr key="hasRole">
<td>Is Admin</td>
<td>{JSON.stringify(hasRole('ADMIN'))}</td>
</tr>
</tbody>
</table>
</>
)
}

export default ProfilePage
71 changes: 44 additions & 27 deletions tasks/smoke-test/playwright-fixtures/devServer.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,43 @@
/* eslint-disable no-empty-pattern */
import { test as base } from '@playwright/test'
import execa from 'execa'
import isPortReachable from 'is-port-reachable'

import { waitForServer } from '../util'

// Declare worker fixtures.
type DevServerFixtures = {
export type DevServerFixtures = {
webServerPort: number
apiServerPort: number
server: any
webUrl: string
}

// Note that we did not provide an test-scoped fixtures, so we pass {}.
const test = base.extend<any, DevServerFixtures>({
webServerPort: [
async ({}, use, workerInfo) => {
async ({}, use) => {
// "port" fixture uses a unique value of the worker process index.
await use(9000 + workerInfo.workerIndex)
await use(9000)
Copy link
Contributor Author

@dac09 dac09 Mar 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes here are because we actually don't want to spin up multiple dev servers (because our dev process regenerates some files)

This does not affect running tests in parallel though, because browser contexts are isolated

},
{ scope: 'worker' },
],
apiServerPort: [
async ({}, use, workerInfo) => {
// "port" fixture uses a unique value of the worker process index.
await use(9001 + workerInfo.workerIndex)
async ({}, use) => {
await use(9001)
},
{ scope: 'worker' },
],
webUrl: [
async ({ webServerPort }, use) => {
await use(`localhost:${webServerPort}`)
},
{ scope: 'worker' },
],

// "server" fixture starts automatically for every worker - we pass "auto" for that.
server: [
async ({ webServerPort, apiServerPort }, use) => {
console.log('Starting dev server.....')

const projectPath = process.env.PROJECT_PATH

if (!projectPath) {
Expand All @@ -41,28 +46,40 @@ const test = base.extend<any, DevServerFixtures>({
)
}

console.log(`Launching dev server at ${projectPath}`)
const isServerAlreadyUp = await isPortReachable(webServerPort, {
host: 'localhost',
})

// Don't wait for this to finish, because it doens't
const devServerHandler = execa.command(
`yarn rw dev --fwd="--no-open" --no-generate`,
{
cwd: projectPath,
shell: true,
env: {
WEB_DEV_PORT: webServerPort,
API_DEV_PORT: apiServerPort,
},
}
)
if (isServerAlreadyUp) {
console.log('Reusing server....')
console.log({
webServerPort,
apiServerPort,
})
} else {
console.log(`Launching dev server at ${projectPath}`)

// Pipe out logs so we can debug, when required
devServerHandler.stdout.on('data', (data) => {
console.log(
'[devServer-fixture] ',
Buffer.from(data, 'utf-8').toString()
// Don't wait for this to finish, because it doens't
const devServerHandler = execa.command(
`yarn rw dev --fwd="--no-open" --no-generate`,
{
cwd: projectPath,
shell: true,
env: {
WEB_DEV_PORT: webServerPort,
API_DEV_PORT: apiServerPort,
},
}
)
})

// Pipe out logs so we can debug, when required
devServerHandler.stdout.on('data', (data) => {
console.log(
'[devServer-fixture] ',
Buffer.from(data, 'utf-8').toString()
)
})
}

console.log('Waiting for dev servers.....')
await waitForServer(webServerPort, 1000)
Expand Down
7 changes: 7 additions & 0 deletions tasks/smoke-test/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import type { PlaywrightTestConfig } from '@playwright/test'
// See https://playwright.dev/docs/test-configuration#global-configuration
const config: PlaywrightTestConfig = {
timeout: 60_000,
// Leaving this here to make debugging easier, by uncommenting
// use: {
// launchOptions: {
// slowMo: 500,
// headless: false,
// },
// },
}

export default config
90 changes: 90 additions & 0 deletions tasks/smoke-test/tests/authChecks.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
PlaywrightTestArgs,
expect,
PlaywrightWorkerArgs,
} from '@playwright/test'
import execa from 'execa'
import fs from 'node:fs'
import path from 'node:path'

import devServerTest, {
DevServerFixtures,
} from '../playwright-fixtures/devServer.fixture'

import { loginAsTestUser, signUpTestUser } from './common'

// Signs up a user before these tests

devServerTest.beforeAll(async ({ browser }: PlaywrightWorkerArgs) => {
const page = await browser.newPage()

await signUpTestUser({
// @NOTE we can't access webUrl in beforeAll, so hardcoded
// But we can switch to beforeEach if required
webUrl: 'http://localhost:9000',
page,
})

await page.close()
})

devServerTest(
'useAuth hook, auth redirects checks',
async ({ page, webUrl }: PlaywrightTestArgs & DevServerFixtures) => {
await page.goto(`${webUrl}/profile`)

// To check redirects to the login page
await expect(page).toHaveURL(`http://${webUrl}/login?redirectTo=/profile`)

await loginAsTestUser({ page, webUrl })

await page.goto(`${webUrl}/profile`)

const usernameRow = await page.waitForSelector('*css=tr >> text=EMAIL')
await expect(await usernameRow.innerHTML()).toBe(
'<td>EMAIL</td><td>[email protected]</td>'
)

const isAuthenticatedRow = await page.waitForSelector(
'*css=tr >> text=isAuthenticated'
)
await expect(await isAuthenticatedRow.innerHTML()).toBe(
'<td>isAuthenticated</td><td>true</td>'
)

const isAdminRow = await page.waitForSelector('*css=tr >> text=Is Admin')
await expect(await isAdminRow.innerHTML()).toBe(
'<td>Is Admin</td><td>false</td>'
)
}
)

devServerTest('requireAuth graphql checks', async ({ page, webUrl }) => {
// Auth
await page.goto(`${webUrl}/posts/1/edit`)

await page.locator('input[name="title"]').fill('This is an edited title!')

// unAuthenticated
await page.click('text=SAVE')
await expect(
page.locator("text=You don't have permission to do that")
).toBeTruthy()

// Authenticated
await loginAsTestUser({ webUrl, page })
await page.goto(`${webUrl}/posts/1/edit`)
await page.locator('input[name="title"]').fill('This is an edited title!')

await Promise.all([
page.waitForNavigation({ url: '**/' }),
page.click('text=SAVE'),
])

// Log Out
await page.goto(`${webUrl}/`)
await page.click('text=Log Out')
await expect(page.locator('text=Login')).toBeTruthy()

await expect(page.locator('text=This is an edited title!')).toBeTruthy()
})
Loading