Skip to content

Commit

Permalink
Merge pull request #852 from eisbuk/feature/privacy-policy
Browse files Browse the repository at this point in the history
Feature/privacy policy
  • Loading branch information
silviot authored Oct 20, 2023
2 parents 752b308 + c2a7c05 commit 7a61243
Show file tree
Hide file tree
Showing 35 changed files with 1,352 additions and 230 deletions.
404 changes: 395 additions & 9 deletions common/config/rush/pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion common/config/rush/repo-state.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
{
"pnpmShrinkwrapHash": "61ae9a690dfb715f3db40c276c5865936ef37dc4",
"pnpmShrinkwrapHash": "6ba2c5c75d7522e2ad44c04036a43eecc27e6110",
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
}
3 changes: 2 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"yup": "0.32.9",
"variant": "~2.1.0",
"firebase": "~9.22.0",
"react-error-boundary": "~4.0.11"
"react-error-boundary": "~4.0.11",
"react-markdown": "~9.0.0"
},
"scripts": {
"build": "echo \"Skipping @eisbuk/client build as part of rush's bulk 'build' script. To build the app for production run: 'rushx build:prod'\"",
Expand Down
126 changes: 76 additions & 50 deletions packages/client/src/AppContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { useSelector } from "react-redux";

import { Collection, OrgSubCollection } from "@eisbuk/shared";
import { Routes, PrivateRoutes } from "@eisbuk/shared/ui";
import { DevWarning } from "@eisbuk/ui";
import {
CollectionSubscription,
usePaginateFirestore,
useFirestoreSubscribe,
} from "@eisbuk/react-redux-firebase-firestore";

import { __isDevStrict__ } from "@/lib/constants";

import { getOrganization } from "@/lib/getters";

import PrivateRoute from "@/components/auth/PrivateRoute";
Expand All @@ -27,6 +30,7 @@ import AttendancePrintable from "@/pages/attendance_printable";
import DebugPage from "@/pages/debug";
import AdminPreferencesPage from "@/pages/admin_preferences";
import SelfRegister from "@/pages/self_register";
import PrivacyPolicy from "@/pages/privacy_policy";

import { getIsAdmin } from "@/store/selectors/auth";

Expand All @@ -52,58 +56,80 @@ const AppContent: React.FC = () => {
useFirestoreSubscribe(getOrganization(), subscribedCollections);
usePaginateFirestore();

// Remove the firestore emulator warning as it's in the way.
// We're adding a warning of our own, incorporated nicely into out layout
React.useEffect(() => {
document.querySelector("p.firebase-emulator-warning")?.remove();
}, []);

// We're showing the emulators warning strictly in "development" mode,
// not in test mode (which can loosely be considered dev mode) so as to not obscure the
// parts of the UI in cypress tests.
const showEmulatorsWarining = __isDevStrict__;

return (
<Switch>
<LoginRoute path={Routes.Login} component={LoginPage} />
<PrivateRoute
exact
path={PrivateRoutes.Root}
component={AttendancePage}
/>
<PrivateRoute
exact
path={PrivateRoutes.Athletes}
component={AthletesPage}
/>
<PrivateRoute
exact
path={PrivateRoutes.NewAthlete}
component={AthleteProfilePage}
/>
<PrivateRoute
path={`${PrivateRoutes.Athletes}/:athlete`}
component={AthleteProfilePage}
/>
<PrivateRoute path={PrivateRoutes.Slots} component={SlotsPage} />
<PrivateRoute
path={Routes.AttendancePrintable}
component={AttendancePrintable}
/>
<PrivateRoute
path={PrivateRoutes.AdminPreferences}
component={AdminPreferencesPage}
/>
<>
<Switch>
<LoginRoute path={Routes.Login} component={LoginPage} />
<PrivateRoute
exact
path={PrivateRoutes.Root}
component={AttendancePage}
/>
<PrivateRoute
exact
path={PrivateRoutes.Athletes}
component={AthletesPage}
/>
<PrivateRoute
exact
path={PrivateRoutes.NewAthlete}
component={AthleteProfilePage}
/>
<PrivateRoute
path={`${PrivateRoutes.Athletes}/:athlete`}
component={AthleteProfilePage}
/>
<PrivateRoute path={PrivateRoutes.Slots} component={SlotsPage} />
<PrivateRoute
path={Routes.AttendancePrintable}
component={AttendancePrintable}
/>
<PrivateRoute
path={PrivateRoutes.AdminPreferences}
component={AdminPreferencesPage}
/>

<PrivateRoute
// Private route is a hack here...if visiting '/customer_area' without a secret key,
// if will handle all cases of auth/non-auth/auth-but-not-registered appropriately.
//
// For admin, however, after they pass the PrivateRoute checks, they will be redirected to
// '/athletes' page (from where they can redirect to the correct customer area - for a given customer)
path={Routes.CustomerArea}
exact={true}
>
<Redirect to={PrivateRoutes.Athletes} />
</PrivateRoute>
<Route
path={`${Routes.CustomerArea}/:secretKey`}
component={CustomerAreaPage}
/>
<Route
exact
path={Routes.ErrorBoundary}
component={ErrorBoundaryPage}
/>
<Route path={Routes.SelfRegister} component={SelfRegister} exact />
<Route path={Routes.Debug} component={DebugPage} />
<Route path={Routes.Deleted} component={Deleted} />
<Route path={Routes.PrivacyPolicy} component={PrivacyPolicy} />
</Switch>

<PrivateRoute
// Private route is a hack here...if visiting '/customer_area' without a secret key,
// if will handle all cases of auth/non-auth/auth-but-not-registered appropriately.
//
// For admin, however, after they pass the PrivateRoute checks, they will be redirected to
// '/athletes' page (from where they can redirect to the correct customer area - for a given customer)
path={Routes.CustomerArea}
exact={true}
>
<Redirect to={PrivateRoutes.Athletes} />
</PrivateRoute>
<Route
path={`${Routes.CustomerArea}/:secretKey`}
component={CustomerAreaPage}
/>
<Route exact path={Routes.ErrorBoundary} component={ErrorBoundaryPage} />
<Route path={Routes.SelfRegister} component={SelfRegister} exact />
<Route path={Routes.Debug} component={DebugPage} />
<Route path={Routes.Deleted} component={Deleted} />
</Switch>
<div className="fixed z-50 bottom-1 left-1/2 -translate-x-1/2 content-container text-center">
<DevWarning open={showEmulatorsWarining} />
</div>
</>
);
};

Expand Down
6 changes: 2 additions & 4 deletions packages/client/src/__testUtils__/envUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { test, TestFunction } from "vitest";

type TestClosure = (name: string, fn?: TestFunction, timeout?: number) => void;
import { test, TestAPI } from "vitest";

/**
* A boolean flag set to `true` if the emulators exist in current environment
Expand All @@ -13,7 +11,7 @@ export const __withEmulators__ = Boolean(process.env.FIRESTORE_EMULATOR_HOST);
* Skips test provided (runs `xtest`) if no firestore emulator found
* @param testArgs paramaters of `test` function
*/
export const testWithEmulator: TestClosure = (...args) => {
export const testWithEmulator = (...args: Parameters<TestAPI>) => {
if (__withEmulators__) {
test(...args);
} else {
Expand Down
104 changes: 104 additions & 0 deletions packages/client/src/__tests__/cloudFunctions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { testWithEmulator } from "@/__testUtils__/envUtils";
import { waitFor } from "@/__testUtils__/helpers";

import { saul } from "@eisbuk/testing/customers";
import { DateTime } from "luxon";

describe("Cloud functions", () => {
describe("ping", () => {
Expand Down Expand Up @@ -293,6 +294,109 @@ describe("Cloud functions", () => {
);
});

describe("acceptPrivacyPolicy", () => {
testWithEmulator(
"should store the timestamp of confirmation to the customer's structure in the db",
async () => {
// set up test state
const { organization } = await setUpOrganization();
const saulRef = adminDb.doc(getCustomerDocPath(organization, saul.id));
await saulRef.set(saul);
// wait for bookings to get created (through data trigger)
await waitFor(() =>
adminDb.doc(getBookingsDocPath(organization, saul.secretKey)).get()
);
// Timestamp used for the test, the actual time doesn't matter,
// only that it's stord in the db after the function is ran.
const timestamp = DateTime.now().toISO();
// run the function
await httpsCallable(
functions,
CloudFunction.AcceptPrivacyPolicy
)({
id: saul.id,
organization,
secretKey: saul.secretKey,
timestamp,
});
const customerSnap = await saulRef.get();
expect(customerSnap.data()?.privacyPolicyAccepted).toEqual({
timestamp,
});
// wait for the bookings data to update
await waitFor(async () => {
const bookingsSnap = await adminDb
.doc(getBookingsDocPath(organization, saul.secretKey))
.get();
// The privacy policy confirmation timestamp should be stored in the db
expect(bookingsSnap.data()?.privacyPolicyAccepted).toEqual({
timestamp,
});
});
}
);

testWithEmulator(
"should return an error if no payload provided",
async () => {
await expect(
httpsCallable(functions, CloudFunction.AcceptPrivacyPolicy)()
).rejects.toThrow(HTTPSErrors.NoPayload);
}
);

testWithEmulator(
"should return an error if no organziation, id, secretKey or timestamp provided",
async () => {
try {
await httpsCallable(functions, CloudFunction.AcceptPrivacyPolicy)({});
} catch (error) {
expect((error as FunctionsError).message).toEqual(
`${HTTPSErrors.MissingParameter}: id, organization, secretKey, timestamp`
);
}
}
);

testWithEmulator(
"should return an error if customer id and secretKey mismatch",
async () => {
const { organization } = await setUpOrganization();
const saulRef = adminDb.doc(getCustomerDocPath(organization, saul.id));
await saulRef.set(saul);
await expect(
httpsCallable(
functions,
CloudFunction.AcceptPrivacyPolicy
)({
organization,
id: saul.id,
secretKey: "wrong-key",
timestamp: DateTime.now().toISO(),
})
).rejects.toThrow(BookingsErrors.SecretKeyMismatch);
}
);

testWithEmulator(
"should return an error if customer not found",
async () => {
const { organization } = await setUpOrganization();
await expect(
httpsCallable(
functions,
CloudFunction.AcceptPrivacyPolicy
)({
organization,
id: saul.id,
secretKey: saul.secretKey,
timestamp: DateTime.now().toISO(),
})
).rejects.toThrow(BookingsErrors.CustomerNotFound);
}
);
});

describe("customerSelfUpdate", () => {
testWithEmulator(
"should update customer data in customer collection and then bookings collection by data trigger",
Expand Down
Loading

0 comments on commit 7a61243

Please sign in to comment.