Add firebase emulator, Add registration script, Add signup spec (#22)

* add firebase emulator, add registration script, add signup spec

* Upgrade firebase emulator and make it pass the E2E tests

---------

Co-authored-by: MartinBraquet <martin.braquet@gmail.com>
This commit is contained in:
Nicholas Chamberlain
2025-12-06 14:43:46 -08:00
committed by GitHub
parent 348a557f5c
commit ef7665c7da
18 changed files with 2859 additions and 140 deletions

View File

@@ -1,3 +1,9 @@
# use firebase emulator for running e2e tests
NEXT_PUBLIC_FIREBASE_EMULATOR=false
FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099
FIREBASE_STORAGE_EMULATOR_HOST=127.0.0.1:9199
# You already have access to basic local functionality (UI, authentication, database read access).
# openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 -in backend/shared/src/googleApplicationCredentials-dev.json -out secrets/googleApplicationCredentials-dev.json.enc

View File

@@ -45,37 +45,18 @@ jobs:
# "common/coverage/lcov.info" \
# "web/coverage/lcov.info" \
# > coverage/lcov.info
# - name: Build app
# env:
# DATABASE_URL: ${{ secrets.DATABASE_URL }}
# run: npm run build
# Optional: Playwright E2E tests
- name: Install Playwright deps
run: |
npx playwright install --with-deps
npx playwright install chromium
# npx playwright install --with-deps
# npm install @playwright/test
- name: Run E2E tests
env:
NEXT_PUBLIC_API_URL: localhost:8088
NEXT_PUBLIC_FIREBASE_ENV: DEV
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_KEY }}
run: |
npx nyc --reporter=lcov yarn --cwd=web serve &
npx nyc --reporter=lcov yarn --cwd=backend/api dev &
npx wait-on http://localhost:3000
npx playwright test tests/e2e
SERVER_PID=$(fuser -k 3000/tcp)
echo $SERVER_PID
kill $SERVER_PID
SERVER_PID=$(fuser -k 8088/tcp)
echo $SERVER_PID
kill $SERVER_PID
chmod +x scripts/e2e.sh
./scripts/e2e.sh
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5

View File

@@ -76,6 +76,8 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
try {
return {kind: 'jwt', data: await auth.verifyIdToken(payload)}
} catch (err) {
const raw = payload.split(".")[0];
console.log("JWT header:", JSON.parse(Buffer.from(raw, "base64").toString()));
// This is somewhat suspicious, so get it into the firebase console
console.error('Error verifying Firebase JWT: ', err, scheme, payload)
throw new APIError(500, 'Error validating token.')

View File

@@ -1,15 +1,22 @@
import * as admin from 'firebase-admin'
import {getServiceAccountCredentials} from "shared/firebase-utils";
import {IS_LOCAL} from "common/hosting/constants";
import {IS_FIREBASE_EMULATOR} from "common/envs/constants";
// Locally initialize Firebase Admin.
export const initAdmin = () => {
if (IS_LOCAL && IS_FIREBASE_EMULATOR) {
console.log("Using Firebase Emulator Suite.")
return admin.initializeApp({
projectId: "compass-57c3c",
storageBucket: "compass-130ba-public",
})
}
if (IS_LOCAL) {
try {
const serviceAccount = getServiceAccountCredentials()
// console.debug(serviceAccount)
if (!serviceAccount.project_id) {
console.debug(`GOOGLE_APPLICATION_CREDENTIALS not set, skipping admin firebase init.`)
return
@@ -27,4 +34,4 @@ export const initAdmin = () => {
console.debug(`Initializing connection to default Firebase...`)
return admin.initializeApp()
}
}

View File

@@ -41,6 +41,9 @@ console.debug(`Running in ${HOSTING_ENV} (${ENV})`,);
// throw new MissingKeyError('firebaseConfig.apiKey')
// }
export const IS_FIREBASE_EMULATOR = process.env.NEXT_PUBLIC_FIREBASE_EMULATOR === "true"
if (IS_FIREBASE_EMULATOR) console.log("Using Firebase emulator.")
export const LOCAL_WEB_DOMAIN = `localhost:3000`
export const LOCAL_BACKEND_DOMAIN = `${IS_WEBVIEW_DEV_PHONE ? '192.168.1.3' : IS_LOCAL_ANDROID ? '10.0.2.2' : 'localhost'}:8088`

View File

@@ -8,5 +8,14 @@
"bucket": "compass-130ba-private",
"rules": "private-storage.rules"
}
]
],
"emulators": {
"auth": {
"port": 9099
},
"ui": {
"enabled": true,
"port": 4000
}
}
}

View File

@@ -29,7 +29,8 @@
"playwright:ui": "playwright test --ui",
"playwright:debug": "playwright test --debug",
"playwright:report": "npx playwright show-report tests/reports/playwright-report",
"postinstall": "./scripts/post_install.sh"
"postinstall": "./scripts/post_install.sh",
"emulate": "firebase emulators:start --only auth --project compass-57c3c"
},
"dependencies": {
"@capacitor/app": "7.1.0",
@@ -65,6 +66,7 @@
"eslint": "8.57.0",
"eslint-plugin-lodash": "^7.4.0",
"eslint-plugin-unused-imports": "4.1.4",
"firebase-tools": "^14.26.0",
"jest": "29.3.1",
"nodemon": "2.0.20",
"prettier": "3.6.2",

View File

@@ -1,5 +1,4 @@
import { defineConfig, devices } from '@playwright/test';
import path from 'path';
export default defineConfig({
testDir: './tests/e2e',
@@ -27,5 +26,6 @@ export default defineConfig({
// use: { ...devices['Desktop Safari'] },
// },
],
timeout: 60000,
});

View File

@@ -1,6 +1,21 @@
#!/bin/bash
# set -e
set -euo pipefail
# Function to clean up background processes
cleanup() {
echo "Stopping background processes..."
for pid in "${PIDS[@]:-}"; do
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" || true
wait "$pid" 2>/dev/null || true
echo "Killed PID $pid"
fi
done
}
# Trap EXIT, INT, TERM to run cleanup automatically
trap cleanup EXIT INT TERM
cd "$(dirname "$0")"/..
@@ -8,15 +23,20 @@ npx playwright install chromium
export NEXT_PUBLIC_API_URL=localhost:8088
export NEXT_PUBLIC_FIREBASE_ENV=DEV
export NEXT_PUBLIC_FIREBASE_EMULATOR=true
export FIREBASE_AUTH_EMULATOR_HOST=127.0.0.1:9099
export FIREBASE_STORAGE_EMULATOR_HOST=127.0.0.1:9199
# Start servers in background and store their PIDs
PIDS=()
npx nyc --reporter=lcov yarn --cwd=web serve & PIDS+=($!)
npx nyc --reporter=lcov yarn --cwd=backend/api dev & PIDS+=($!)
yarn emulate & PIDS+=($!)
npx nyc --reporter=lcov yarn --cwd=web serve &
npx nyc --reporter=lcov yarn --cwd=backend/api dev &
npx wait-on http://localhost:3000
npx playwright test tests/e2e --headed
SERVER_PID=$(fuser -k 3000/tcp)
echo $SERVER_PID
kill $SERVER_PID
SERVER_PID=$(fuser -k 8088/tcp)
echo $SERVER_PID
kill $SERVER_PID
npx tsx scripts/setup-auth.ts
npx playwright test tests/e2e
exit ${TEST_FAILED:-0}

16
scripts/setup-auth.ts Normal file
View File

@@ -0,0 +1,16 @@
import axios from 'axios';
import { config } from '../tests/e2e/web/SPEC_CONFIG.js';
async function createAuth() {
const base = 'http://localhost:9099/identitytoolkit.googleapis.com/v1';
await axios.post(`${base}/accounts:signUp?key=fake-api-key`, {
email: config.USERS.DEV_1.EMAIL,
password: config.USERS.DEV_1.PASSWORD,
returnSecureToken: true
});
console.log('Auth created', config.USERS.DEV_1.EMAIL)
}
createAuth();

View File

@@ -0,0 +1,23 @@
export const config = {
BASE_URL: 'http://localhost:3000',
USERS: {
DEV_1: {
EMAIL: 'dev_1@compass.com',
PASSWORD: 'dev_1Password',
},
DEV_2: {
EMAIL: 'dev_2@compass.com',
PASSWORD: 'dev_2Password',
},
SPEC: {
EMAIL: 'spec@compass.com',
PASSWORD: 'compassConnections1!',
},
SPEC_GOOGLE: {
EMAIL: 'compass.connections.test@gmail.com',
//unsure if gmail password should be public
PASSWORD: '',
}
},
};

View File

@@ -1,5 +0,0 @@
export const config = {
BASE_URL: 'http://localhost:3000',
DEFAULT_LOGIN: 'defaultUser@dev.com',
DEFAULT_PASSWORD: 'defaultPassword',
};

View File

@@ -0,0 +1,42 @@
import { test as base } from '@playwright/test';
import axios from 'axios';
import { config } from '../SPEC_CONFIG';
const baseUrl = 'http://localhost:9099/identitytoolkit.googleapis.com/v1';
async function deleteUser(email: string, password: string) {
try {
const login = await axios.post(
`${baseUrl}/accounts:signInWithPassword?key=fake-api-key`,
{
email,
password,
returnSecureToken: true
}
);
await axios.post(
`${baseUrl}/accounts:delete?key=fake-api-key`,
{ idToken: login.data.idToken }
);
} catch (err: any) {
console.log(err);
}
}
type CleanupFixtures = {
cleanupUsers: void;
};
export const test = base.extend<CleanupFixtures>({
cleanupUsers: [
async ({}, use) => {
// Run all tests first
await use();
//then delete users
await deleteUser(config.USERS.SPEC.EMAIL, config.USERS.SPEC.PASSWORD);
},
{ auto: true },
],
});

View File

@@ -1,21 +1,19 @@
import { test as base, Page, expect } from '@playwright/test';
import { SignInPage } from '../pages/signInPage';
import { config } from '../TESTING_CONFIG';
import { AuthPage } from '../pages/AuthPage';
import { config } from '../SPEC_CONFIG';
export const test = base.extend<{
authenticatedPage: Page;
}>({
authenticatedPage: async ({ page }, use) => {
const signInPage = new SignInPage(page);
const authPage = new AuthPage(page);
await page.goto('/signin');
await signInPage.fillEmailField(config.DEFAULT_LOGIN);
await signInPage.fillPasswprdField(config.DEFAULT_PASSWORD);
await signInPage.clickSignInWithEmailButton();
await authPage.fillEmailField(config.USERS.DEV_1.EMAIL);
await authPage.fillPasswordField(config.USERS.DEV_1.PASSWORD);
await authPage.clickSignInWithEmailButton();
await page.waitForLoadState('networkidle');
await page.waitForURL('/');
await page.waitForURL(/^(?!.*signin).*$/);
expect(page.url()).not.toContain('/signin')

View File

@@ -1,39 +1,51 @@
import { expect, Locator, Page } from '@playwright/test';
//sets up of all the functions that signin tests will use.
export class SignInPage{
export class AuthPage{
private readonly signInLink: Locator;
private readonly signUpButton: Locator;
private readonly emailField: Locator;
private readonly passwordField: Locator;
private readonly signInWithEmailButton: Locator;
private readonly signInWithGoogleButton: Locator;
private readonly signUpWithEmailButton: Locator;
constructor(public readonly page: Page) {
this.signInLink=page.getByRole('link', { name: 'Sign in' });
this.signUpButton=page.getByRole('button', {name: 'Sign up'});
this.emailField=page.getByLabel('Email');
this.passwordField=page.getByLabel('Password');
this.signInWithEmailButton=page.getByRole('button',{name: 'Sign in With Email'});
this.signInWithGoogleButton=page.getByRole('button',{name: 'Google'});
this.signInWithEmailButton=page.getByRole('button', {name: 'Sign in with Email'});
this.signInWithGoogleButton=page.getByRole('button', {name: 'Google'});
this.signUpWithEmailButton=page.getByRole('button', {name: 'Sign up with Email'});
}
async clickSignInText() {
async clickSignInLink() {
await this.signInLink.click();
}
async clickSignUpButton() {
await this.signUpButton.click();
}
async clickSignInWithEmailButton() {
await this.signInWithEmailButton.click();
}
async clickSignInWithEGoogleButton() {
async clickSignInWithGoogleButton() {
await this.signInWithGoogleButton.click();
}
async clickSignUpWithEmailButton() {
await this.signUpWithEmailButton.click();
}
async fillEmailField(email: string) {
await expect(this.emailField).toBeVisible();
await this.emailField.fill(email);
}
async fillPasswprdField(password: string) {
async fillPasswordField(password: string) {
await expect(this.passwordField).toBeVisible();
await this.passwordField.fill(password);
}

View File

@@ -0,0 +1,21 @@
import { expect } from '@playwright/test';
import { test } from '../fixtures/deleteUserFixture';
import { AuthPage } from '../pages/AuthPage';
import { config } from '../SPEC_CONFIG';
test('user can sign up with email + password', async ({ page }) => {
const auth = new AuthPage(page);
await page.goto('/');
await auth.clickSignUpButton();
await auth.fillEmailField(config.USERS.SPEC.EMAIL);
await auth.fillPasswordField(config.USERS.SPEC.PASSWORD);
await auth.clickSignUpWithEmailButton();
await page.waitForURL(/^(?!.*\/signup).*$/);
expect(page.url()).not.toContain('/signup');
});

View File

@@ -1,7 +1,7 @@
import {type User} from 'common/user'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import {getAuth, GoogleAuthProvider, signInWithCredential, signInWithPopup} from 'firebase/auth'
import {getAuth, GoogleAuthProvider, signInWithCredential, signInWithPopup, connectAuthEmulator} from 'firebase/auth'
import {safeLocalStorage} from '../util/local'
import {app} from './init'
@@ -9,6 +9,7 @@ import {GOOGLE_CLIENT_ID} from "common/constants"
import {isAndroidWebView} from "web/lib/util/webview"
import {SocialLogin} from "@capgo/capacitor-social-login"
import {Capacitor} from "@capacitor/core"
import {IS_FIREBASE_EMULATOR} from "common/envs/constants"
dayjs.extend(utc)
@@ -16,6 +17,12 @@ export type {User}
export const auth = getAuth(app)
if (IS_FIREBASE_EMULATOR) {
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true })
}
// console.log('auth:', auth)
export const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY'
// Scenarios:

2731
yarn.lock
View File

File diff suppressed because it is too large Load Diff