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

Fix/collection of fixes 5 #828

Merged
merged 8 commits into from
Sep 18, 2023
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"test:triggers": "bash -c '(trap \"kill 0\" SIGINT; ../functions/build_scripts/watch.js & firebase -c ../../firebase-testing.json emulators:exec --project eisbuk -- \" vitest dataTriggers\")'",
"test:rules": "bash -c '(trap \"kill 0\" SIGINT; ../functions/build_scripts/watch.js & firebase -c ../../firebase-testing.json emulators:exec --project eisbuk -- \" vitest firestoreRules\")'",
"test:integrations": "bash -c '(trap \"kill 0\" SIGINT; ../functions/build_scripts/watch.js & firebase -c ../../firebase-testing.json emulators:exec --project eisbuk -- \" vitest integrations\")'",
"test:emulators:ci": "export CI=true && export VITEST_JUNIT_SUITE_NAME=\"Client tests\" && bash -c '(trap \"kill 0\" SIGINT; ../functions/build_scripts/watch.js & firebase -c ../../firebase-testing.json emulators:exec --project eisbuk -- \" vitest run --coverage --reporter=junit --reporter=html --outputFile.junit=junit.xml/\")'",
"test:emulators:ci": "CI=true VITEST_JUNIT_SUITE_NAME=\"Client tests\" bash -c '(trap \"kill 0\" SIGINT; ../functions/build_scripts/watch.js & firebase -c ../../firebase-testing.json emulators:exec --project eisbuk -- \" vitest run --coverage --reporter=junit --reporter=html --outputFile.junit=junit.xml/\")' || true # We always exit with code 0: a subsequent CI step will fail if a test fails.",
"test:quicktest": "echo Running tests with no emulators support && vitest --ui",
"test": "echo 'Running all tests; using emulators.' && rushx test:emulators:ui",
"storybook": "storybook dev -p 6006",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
IconButtonShape,
IconButtonSize,
} from "@eisbuk/ui";
import { testId } from "@eisbuk/testing/testIds";

import { Calendar } from "@eisbuk/svg";

import {
Expand Down Expand Up @@ -42,6 +44,7 @@ const AddToCalendar: React.FC = () => {

<IconButton
aria-label={t(ActionButton.AddToCalendar)}
data-testid={testId("add-to-calendar")}
className="fixed right-6 bottom-8 z-40 bg-cyan-700 text-white shadow-xl md:hidden"
size={IconButtonSize.XL}
contentSize={IconButtonContentSize.Loose}
Expand Down
7 changes: 5 additions & 2 deletions packages/client/src/pages/customer_area/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import CalendarView from "./views/Calendar";
import ProfileView from "./views/Profile";
import { useSecretKey, useDate } from "./hooks";

import AddToCalendar from "@/components/atoms/AddToCalendar";
// import AddToCalendar from "@/components/atoms/AddToCalendar";

import Layout from "@/controllers/Layout";

Expand Down Expand Up @@ -92,7 +92,10 @@ const CustomerArea: React.FC = () => {
{view !== "ProfileView" && (
<CalendarNav
{...calendarNavProps}
additionalContent={<AddToCalendar />}
// TODO: Reinstate this when the ability to add multiple events is fixed
// See: https://github.com/eisbuk/EisBuk/issues/827
//
// additionalContent={<AddToCalendar />}
jump="month"
/>
)}
Expand Down
12 changes: 12 additions & 0 deletions packages/client/src/pages/debug/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ const DebugPage: React.FC = () => {
Remove Invalid Customer Phones
</DebugPageButton>
</div>

<div className="p-2">
<DebugPageButton
onClick={createFunctionCaller(
functions,
CloudFunction.ClearDeletedCustomersRegistrationAndCategories
)}
color={ButtonColor.Primary}
>
Clear Deleted Customers Registration And Categories
</DebugPageButton>
</div>
</div>
</LayoutContent>
</Layout>
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/store/actions/__testUtils__/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
CustomerBookings,
Customer,
BookingSubCollection,
CustomerLoose,
sanitizeCustomer,
SlotAttendnace,
CustomerFull,
} from "@eisbuk/shared";

import { TestEnvFirestore } from "@/__testSetup__/firestore";
Expand Down Expand Up @@ -151,7 +151,7 @@ export const setupTestBookings: AdminSetupFunction<{
* Set up `customers` data entry in emulated store in redux store
*/
export const setupTestCustomer: AdminSetupFunction<{
customer: CustomerLoose;
customer: Partial<CustomerFull>;
organization: string;
}> = async ({ customer, db, store, organization }) => {
// id customer id or secretKey not provided, generate locally
Expand Down
120 changes: 117 additions & 3 deletions packages/client/src/store/actions/__tests__/customerOperations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { describe, vi, expect, afterEach } from "vitest";

import { Customer, Category } from "@eisbuk/shared";
import { Customer, Category, CustomerFull } from "@eisbuk/shared";
import i18n, { NotificationMessage } from "@eisbuk/translations";

import { saul } from "@eisbuk/testing/customers";
Expand All @@ -31,12 +31,15 @@ import {
collection,
doc,
getDoc,
setDoc,
getBookedSlotDocPath,
} from "@/utils/firestore";

import { testWithEmulator } from "@/__testUtils__/envUtils";
import { stripIdAndSecretKey } from "@/__testUtils__/customers";
import { setupTestCustomer } from "../__testUtils__/firestore";
import { runThunk } from "@/__testUtils__/helpers";
import { DateTime } from "luxon";

const mockDispatch = vi.fn();

Expand Down Expand Up @@ -149,14 +152,14 @@ describe("customerOperations", () => {

describe("deleteCustomer", () => {
testWithEmulator(
"should delete existing customer in database",
"when customer is deleted, should mark them as deleted and clear out their categories and card number",
async () => {
// setup test state
const store = getNewStore();
const { db, organization } = await getTestEnv({
setup: (db, { organization }) =>
setupTestCustomer({
customer: saul,
customer: { ...saul, subscriptionNumber: "123" },
store,
db,
organization,
Expand All @@ -176,7 +179,118 @@ describe("customerOperations", () => {
expect(deletedSaul).toEqual({
...saul,
deleted: true,
categories: [],
subscriptionNumber: "",
});
// check for success notification
expect(mockDispatch).toHaveBeenCalledWith(
enqueueNotification({
message: i18n.t(NotificationMessage.CustomerDeleted, {
name: saul.name,
surname: saul.surname,
}),
variant: NotifVariant.Success,
})
);
}
);

testWithEmulator(
"should not delete customer if the customer has bookings for a future date",
async () => {
// setup test state
const store = getNewStore();
const { db, organization } = await getTestEnv({
setup: async (db, { organization }) => {
await setupTestCustomer({
customer: saul,
store,
db,
organization,
});
await setDoc(
doc(
db,
getBookedSlotDocPath(organization, saul.secretKey, "slot-1")
),
{
// Set up a date sometime in the future
date: DateTime.now().plus({ days: 2 }).toISODate(),
// Interval is completely irrelevant
interval: "09:00-10:00",
}
);
},
});
// make sure tests are ran against test generated organization
getOrganizationSpy.mockReturnValueOnce(organization);
// make sure that the db used by the thunk is test db
const getFirestore = () => db;
// attempt delete
const testThunk = deleteCustomer(saul);
await runThunk(testThunk, mockDispatch, store.getState, {
getFirestore,
});
// customer shouldn't be deleted
const saulDocRef = doc(db, getCustomersPath(organization), saul.id);
const updatedSaul = (await getDoc(saulDocRef)).data() as CustomerFull;
expect(Boolean(updatedSaul.deleted)).toEqual(false);
// should have shown an error letting the user know the reason for failuer
expect(mockDispatch).toHaveBeenCalledWith(
enqueueNotification({
message: i18n.t(
NotificationMessage.CustomerDeleteErrorFutureBookings,
{
name: saul.name,
surname: saul.surname,
}
),
variant: NotifVariant.Error,
})
);
}
);

testWithEmulator(
"existing bookings shouldn't prevent customer from being deleted if all bookings dates are in the past",
async () => {
// setup test state
const store = getNewStore();
const { db, organization } = await getTestEnv({
setup: async (db, { organization }) => {
await setupTestCustomer({
customer: saul,
store,
db,
organization,
});
await setDoc(
doc(
db,
getBookedSlotDocPath(organization, saul.secretKey, "slot-1")
),
{
// Set up a date sometime in the past
date: DateTime.now().minus({ days: 2 }).toISODate(),
// Interval is completely irrelevant
interval: "09:00-10:00",
}
);
},
});
// make sure tests are ran against test generated organization
getOrganizationSpy.mockReturnValueOnce(organization);
// make sure that the db used by the thunk is test db
const getFirestore = () => db;
// attempt delete
const testThunk = deleteCustomer(saul);
await runThunk(testThunk, mockDispatch, store.getState, {
getFirestore,
});
// customer should be deleted
const saulDocRef = doc(db, getCustomersPath(organization), saul.id);
const updatedSaul = (await getDoc(saulDocRef)).data() as CustomerFull;
expect(Boolean(updatedSaul.deleted)).toEqual(true);
// check for success notification
expect(mockDispatch).toHaveBeenCalledWith(
enqueueNotification({
Expand Down
39 changes: 37 additions & 2 deletions packages/client/src/store/actions/customerOperations.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DateTime } from "luxon";

import { CustomerLoose, Customer } from "@eisbuk/shared";
import i18n, { NotificationMessage } from "@eisbuk/translations";

Expand All @@ -16,6 +18,8 @@ import {
doc,
setDoc,
addDoc,
getDocs,
getBookedSlotsPath,
} from "@/utils/firestore";

/**
Expand Down Expand Up @@ -78,10 +82,41 @@ export const deleteCustomer =
(customer: Customer): FirestoreThunk =>
async (dispatch, _, { getFirestore }) => {
try {
const organization = getOrganization();
const db = getFirestore();
const docRef = doc(db, getCustomersPath(getOrganization()), customer.id);
const docRef = doc(db, getCustomersPath(organization), customer.id);

// Check if customer has bookings in the future
const allBookings = await getDocs(
collection(db, getBookedSlotsPath(organization, customer.secretKey))
);
const hasFutureBookings = allBookings.docs.some((doc) => {
const { date } = doc.data();
const cond = date >= DateTime.now().toISODate();
return cond;
});

await setDoc(docRef, { deleted: true }, { merge: true });
if (hasFutureBookings) {
dispatch(
enqueueNotification({
message: i18n.t(
NotificationMessage.CustomerDeleteErrorFutureBookings,
{
name: customer.name,
surname: customer.surname,
}
),
variant: NotifVariant.Error,
})
);
return;
}

await setDoc(
docRef,
{ deleted: true, categories: [], subscriptionNumber: "" },
{ merge: true }
);

dispatch(
enqueueNotification({
Expand Down
10 changes: 4 additions & 6 deletions packages/e2e/integration/calendar_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { DateTime, DateTimeUnit } from "luxon";
import { Customer, CustomerBookings, SlotInterface } from "@eisbuk/shared";
import i18n, {
ActionButton,
BookingAria,
createDateTitle,
NotificationMessage,
} from "@eisbuk/translations";
Expand Down Expand Up @@ -110,7 +109,8 @@ xdescribe("Date Switcher", () => {
});
});

describe("Download ics file to Add To Calendar", () => {
/** @TODO un-skip this once add to calendar button is fixed */
xdescribe("Download ics file to Add To Calendar", () => {
it("checks email was sent and calendar collection was updated successfully", () => {
cy.setClock(testDateLuxon.toMillis());

Expand All @@ -125,11 +125,9 @@ describe("Download ics file to Add To Calendar", () => {
)
);
cy.visit([Routes.CustomerArea, saul.secretKey].join("/"));
cy.getAttrWith("aria-label", i18n.t(BookingAria.BookButton))
.first()
.click({ force: true });
cy.getByTestId("book-button").first().click({ force: true });

cy.contains(i18n.t(ActionButton.AddToCalendar) as string).click();
cy.getByTestId("add-to-calendar").click();
cy.getAttrWith("type", "email").clearAndType(
saul.email || "valid@email.com"
);
Expand Down
32 changes: 32 additions & 0 deletions packages/functions/src/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,35 @@ export const removeInvalidCustomerPhones = functions
return { success: true };
}
);

export const clearDeletedCustomersRegistrationAndCategories = functions
.region(__functionsZone__)
.https.onCall(async ({ organization }, { auth }) => {
if (!(await checkUser(organization, auth))) throwUnauth();

const allCustomers = await admin
.firestore()
.collection(Collection.Organizations)
.doc(organization)
.collection(OrgSubCollection.Customers)
.get();

const deletedCustomers = allCustomers.docs.filter(
(doc) => doc.data().deleted
);

// Clear registrationCode and customers for all deleted customers
const batch = admin.firestore().batch();
deletedCustomers.forEach((doc) => {
batch.set(
doc.ref,
{
registrationCode: "",
categories: [],
},
{ merge: true }
);
});

await batch.commit();
});
1 change: 1 addition & 0 deletions packages/shared/src/ui/enums/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export enum CloudFunction {
DeleteOrphanedBookings = "deleteOrphanedBookings",
PopulateDefaultEmailTemplates = "populateDefaultEmailTemplates",
RemoveInvalidCustomerPhones = "removeInvalidCustomerPhones",
ClearDeletedCustomersRegistrationAndCategories = "clearDeletedCustomersRegistrationAndCategories",
}
2 changes: 2 additions & 0 deletions packages/testing/src/testIds/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type TestIDList = [

// #region BookingCard
"booking-interval-card",
"book-button",
// #endregion BookingCard

// #region CustomerList
Expand All @@ -85,6 +86,7 @@ type TestIDList = [

// #region ActionButton
"add-athlete",
"add-to-calendar",
// #endregion ActionButton

// #region AthleteForm
Expand Down
Loading