diff --git a/package-lock.json b/package-lock.json index 270b0cd..d88b6e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ }, "devDependencies": { "@faker-js/faker": "^8.4.1", + "@firebase/rules-unit-testing": "^3.0.2", "@types/lodash": "^4.17.0", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", @@ -1404,6 +1405,42 @@ "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.1.tgz", "integrity": "sha512-PgmfUugcJAinPLsJlYcBbNZe7KE2omdQw1WCT/z46nKkNVGkuHdVFSq54s3wiFa9BoHmLZ01u4hGXIhm6MdLOw==" }, + "node_modules/@firebase/rules-unit-testing": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@firebase/rules-unit-testing/-/rules-unit-testing-3.0.2.tgz", + "integrity": "sha512-3wysX8g3grwgm8WbaMoNnNP74WGQFYEm7QIa2MFCaUXyUxVAV/9rGHWPPSmqP7IWGoXFOG6cOLOsry6PeT+BoA==", + "dev": true, + "dependencies": { + "@types/node-fetch": "2.6.4", + "node-fetch": "2.6.7" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "firebase": "^10.0.0" + } + }, + "node_modules/@firebase/rules-unit-testing/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@firebase/storage": { "version": "0.12.4", "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.4.tgz", @@ -2496,6 +2533,30 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/papaparse": { "version": "5.3.14", "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", @@ -3112,8 +3173,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "optional": true + "dev": true }, "node_modules/balanced-match": { "version": "1.0.2", @@ -3444,7 +3504,6 @@ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "optional": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3702,7 +3761,6 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, - "optional": true, "engines": { "node": ">=0.4.0" } @@ -5455,7 +5513,6 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "optional": true, "engines": { "node": ">= 0.6" } @@ -5465,7 +5522,6 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "optional": true, "dependencies": { "mime-db": "1.52.0" }, @@ -7012,8 +7068,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "optional": true + "dev": true }, "node_modules/ts-api-utils": { "version": "1.3.0", @@ -7413,8 +7468,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "optional": true + "dev": true }, "node_modules/websocket-driver": { "version": "0.7.4", @@ -7442,7 +7496,6 @@ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, - "optional": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index 548026a..28028bc 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@typescript-eslint/eslint-plugin": "^7.2.0", + "@firebase/rules-unit-testing": "^3.0.2", "@typescript-eslint/parser": "^7.2.0", "@vitejs/plugin-react-swc": "^3.5.0", "@vitest/coverage-istanbul": "^1.5.1", diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index 789d8a7..dda22e1 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -1,7 +1,10 @@ // Import the functions you need from the SDKs you need -import { initializeApp } from "firebase/app"; -import {connectAuthEmulator, getAuth } from "firebase/auth"; -import {connectFirestoreEmulator, getFirestore } from "firebase/firestore"; +import {FirebaseApp, initializeApp } from "firebase/app"; +import {Auth, connectAuthEmulator, getAuth } from "firebase/auth"; +import {Firestore, connectFirestoreEmulator, getFirestore } from "firebase/firestore"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +import firebase from "firebase/compat"; // Your web app's Firebase configuration export const firebaseConfig = { @@ -14,15 +17,38 @@ export const firebaseConfig = { measurementId: "G-QF0Z0VGCJ0" }; -// Initialize Firebase -export const app = initializeApp(firebaseConfig); -// export const analytics = getAnalytics(app); -export const auth = getAuth(app); // Initialize Firebase Authentication -export const db = getFirestore(app); - -if (import.meta.env.DEV) { +let _app = undefined; +let _auth = undefined; +let _db = undefined; +if (import.meta.env.MODE === "test") { + // +} +else if (import.meta.env.DEV) { + // Initialize Firebase + _app = initializeApp(firebaseConfig); + // export const analytics = getAnalytics(app); + _auth = getAuth(_app); // Initialize Firebase Authentication + _db = getFirestore(_app); console.log("Connecting to emulators"); - connectAuthEmulator(auth, "http://localhost:9099"); - connectFirestoreEmulator(db, "localhost", 8080); + connectAuthEmulator(_auth, "http://localhost:9099"); + connectFirestoreEmulator(_db, "localhost", 8080); } +else { + // Initialize Firebase + _app = initializeApp(firebaseConfig); + // export const analytics = getAnalytics(app); + _auth = getAuth(_app); // Initialize Firebase Authentication + _db = getFirestore(_app); +} + +export const app: FirebaseApp = _app!; +export const auth: Auth = _auth!; +export let db: Firestore = _db!; + +export function setTestDBContext(context: Firestore | firebase.firestore.Firestore) { + if (import.meta.env.MODE === "test") { + db = context; + } +} + diff --git a/src/utils/firestore.test.ts b/src/utils/firestore.test.ts new file mode 100644 index 0000000..eecfdbb --- /dev/null +++ b/src/utils/firestore.test.ts @@ -0,0 +1,119 @@ +import {initializeTestEnvironment, RulesTestEnvironment} from "@firebase/rules-unit-testing"; +import fs from "node:fs"; +import {describe, expect, test} from "vitest"; +import {faker, fakerEN_GB} from "@faker-js/faker"; +import {setTestDBContext} from "./firebase.ts"; +import {getUserPrefs, setUserPrefs, UserPrefs} from "./user_prefs.ts"; +import _ from "lodash"; +import {collection, deleteDoc, doc} from "firebase/firestore"; +import {getTransactionsByDocName, Transaction, writeNewTransaction} from "./transaction.ts"; + +export async function getTestEnv(): Promise { + return await initializeTestEnvironment({ + projectId: "budget-19", + firestore: { + rules: fs.readFileSync("firestore.rules", "utf8"), + host: "127.0.0.1", + port: 8080 + }, + }); +} + +describe("Firestore Rules Tests", () => { + test("Read/Update/Delete/Create UserPrefs Unauthenticated", async () => { + const t = await getTestEnv(); + const user = { uid: faker.string.alphanumeric(20) } + const context = t.authenticatedContext(user.uid); + setTestDBContext(context.firestore()); + + const prefs = UserPrefs.newChecked(0.2, 0.4); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + await setUserPrefs(user, prefs); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const written_prefs = await getUserPrefs(user); + + expect(_.isEqual(prefs, written_prefs), "Prefs not written").toBeTruthy(); + + const no_auth_context = t.unauthenticatedContext(); + setTestDBContext(no_auth_context.firestore()); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const read_pref = await getUserPrefs(user); + expect(_.isEqual(prefs, read_pref), "Unauthorised read").toBeFalsy(); + + await expect(async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + await setUserPrefs(user, UserPrefs.default()); + }, "Unauthorised write").rejects.toThrowError(); + + await expect(async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + await setUserPrefs({ uid: faker.string.alphanumeric(20) }, UserPrefs.default()); + }, "Unauthorised create").rejects.toThrowError(); + + await expect(async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + await deleteDoc(collection(context.firestore(), "UserPrefs"), user.uid); + }, "Unauthorised delete").rejects.toThrowError(); + + await t.cleanup(); + }); + + test("Read/Update/Delete/Create Transaction Unauthenticated", async () => { + const t = await getTestEnv(); + const user = { uid: faker.string.alphanumeric(20) } + const context = t.authenticatedContext(user.uid); + setTestDBContext(context.firestore()); + + const transaction = new Transaction( + fakerEN_GB.location.streetAddress() + ", " + fakerEN_GB.location.city() + ", " + fakerEN_GB.location.zipCode(), + parseFloat(faker.finance.amount({min: -1000, max: 1000})), + faker.word.noun(), + faker.finance.currency().code, + faker.date.past().valueOf(), + faker.lorem.sentence(), + faker.internet.emoji(), + faker.word.noun(), + faker.lorem.sentence(), + user.uid + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const written_transaction = await writeNewTransaction(user, transaction); + + const no_auth_context = t.unauthenticatedContext(); + setTestDBContext(no_auth_context.firestore()); + + await expect(async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + await getTransactionsByDocName(user, written_transaction.forceGetDocName()); + }, "Unauthorised read").rejects.toThrowError(); + + await expect(async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + await setUserPrefs(user, UserPrefs.default()); + }, "Unauthorised write").rejects.toThrowError(); + + await expect(async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + await setUserPrefs({ uid: faker.string.alphanumeric(20) }, UserPrefs.default()); + }, "Unauthorised create").rejects.toThrowError(); + + await expect(async () => { + await deleteDoc(doc(collection(context.firestore(), "UserPrefs"), user.uid)); + }, "Unauthorised delete").rejects.toThrowError(); + + await t.cleanup(); + }); +}); \ No newline at end of file diff --git a/src/utils/transaction.test.ts b/src/utils/transaction.test.ts index 38be3ae..dd0c743 100644 --- a/src/utils/transaction.test.ts +++ b/src/utils/transaction.test.ts @@ -12,6 +12,8 @@ import _ from "lodash"; import { describe, expect, test } from "vitest"; import {where} from "firebase/firestore"; import {emojis} from "../components/transactions/Transaction.ts"; +import {getTestEnv} from "./firestore.test.ts"; +import {setTestDBContext} from "./firebase.ts"; function fakeTransaction(uid: string, name?: string): Transaction { return new Transaction( @@ -33,29 +35,37 @@ describe("Firestore Transaction Tests", () => { expect(_.isEqual(new Set(Object.keys(emojis)), TransactionCategories), "Emoji keys do not match transaction categories").toBeTruthy(); }); test("Write/Read Test", async () => { - const user = { uid: "sample_uid" } - + const t = await getTestEnv(); + const user = {uid: "sample_user"}; + const context = t.authenticatedContext(user.uid); + setTestDBContext(context.firestore()); + const new_transaction = fakeTransaction(user.uid); - expect(new_transaction.getDocName(), "New transaction has no docName").toBe(undefined); + expect(new_transaction.getDocName(), "New transaction has docName").toBe(undefined); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error const written_transaction = await writeNewTransaction(user, new_transaction); - expect(written_transaction.getDocName(), "Written transaction has docName").toBeDefined(); + expect(written_transaction.getDocName(), "Written transaction has no docName").toBeDefined(); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error const fetched_transaction = await getTransactionsByDocName(user, written_transaction.forceGetDocName()); - expect(fetched_transaction, "Written transaction can be fetched").toBeDefined(); + expect(fetched_transaction, "Written transaction can't be fetched").toBeDefined(); - expect(_.isEqual(written_transaction, fetched_transaction), "Fetched transaction matches written transaction").toBeTruthy(); + expect(_.isEqual(written_transaction, fetched_transaction), "Fetched transaction doesn't match written transaction").toBeTruthy(); + + await t.cleanup(); }); test("Write/Read Batch Test", async () => { - const user = { uid: "sample_uid" } + const t = await getTestEnv(); + const user = {uid: "sample_user"}; + const context = t.authenticatedContext(user.uid); + setTestDBContext(context.firestore()); const transaction_name = faker.string.alphanumeric(20); @@ -84,11 +94,16 @@ describe("Firestore Transaction Tests", () => { .sort((a, b) => parseInt(a.description) - parseInt(b.description)); sorted_transactions.forEach((t, i) => t.setDocName(sorted_fetched_transactions[i].forceGetDocName())); - expect(_.isEqual(sorted_transactions, sorted_fetched_transactions), "Written transactions fetched").toBeTruthy(); + expect(_.isEqual(sorted_transactions, sorted_fetched_transactions), "Written transactions not fetched").toBeTruthy(); + + await t.cleanup(); }, 10_000); test("Read All Test", async () => { - const user = { uid: "sample_uid" } + const t = await getTestEnv(); + const user = {uid: "sample_user"}; + const context = t.authenticatedContext(user.uid); + setTestDBContext(context.firestore()); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error @@ -108,12 +123,15 @@ describe("Firestore Transaction Tests", () => { expect( updated_transactions.reduce((curr, el) => curr || _.isEqual(el, new_transaction), false) - , "Written transaction fetched").toBeTruthy(); - + , "Written transaction not fetched").toBeTruthy(); + await t.cleanup(); }, 10_000); test("Delete Test", async () => { - const user = { uid: "sample_uid" } + const t = await getTestEnv(); + const user = {uid: "sample_user"}; + const context = t.authenticatedContext(user.uid); + setTestDBContext(context.firestore()); const transaction_name = faker.string.alphanumeric(20); @@ -124,18 +142,23 @@ describe("Firestore Transaction Tests", () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error expect((await getTransactionsFilterOrderBy(user, where("name", "==", transaction_name))).length - , "New transaction to be present").toBe(1); + , "New transaction not present").toBe(1); await deleteTransaction(transaction.forceGetDocName()); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error expect((await getTransactionsFilterOrderBy(user, where("name", "==", transaction_name))).length - , "Transaction to be deleted").toBe(0); + , "Transaction not deleted").toBe(0); + + await t.cleanup(); }); test("Overwrite Batch Test", async () => { - const user = { uid: "sample_uid" } + const t = await getTestEnv(); + const user = {uid: "sample_user"}; + const context = t.authenticatedContext(user.uid); + setTestDBContext(context.firestore()); const transactions = []; @@ -153,7 +176,7 @@ describe("Firestore Transaction Tests", () => { // @ts-expect-error const transactions_with_doc = await getTransactionsFilterOrderBy(user, where("name", "==", transaction_name)); - expect(transactions_with_doc.length, "Transactions written").toBe(transactions.length); + expect(transactions_with_doc.length, "Transactions not written").toBe(transactions.length); const new_transaction_name = faker.string.alphanumeric(20); @@ -168,10 +191,15 @@ describe("Firestore Transaction Tests", () => { const overwritten_transactions = await getTransactionsFilterOrderBy(user, where("name", "==", new_transaction_name)); expect(overwritten_transactions.reduce((curr, t) => curr && (t.name == new_transaction_name), true)).toBeTruthy(); + + await t.cleanup(); }, 20_000); test("Overwrite Test", async () => { - const user = { uid: "sample_uid" } + const t = await getTestEnv(); + const user = {uid: "sample_user"}; + const context = t.authenticatedContext(user.uid); + setTestDBContext(context.firestore()); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error @@ -185,14 +213,16 @@ describe("Firestore Transaction Tests", () => { // @ts-expect-error const overwritten_transaction = await overwriteTransaction(user, transaction.forceGetDocName(), transaction); - expect(overwritten_transaction.getDocName(), "Overwritten transaction to have the same docName").toBe(transaction.forceGetDocName()); + expect(overwritten_transaction.getDocName(), "Overwritten transaction doesn't have the same docName").toBe(transaction.forceGetDocName()); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error const read_transaction = await getTransactionsByDocName(user, transaction.forceGetDocName()); - expect(read_transaction, "Overwritten transaction to exist").toBeDefined(); + expect(read_transaction, "Overwritten transaction doesn't exist").toBeDefined(); + + expect(read_transaction!.name, "Overwritten changes aren't present").toBe(new_name); - expect(read_transaction!.name, "Overwritten changes to be present").toBe(new_name); + await t.cleanup(); }); }); \ No newline at end of file diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index 610f406..71923c5 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -1,7 +1,9 @@ -import {collection, +import { + collection, deleteDoc, doc, DocumentSnapshot, getDoc, getDocs, query, QueryConstraint, setDoc, SnapshotOptions, - where, writeBatch} from "firebase/firestore"; + where, writeBatch +} from "firebase/firestore"; import {User} from "firebase/auth"; import {db} from "./firebase.ts"; import {Categories, goalCategories} from "./user_prefs.ts"; @@ -100,7 +102,7 @@ export class Transaction { } } -// ! This is set by Firebase - do not change! +// ! This is set by Firestore - do not change! const MAX_BATCH_SIZE = 500; // Returns all transactions for the given `user` with the `docName` attribute set diff --git a/src/utils/user_prefs.test.ts b/src/utils/user_prefs.test.ts index bc8f7f1..1e71ca3 100644 --- a/src/utils/user_prefs.test.ts +++ b/src/utils/user_prefs.test.ts @@ -3,6 +3,8 @@ import {faker} from "@faker-js/faker"; import {UserPrefs, getUserPrefs, setUserPrefs, Categories} from "./user_prefs.ts"; import _ from "lodash"; import {TransactionCategories} from "./transaction.ts"; +import {getTestEnv} from "./firestore.test.ts"; +import {setTestDBContext} from "./firebase.ts"; describe("Firestore UserPrefs Tests", () => { @@ -10,7 +12,10 @@ describe("Firestore UserPrefs Tests", () => { expect(_.isEqual(new Set(Object.keys(Categories)), TransactionCategories), "Emoji keys do not match transaction categories").toBeTruthy(); }); test("Read/Write/Default Value Test", async () => { + const t = await getTestEnv(); const user = { uid: faker.string.alphanumeric(20) } + const context = t.authenticatedContext(user.uid); + setTestDBContext(context.firestore()); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error @@ -22,12 +27,14 @@ describe("Firestore UserPrefs Tests", () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error - await setUserPrefs(user, prefs); + await setUserPrefs(user, new_prefs); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error const read_prefs = await getUserPrefs(user); expect(_.isEqual(read_prefs, new_prefs), "UserPref changes to be read").toBeTruthy(); + + await t.cleanup(); }); }); \ No newline at end of file