mirror of
https://github.com/CompassConnections/Compass.git
synced 2025-12-23 22:18:43 -05:00
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:
committed by
GitHub
parent
348a557f5c
commit
ef7665c7da
@@ -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
|
||||
|
||||
27
.github/workflows/ci.yml
vendored
27
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -8,5 +8,14 @@
|
||||
"bucket": "compass-130ba-private",
|
||||
"rules": "private-storage.rules"
|
||||
}
|
||||
]
|
||||
],
|
||||
"emulators": {
|
||||
"auth": {
|
||||
"port": 9099
|
||||
},
|
||||
"ui": {
|
||||
"enabled": true,
|
||||
"port": 4000
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
});
|
||||
|
||||
@@ -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
16
scripts/setup-auth.ts
Normal 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();
|
||||
23
tests/e2e/web/SPEC_CONFIG.js
Normal file
23
tests/e2e/web/SPEC_CONFIG.js
Normal 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: '',
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export const config = {
|
||||
BASE_URL: 'http://localhost:3000',
|
||||
DEFAULT_LOGIN: 'defaultUser@dev.com',
|
||||
DEFAULT_PASSWORD: 'defaultPassword',
|
||||
};
|
||||
42
tests/e2e/web/fixtures/deleteUserFixture.ts
Normal file
42
tests/e2e/web/fixtures/deleteUserFixture.ts
Normal 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 },
|
||||
],
|
||||
});
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
21
tests/e2e/web/specs/signUp.spec.ts
Normal file
21
tests/e2e/web/specs/signUp.spec.ts
Normal 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');
|
||||
});
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user