diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..acf504b --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,83 @@ +name: End-to-End Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + e2e-testing: + timeout-minutes: 60 + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.19.4' + + - name: Install Dependencies + run: | + npm install -g wait-on + npm ci + cd client && npm ci + cd ../server && npm ci + + - name: Install Playwright Browsers + run: npx playwright install --with-deps + + - name: Start Express Server + env: + SUPABASE_URL: ${{ secrets.TEST_SUPABASE_URL }} + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.TEST_SUPABASE_SERVICE_ROLE_KEY }} + PORT: 5000 + run: | + cd server + npm run start:ci-test & + wait-on http://localhost:5000 + + - name: Start React Client + env: + REACT_APP_SUPABASE_URL: ${{ secrets.TEST_SUPABASE_URL }} + REACT_APP_SUPABASE_ANON_KEY: ${{ secrets.TEST_SUPABASE_ANON_KEY }} + PORT: 3000 + run: | + cd client + npm run start:ci-test & + wait-on http://localhost:3000 + + - name: Run Playwright Tests + run: npx playwright test + env: + SUPABASE_URL: ${{ secrets.TEST_SUPABASE_URL }} + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.TEST_SUPABASE_SERVICE_ROLE_KEY }} + + - name: Upload HTML report to Azure + if: always() + shell: bash + run: | + # Creates a unique folder name for this specific test run + REPORT_DIR='run-${{ github.run_id }}-${{ github.run_attempt }}' + + # Uploads the report to the $web container + azcopy cp --recursive "./playwright-report/*" "https://kintree2026.blob.core.windows.net/\$web/$REPORT_DIR" + + # Prints a clickable link in the GitHub Actions logs + echo "::notice title=HTML report url::https://kintree2026.z13.web.core.windows.net/$REPORT_DIR/index.html" + env: + AZCOPY_AUTO_LOGIN_TYPE: SPN + AZCOPY_SPA_APPLICATION_ID: '${{ secrets.AZCOPY_SPA_APPLICATION_ID }}' + AZCOPY_SPA_CLIENT_SECRET: '${{ secrets.AZCOPY_SPA_CLIENT_SECRET }}' + AZCOPY_TENANT_ID: '${{ secrets.AZCOPY_TENANT_ID }}' + + - name: Upload Playwright Report to GitHub Artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index ff049cf..f660cd3 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,7 @@ web_modules/ .env .env.development.local .env.test.local +.env.test .env.production.local .env.local @@ -138,3 +139,10 @@ dist .yarn/install-state.gz .pnp.* client/src/components/AddToTree/sample-data.json + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/README.md b/README.md index 042658d..752ad01 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,35 @@ Then, from the same directory, run the following command to run the server/API: `node server.js` +### Testing + +This project uses a separate test database to run tests against. Therefore, make sure to paste in the proper env variables. + +Paste `TEST_SUPABASE_URL` and `TEST_SUPABASE_SERVICE_ROLE_KEY` into a .env.test file in the root directory of the project. + +Additionally, add `REACT_APP_SUPABASE_URL` and `REACT_APP_SUPABASE_ANON_KEY` to a .env.test file in the /client/ directory. + +Finally, `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` should be added to the .env.test file in the /server/ directory as well. + +Tree structure: +``` +/SeniorProject_KinTree + /client + .env.test + /server + .env.test + .env.test +``` + +Move into the /server/ directory and run `npm run start:dev-test` to setup the auth storage and test environment. + +Cd into the /client/ directory and run `npm run start:dev-test` to start the frontend in test mode. + +Then, navigate back to the /SeniorProject_KinTree/ root directory and run: + +`npm run test` ### Emails setup Follow the steps on [sending emails with Resend](https://github.com/resend/resend-examples/tree/main/express-resend-examples) to verify your domain and obtain your RESEND_API_KEY to store in `.env`. -Set EMAIL_FROM in `.env`. \ No newline at end of file +Set EMAIL_FROM in `.env`. diff --git a/TESTPLAN.md b/TESTPLAN.md new file mode 100644 index 0000000..de37aae --- /dev/null +++ b/TESTPLAN.md @@ -0,0 +1,429 @@ +# KinTree End-to-End Test Plan + +## 1. Purpose +This plan documents the primary user flows in KinTree and defines how each flow should be tested. It is designed to guide Playwright UI tests plus supporting API/integration checks. + +## 2. Scope +Application areas covered: +- Authentication and account lifecycle +- Profile management and account security +- Family member management and relationship linking +- Tree visualization and shared tree workflows +- Family event dashboard +- Settings, help, and chat pages + +## 3. Test Environments +- Client app: http://localhost:3000 +- Server API: http://localhost:5000 +- Auth/storage provider: Supabase + +Baseline test users: +- User A: existing registered account with at least one family member +- User B: existing registered account not yet linked to User A +- User C: account with shared trees received +- User D: account with MFA enabled + +## 4. Flow Catalog + +### Flow 1: Register a New Account + +Routes: +- /register +- /login + +APIs touched: +- POST /api/auth/sync + +Steps: +1. Open /register. +2. Fill required fields with valid values. +3. Submit Create Account. +4. Verify redirect to /login. + +Expected results: +- Registration succeeds and account metadata is persisted. +- Login page loads after registration. + +Negative coverage: +- Invalid email format rejected. +- Duplicate email returns visible error. + +### Flow 2: Login with Email and Password + +Routes: +- /login + +Auth behavior: +- Optional remember-me (stores email in local storage) +- Optional MFA challenge for users with verified TOTP + +Steps: +1. Open /login. +2. Enter valid credentials. +3. Submit Sign In. + +Expected results: +- Successful users land on /. +- 'Remember me' restores saved email on next login screen load. + +Negative coverage: +- Invalid credentials show error text. +- Protected routes redirect to /login if not authenticated. + +### Flow 3: Login with MFA (TOTP) + +Routes: +- /login + +Auth behavior: +- If a verified TOTP factor exists, user must verify a 6-digit code. + +Steps: +1. Login with valid username/password for MFA-enabled account. +2. Verify MFA challenge UI appears. +3. Enter valid TOTP code and submit. + +Expected results: +- User is redirected to / after successful verification. + +Negative coverage: +- Invalid code shows verification error and remains on MFA step. + +### Flow 4: Password Reset and Update Password + +Routes: +- /reset-password +- /update-password +- /login + +Steps: +1. Open /reset-password and submit a valid email. +2. Follow reset link to /update-password. +3. Enter a valid new password and submit. +4. Re-authenticate at /login with new password. + +Expected results: +- Reset request shows success message. +- Password update succeeds and redirects to /login. +- New password works for login. + +### Flow 5: View Own Profile Data + +Routes: +- /account/:id (own id) + +APIs touched: +- GET /api/auth/user/email/:email +- GET /api/auth/user/:id + +Steps: +1. Navigate to own account page. +2. Verify profile, personal info, and address sections render. + +Expected results: +- Profile fields are populated from database values. +- Page loads without fallback errors. + +### Flow 6: Edit Own Profile and Persist Changes + +Routes: +- /account/:id + +APIs touched: +- PUT /api/auth/profile +- POST /api/auth/upload-profile-picture + +Steps: +1. Enter edit mode for profile/personal/address sections. +2. Update fields and save. +3. Refresh page. + +Expected results: +- Saved values persist after refresh. +- Success feedback is shown. + +Negative coverage: +- Oversized image upload rejected. +- Non-image upload rejected. + +### Flow 7: Manage Account Security (MFA + Email Verification) + +Routes: +- /account/:id (own account) + +Steps: +1. Start authenticator setup. +2. Scan QR or use pending setup and enter TOTP code. +3. Verify authenticator status becomes enabled. +4. Disable authenticator and verify status toggles. +5. If email unverified, trigger resend verification email. + +Expected results: +- MFA state transitions are correct and visible. +- Verification resend returns success status. + +### Flow 8: Delete Account + +Routes: +- /account/:id +- /login + +APIs touched: +- DELETE /api/auth/remove/:id + +Steps: +1. Open own account page. +2. Click Delete account. +3. Confirm deletion in modal. + +Expected results: +- Account and related data are removed. +- User is logged out and redirected to /login. + +Negative coverage: +- Backend failure surfaces user-visible error and keeps modal flow recoverable. + +### Flow 9: View Family Directory and Open Member Profile + +Routes: +- /family +- /account/:id + +APIs touched: +- GET /api/family-members/user/:userId + +Steps: +1. Open /family. +2. Search for a member by first/last name. +3. Sort list and verify ordering. +4. Open member profile with View link. + +Expected results: +- Matching members appear in list. +- Navigation lands on target member profile. + +### Flow 10: Add Existing User as Family Member + +Routes: +- /tree (Add Family Member popup) +- /account/:id + +APIs touched: +- GET /api/family-members/user/:userId +- GET /api/auth/users +- GET /api/family-members/active/:id +- POST /api/family-members +- POST /api/relationships + +Steps: +1. Open tree page and launch Add Family Member popup. +2. Search and select an existing registered user. +3. Choose relationship type (and maternal/paternal side when required). +4. Submit add. + +Expected results: +- New tree member record is created. +- Relationship record is created. +- User is redirected to selected member profile. + +Negative coverage: +- Cannot add self. +- Cannot add a duplicate existing family member. + +### Flow 11: Add Manual (Non-Registered) Family Member + +Routes: +- /tree (Add Family Member popup) +- /account/:id + +APIs touched: +- GET /api/family-members/active/:id +- POST /api/family-members +- POST /api/relationships + +Steps: +1. In Add Family Member popup, switch to manual mode. +2. Enter member details and relationship. +3. Submit add. + +Expected results: +- Member is created with no linked user account. +- Relationship is created. +- User can edit the family member. + +### Flow 12: Remove Family Member. + +Routes: +- /account/:id + +APIs touched: +- GET /api/tree-info/:id +- GET /api/family-members/active/:id +- POST /api/family-members +- DELETE /api/relationships + +Steps: +1. Open /family. +2. Click on a member's 'View' button to open their account details. +3. Submit and verify delete. + +Expected results: +- Family member is removed from the tree and user's family page. +- Tree and member persist on failure with visible error message. + +### Flow 13: Share Tree with Another Member + +Routes: +- /tree/sharetree + +APIs touched: +- GET /api/family-members/user/:userId +- GET /api/auth/users +- GET /api/tree-info/:id +- POST /api/share-trees/share + +Steps: +1. Open share tree page. +2. Search/select a family member or send through email. +3. Submit share. + +Expected results: +- Shared tree record is created. +- Email is sent if selected. +- Sender is redirected to home/dashboard. + +Negative coverage: +- Missing required selected member or email blocks submit. + +### Flow 14: View Incoming Shared Trees and Open One + +Routes: +- /tree/viewsharedtrees +- /sharedtree/:id + +APIs touched: +- GET /api/share-trees/receiver/:id +- GET /api/auth/users +- GET /api/share-trees/:id + +Steps: +1. Login as receiving user. +2. Open shared trees list page. +3. Verify list includes incoming shares. +4. Click View Tree for a share. + +Expected results: +- Receiver can navigate to shared tree view. +- Sender identity and tree content display. + +### Flow 15: View Relationship Badge on Another User Profile + +Routes: +- /account/:id (other user) + +APIs touched: +- GET /api/relationships/between/:viewerId/:profileId + +Steps: +1. Open profile of another user/family member. +2. Observe relationship tag. + +Expected results: +- Relationship badge appears for linked members. +- Missing relationship returns neutral UI without crash. + +### Flow 16: Event Dashboard CRUD + +Routes: +- / +- /useractivitydash + +APIs touched: +- GET /api/events/:auth_uid +- POST /api/events +- PUT /api/events/:id +- DELETE /api/events/:id + +Steps: +1. Open dashboard and create a new event. +2. Verify event appears in list. +3. Edit event and verify updates. +4. Delete event and verify removal. +5. Search by title/date and toggle sort order. + +Expected results: +- CRUD operations persist correctly. +- Search and sort update list presentation as expected. + +### Flow 17: Backup and Restore (Unimplemented) + +Routes: +- Backend/API flow (UI trigger pending in Settings) + +APIs touched: +- POST /api/backup/:id +- POST /api/backup/restore/:id + +Steps: +1. Trigger backup for test user. +2. Mutate user family data. +3. Trigger restore. +4. Re-open family/tree views and verify restoration. + +Expected results: +- Backup payload captures account-related records. +- Restore rehydrates prior data snapshot. + +### Flow 17A: Merge Shared Tree Members and Assign Relationships (Unimplemented) + +Routes: +- /sharedtree/:id + +APIs touched: +- POST /api/share-trees/merge/:sharedTreeId +- POST /api/share-trees/assign-relationship + +Steps: +1. Open a shared tree as the receiver. +2. Select shared members to merge into receiver base tree. +3. Submit merge request. +4. Assign relationship types to newly merged members. + +Expected results: +- Merged members are persisted to receiver data. +- Relationship assignments are saved and reflected in tree/profile views. + +Negative coverage: +- Empty merge selection is rejected. +- Invalid sharedTreeId returns handled error. + +### Flow 18: Navigation and Informational Pages + +Routes: +- /websitesettings +- /help +- /chat + +Steps: +1. Open each route as authenticated user. +2. Verify core page sections load and navigation remains functional. + +Expected results: +- Settings renders controls (including dark mode toggle). +- Help FAQ content is visible. +- Chat placeholder content is visible. + +## 5. Cross-Cutting Assertions +Apply these checks in most UI flows: +- Auth guard behavior for protected routes +- User-friendly error messaging for failed API requests +- No console exceptions that break flow completion (crash) + +## 6. Traceability Matrix (Route/API to Flow) +- Auth and account: Flows 1-8 +- Family and profile viewing: Flows 9, 15 +- Tree and membership: Flows 10-12 +- Shared trees: Flows 13-14 +- Events: Flow 16 +- Backup/restore and shared merge operations: Flows 17-17A +- Support/settings placeholders: Flow 18 diff --git a/client/.gitignore b/client/.gitignore index 9fcb689..98c2430 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -16,6 +16,7 @@ .env.local .env.development.local .env.test.local +.env.test .env.production.local project-root/.env npm-debug.log* diff --git a/client/package-lock.json b/client/package-lock.json index 2d46024..76e39be 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -28,7 +28,9 @@ "yup": "^1.6.1" }, "devDependencies": { - "@babel/plugin-proposal-private-property-in-object": "^7.21.11" + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "cross-env": "^10.1.0", + "env-cmd": "^11.0.0" } }, "node_modules/@adobe/css-tools": { @@ -2464,6 +2466,12 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -7054,6 +7062,23 @@ "node": ">=10" } }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8435,6 +8460,41 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-cmd": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-11.0.0.tgz", + "integrity": "sha512-gnG7H1PlwPqsGhFJNTv68lsDGyQdK+U9DwLVitcj1+wGq7LeOBgUzZd2puZ710bHcH9NfNeGWe2sbw7pdvAqDw==", + "dev": true, + "dependencies": { + "@commander-js/extra-typings": "^13.1.0", + "commander": "^13.1.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "env-cmd": "bin/env-cmd.js" + }, + "engines": { + "node": ">=20.10.0" + } + }, + "node_modules/env-cmd/node_modules/@commander-js/extra-typings": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", + "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", + "dev": true, + "peerDependencies": { + "commander": "~13.1.0" + } + }, + "node_modules/env-cmd/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", diff --git a/client/package.json b/client/package.json index f0b2061..72630a1 100644 --- a/client/package.json +++ b/client/package.json @@ -24,6 +24,8 @@ }, "scripts": { "start": "react-scripts start", + "start:ci-test": "react-scripts start", + "start:dev-test": "env-cmd -f .env.test react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" @@ -47,6 +49,8 @@ ] }, "devDependencies": { - "@babel/plugin-proposal-private-property-in-object": "^7.21.11" + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "cross-env": "^10.1.0", + "env-cmd": "^11.0.0" } } diff --git a/client/src/components/AddFamilyMember/AddFamilyMember.js b/client/src/components/AddFamilyMember/AddFamilyMember.js index 91e05a6..234877a 100644 --- a/client/src/components/AddFamilyMember/AddFamilyMember.js +++ b/client/src/components/AddFamilyMember/AddFamilyMember.js @@ -265,11 +265,12 @@ function AddFamilyMemberPopup({ trigger, userid }) { // Resolve relationships to create across the database // IMPORTANT: Use treeUserId (the member ID), not currentAccountID (the user ID) + const safeConnectTo = String(data.connectTo || '').startsWith('ghost_') ? null : data.connectTo; const relsToCreate = relationshipService.getRequiredDBRelationships( treeUserId, treeMemberId, data.selectedMemberRelationship, - data.connectTo, + safeConnectTo, data.matPat || null ); @@ -325,8 +326,8 @@ function AddFamilyMemberPopup({ trigger, userid }) { const dbLinkCount = relsToCreate.length; const mainRel = data.selectedMemberRelationship; let factualSummary = `Added as your ${mainRel.charAt(0).toUpperCase() + mainRel.slice(1)}`; - if (dbLinkCount > 1 && data.connectTo) { - const partnerName = treeInfo[data.connectTo]?.data["first name"] || "Partner"; + if (dbLinkCount > 1 && safeConnectTo) { + const partnerName = treeInfo[safeConnectTo]?.data["first name"] || "Partner"; factualSummary += ` and linked to ${partnerName}`; } @@ -395,11 +396,12 @@ function AddFamilyMemberPopup({ trigger, userid }) { // Resolve relationships to create across the database // IMPORTANT: Use treeUserId (the member ID), not currentAccountID (the user ID) + const safeConnectTo2 = String(data.connectTo2 || '').startsWith('ghost_') ? null : data.connectTo2; const relsToCreate = relationshipService.getRequiredDBRelationships( treeUserId, treeMemberId, data.relationship, - data.connectTo2, + safeConnectTo2, data.matPat2 || null ); @@ -456,8 +458,8 @@ function AddFamilyMemberPopup({ trigger, userid }) { const dbLinkCount = relsToCreate.length; const mainRel = data.relationship; let factualSummary = `Added as your ${mainRel.charAt(0).toUpperCase() + mainRel.slice(1)}`; - if (dbLinkCount > 1 && data.connectTo2) { - const partnerName = treeInfo[data.connectTo2]?.data["first name"] || "Partner"; + if (dbLinkCount > 1 && safeConnectTo2) { + const partnerName = treeInfo[safeConnectTo2]?.data["first name"] || "Partner"; factualSummary += ` and linked to ${partnerName}`; } diff --git a/client/src/pages/CreateAccount/CreateAccount.js b/client/src/pages/CreateAccount/CreateAccount.js index 2a2c0f8..0075749 100644 --- a/client/src/pages/CreateAccount/CreateAccount.js +++ b/client/src/pages/CreateAccount/CreateAccount.js @@ -14,8 +14,13 @@ const yupValidation = yup.object().shape( { firstname: yup.string().required("First name is a required field."), lastname: yup.string().required("Last name is a required field."), - birthdate: yup.date().required("Birthdate is a required field."), - gender: yup.string().oneOf(['M', 'F'], 'Please select a valid option').required('Gender field is required'), + birthdate: yup + .date() + .transform((value, originalValue) => (originalValue === '' ? null : value)) + .nullable() + .typeError("Birthdate is a required field.") + .required("Birthdate is a required field."), + gender: yup.string().oneOf(['M', 'F'], 'Gender field is required').required('Gender field is required'), email: yup.string().required("Email is a required field.") .matches( "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$" @@ -217,8 +222,8 @@ const CreateAccount = () => {