Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a03f9e4
feat(user-metadata): enhance Google metadata handling and update tests
tyler-dane Mar 7, 2026
5f3a5d0
fix(backend): guard stale watch metadata assessment errors
cursoragent Mar 7, 2026
c814d52
fix(backend): prevent concurrent sync by skipping assessment in impor…
cursoragent Mar 7, 2026
5d674fa
fix(sync,web): recover invalid sync tokens and handle connected status
cursoragent Mar 7, 2026
f7034db
fix(web): remove redundant syncStatus check in useConnectGoogle hook
cursoragent Mar 7, 2026
2942646
feat(sync): enhance gcal import handling in SyncController tests
tyler-dane Mar 7, 2026
dc14431
fix(util): improve error handling in waitUntilEvent utility
tyler-dane Mar 8, 2026
9f9fa34
feat(auth): enhance Google Calendar connection handling in useConnect…
tyler-dane Mar 8, 2026
04413ed
feat(sync): enhance Google Calendar import functionality and testing
tyler-dane Mar 8, 2026
d239a1f
feat(sync): improve Google Calendar repair functionality in useConnec…
tyler-dane Mar 8, 2026
627b3d3
docs(tests): update testing guidelines to avoid direct persistence la…
tyler-dane Mar 8, 2026
9be4a32
fix(web): use state-specific labels for Google Calendar command palette
cursoragent Mar 8, 2026
7eae7d4
test(web): update test assertions to match state-specific Google Cale…
cursoragent Mar 8, 2026
9ca5562
fix(web): prioritize reconnect_required over importing state in Googl…
cursoragent Mar 8, 2026
6c46ccf
fix(backend): remove unused driver methods after local refactor
cursoragent Mar 8, 2026
092f48e
fix(husky): run lint-staged in quiet mode during pre-commit hook
tyler-dane Mar 8, 2026
5902341
fix(sync): enhance Google Calendar sync handling and notification pro…
tyler-dane Mar 8, 2026
7c05304
fix(user-metadata): refactor user metadata retrieval and update logic
tyler-dane Mar 8, 2026
03abffa
feat(user-metadata): implement user metadata refresh and loading stat…
tyler-dane Mar 8, 2026
9a3cdc1
test(DayCmdPalette): enhance tests for command palette button states
tyler-dane Mar 13, 2026
960611f
refactor(google-auth): update socket handling on Google revocation
tyler-dane Mar 13, 2026
ecd51e1
feat(websocket): enhance Google Calendar import handling with structu…
tyler-dane Mar 13, 2026
ca471b7
feat(google-auth): enhance Google sign-in handling with new utility f…
tyler-dane Mar 13, 2026
1026cec
feat(google-auth): refactor Google authentication flow and enhance re…
tyler-dane Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursorrules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const button = container.querySelector('.add-button');
- Use async/await for asynchronous tests
- Mock external services (Google Calendar API, MongoDB) appropriately
- Test error handling and edge cases
- **Do not import `mongoService` or other persistence layers directly in tests.** Use the test drivers in `packages/backend/src/__tests__/drivers/` (e.g. `UserDriver`, `WatchDriver`) so that tests stay agnostic of the backing store and switching away from Mongo later does not require test changes.

**Real examples:**

Expand Down
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint-staged
yarn lint-staged --quiet
3 changes: 3 additions & 0 deletions docs/testing-playbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,12 @@ Preferred style:
- realistic request flows when possible
- mock only external services, not internal business logic

**Do not import `mongoService` (or other persistence implementations) directly in tests.** Use test drivers instead (e.g. `UserDriver`, `WatchDriver` in `packages/backend/src/__tests__/drivers/`). Drivers encapsulate persistence so that switching away from Mongo (or another store) in the future does not require changing test code.

Useful anchors:

- `packages/backend/src/__tests__`
- `packages/backend/src/__tests__/drivers/`
- `packages/backend/src/event/services/*.test.ts`
- `packages/backend/src/sync/**/*.test.ts`

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"postinstall": "husky install | chmod ug+x .husky/*",
"test": "cross-env TZ=Etc/UTC ./node_modules/.bin/jest",
"test:e2e": "playwright test",
"test:backend": "yarn test backend",
"test:backend": "cross-env TZ=Etc/UTC ./node_modules/.bin/jest --selectProjects backend",
"test:core": "yarn test core",
"test:web": "cross-env TZ=Etc/UTC ./node_modules/.bin/jest web",
"test:scripts": "yarn test scripts",
Expand Down
33 changes: 2 additions & 31 deletions packages/backend/src/__tests__/drivers/sync.controller.driver.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import request from "supertest";
import { GCAL_NOTIFICATION_ENDPOINT } from "@core/constants/core.constants";
import {
IMPORT_GCAL_END,
IMPORT_GCAL_START,
} from "@core/constants/websocket.constants";
import { Status } from "@core/errors/status.codes";
import { type Payload_Sync_Notif } from "@core/types/sync.types";
import { type BaseDriver } from "@backend/__tests__/drivers/base.driver";
Expand All @@ -12,33 +8,6 @@ import { encodeChannelToken } from "@backend/sync/util/watch.util";
export class SyncControllerDriver {
constructor(private readonly baseDriver: BaseDriver) {}

async waitUntilImportGCalStart<Result = unknown[]>(
websocketClient: ReturnType<BaseDriver["createWebsocketClient"]>,
beforeEvent: () => Promise<unknown> = () => Promise.resolve(),
afterEvent: (...args: void[]) => Promise<Result> = (...args) =>
Promise.resolve(args as Result),
): Promise<Result> {
return this.baseDriver.waitUntilWebsocketEvent<void[], Result>(
websocketClient,
IMPORT_GCAL_START,
beforeEvent,
afterEvent,
);
}

async waitUntilImportGCalEnd<Result = unknown[]>(
websocketClient: ReturnType<BaseDriver["createWebsocketClient"]>,
beforeEvent: () => Promise<unknown> = () => Promise.resolve(),
afterEvent: (...args: [string | undefined]) => Promise<Result> = (
...args
) => Promise.resolve(args as Result),
): Promise<Result> {
return this.baseDriver.waitUntilWebsocketEvent<
[string | undefined],
Result
>(websocketClient, IMPORT_GCAL_END, beforeEvent, afterEvent);
}

async handleGoogleNotification(
{
token,
Expand All @@ -64,12 +33,14 @@ export class SyncControllerDriver {

async importGCal(
session?: { userId: string },
body?: { force?: boolean },
status: Status = Status.NO_CONTENT,
): Promise<
Omit<request.Response, "body"> & { body: { id: string; status: string } }
> {
return request(this.baseDriver.getServerUri())
.post("/api/sync/import-gcal")
.send(body)
.use(this.baseDriver.setSessionPlugin(session))
.expect(status);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { JSONObject } from "supertokens-node/recipe/usermetadata";
import { type UserMetadata } from "@core/types/user.types";
import userMetadataService from "@backend/user/services/user-metadata.service";

export class UserMetadataServiceDriver {
updateUserMetadata(params: {
userId: string;
data: Partial<UserMetadata>;
}): Promise<UserMetadata> {
return userMetadataService.updateUserMetadata(params);
}

fetchUserMetadata(
userId: string,
userContext?: Record<string, JSONObject>,
): Promise<UserMetadata> {
return userMetadataService.fetchUserMetadata(userId, userContext);
}
}
16 changes: 14 additions & 2 deletions packages/backend/src/__tests__/drivers/user.driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import userService from "../../user/services/user.service";

interface CreateUserOptions {
withGoogleRefreshToken?: boolean;
/** When false, creates a user with no Google data (never connected). */
withGoogle?: boolean;
}

export class UserDriver {
Expand Down Expand Up @@ -38,7 +40,7 @@ export class UserDriver {
static async createUser(
options: CreateUserOptions = {},
): Promise<WithId<Schema_User>> {
const { withGoogleRefreshToken = true } = options;
const { withGoogleRefreshToken = true, withGoogle = true } = options;
const gUser = UserDriver.generateGoogleUser();
const gRefreshToken = faker.internet.jwt();

Expand All @@ -49,6 +51,14 @@ export class UserDriver {

const _id = new ObjectId(userId);

// Simulate "user never connected Google" by removing all Google data
if (!withGoogle) {
await mongoService.user.updateOne({ _id }, { $unset: { google: "" } });
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- intentionally omit google from returned user
const { google: _google, ...rest } = user;
return { ...rest, _id };
}

// Remove refresh token if requested (simulates revoked token scenario)
if (!withGoogleRefreshToken) {
await mongoService.user.updateOne(
Expand All @@ -67,6 +77,8 @@ export class UserDriver {
}

static async createUsers(count: number): Promise<Array<WithId<Schema_User>>> {
return Promise.all(Array.from({ length: count }, UserDriver.createUser));
return Promise.all(
Array.from({ length: count }, () => UserDriver.createUser()),
);
}
}
14 changes: 14 additions & 0 deletions packages/backend/src/__tests__/drivers/watch.driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import mongoService from "@backend/common/services/mongo.service";

/**
* Test driver for the watch collection.
* Use this instead of importing mongoService in tests so persistence can be
* swapped (e.g. away from Mongo) without changing test code.
*/
export class WatchDriver {
static deleteManyByUser(
userId: string,
): ReturnType<typeof mongoService.watch.deleteMany> {
return mongoService.watch.deleteMany({ user: userId });
}
}
4 changes: 2 additions & 2 deletions packages/backend/src/auth/schemas/reconnect-google.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ export type ParsedReconnectGoogleParams = {
};

export function parseReconnectGoogleParams(
sessionUserId: string,
compassUserId: string,
gUser: TokenPayload,
oAuthTokens: Pick<Credentials, "refresh_token" | "access_token">,
): ParsedReconnectGoogleParams {
const cUserId = zObjectId
.parse(sessionUserId, { error: () => "Invalid credentials" })
.parse(compassUserId, { error: () => "Invalid credentials" })
.toString();
StringV4Schema.parse(gUser.sub, { error: () => "Invalid Google user ID" });
const refreshToken = StringV4Schema.parse(oAuthTokens.refresh_token, {
Expand Down
Loading
Loading