Repository: SwitchbackTech/compass (ts)

Issue:
Can't sign in after sync expired
### Where did this happen?

Local code with an up-to-date main branch

### Expected Behavior

_No response_

### Current Behavior
User isn't able to sign in. After clicking through the Oauth screen, they are sent back to the login page.

Backend error:

```
 [error] app:error.handler: {"result":"Incremental sync failed because no sync token was found","description":"Developer made a logic error","statusCode":500,"isOperational":true}

500 POST /api/oauth/google 548.928ms Mon, 14 Apr 2025 11:24:20 GMT
```


### Steps to Reproduce

1. Sign in
2. Wait until sync expires (adjust the .env value to make this happen quicker)
3. Sign out (this results in the sync token being deleted) 
4. Try to sign in. This triggers a partial Import → Get Sync
5. Sync not found
6. 500 error returned
7. Not able to sign in

### Possible Solution

Fallback to a full import if the sync token can't be found during a login (partial import)

### Context

As long as our sync-maintenance is working, this is unlikely to be an issue.

However, adding this will help if the sync maintenance has a bug that prevents it from working or if we're testing in a staging environment where the expiration window is short enough that it passes and the user's sync watch is gone.

Pull request description:
✨ Feat: Resync user data if sync token not found during login
Closes #308 

This PR resyncs user data if their sync token couldn't be found during login

----

This pull request introduces several changes to improve the handling of Google synchronization errors and adds comprehensive tests for the `AuthController`. The most important changes include the addition of tests for the `login` method, handling `NoSyncToken` errors during incremental sync, and updating error handling in the `SyncImport` service.

### Tests and Error Handling Improvements:

* [`packages/backend/src/auth/controllers/auth.controller.test.ts`](diffhunk://#diff-138135e8ca28da77f66301292f489e602806936c0543fba4926ca73e0734827cR1-R68): Added unit tests for the `login` method in `AuthController`, including mocks for dependencies and a test case to handle the `NoSyncToken` error.

### Error Handling Enhancements:

* [`packages/backend/src/auth/controllers/auth.controller.ts`](diffhunk://#diff-de776b4489d0d550938dc2afd25cb66628c14ef05aa4bbbaf1accf7491feefc1R154-R166): Updated the `AuthController` to handle `NoSyncToken` errors during the `importIncremental` method call and trigger a resync of Google data.
* [`packages/backend/src/auth/controllers/auth.controller.ts`](diffhunk://#diff-de776b4489d0d550938dc2afd25cb66628c14ef05aa4bbbaf1accf7491feefc1L21-R24): Imported `SyncError` from `error.constants` to support the new error handling logic.
* [`packages/backend/src/sync/services/import/sync.import.ts`](diffhunk://#diff-6a6368e54fd4ea9da9158798c3e2cf5dc4b0d2c38533c2fa0ebf5e5e5d8b4832L185-R184): Changed the error thrown when no sync token is found from `GenericError.DeveloperError` to `SyncError.NoSyncToken` to provide more specific error information.

Patch:
diff --git a/packages/backend/src/auth/controllers/auth.controller.ts b/packages/backend/src/auth/controllers/auth.controller.ts
index 9efbb434..bd6a5a87 100644
--- a/packages/backend/src/auth/controllers/auth.controller.ts
+++ b/packages/backend/src/auth/controllers/auth.controller.ts
@@ -18,7 +18,10 @@ import compassAuthService from "@backend/auth/services/compass.auth.service";
 import GoogleAuthService, {
   getGAuthClientForUser,
 } from "@backend/auth/services/google.auth.service";
-import { GcalError } from "@backend/common/constants/error.constants";
+import {
+  GcalError,
+  SyncError,
+} from "@backend/common/constants/error.constants";
 import { error } from "@backend/common/errors/handlers/error.handler";
 import { isInvalidGoogleToken } from "@backend/common/services/gcal/gcal.utils";
 import {
@@ -148,7 +151,19 @@ class AuthController {
       await updateGoogleRefreshToken(cUserId, gRefreshToken);
     }
 
-    await syncService.importIncremental(cUserId, gcal);
+    try {
+      await syncService.importIncremental(cUserId, gcal);
+    } catch (e) {
+      if (
+        e instanceof Error &&
+        e.message === SyncError.NoSyncToken.description
+      ) {
+        logger.info(
+          `Resyncing google data due to missing sync for user: ${cUserId}`,
+        );
+        await userService.reSyncGoogleData(cUserId);
+      }
+    }
 
     await userService.saveTimeFor("lastLoggedInAt", cUserId);
 
diff --git a/packages/backend/src/sync/services/import/sync.import.ts b/packages/backend/src/sync/services/import/sync.import.ts
index f1e01a9e..c3b582ea 100644
--- a/packages/backend/src/sync/services/import/sync.import.ts
+++ b/packages/backend/src/sync/services/import/sync.import.ts
@@ -11,7 +11,6 @@ import { ENV } from "@backend/common/constants/env.constants";
 import {
   EventError,
   GcalError,
-  GenericError,
   SyncError,
 } from "@backend/common/constants/error.constants";
 import { error } from "@backend/common/errors/handlers/error.handler";
@@ -182,7 +181,7 @@ export class SyncImport {
     }
     if (syncToken === undefined || syncToken === null) {
       throw error(
-        GenericError.DeveloperError,
+        SyncError.NoSyncToken,
         "Incremental sync failed because no sync token was found",
       );
     }


Test patch:
diff --git a/packages/backend/src/auth/controllers/auth.controller.test.ts b/packages/backend/src/auth/controllers/auth.controller.test.ts
new file mode 100644
index 00000000..8d5adc24
--- /dev/null
+++ b/packages/backend/src/auth/controllers/auth.controller.test.ts
@@ -0,0 +1,68 @@
+import { ObjectId } from "mongodb";
+import { WithId } from "mongodb";
+import { gCalendar } from "@core/types/gcal";
+import { Schema_User } from "@core/types/user.types";
+import { SyncError } from "@backend/common/constants/error.constants";
+import mongoService from "@backend/common/services/mongo.service";
+import syncService from "@backend/sync/services/sync.service";
+import userService from "@backend/user/services/user.service";
+import authController from "./auth.controller";
+
+jest.mock("@backend/sync/services/sync.service");
+jest.mock("@backend/user/services/user.service");
+jest.mock("@backend/common/services/mongo.service", () => ({
+  user: {
+    findOneAndUpdate: jest.fn(),
+  },
+}));
+jest.mock("@backend/common/middleware/supertokens.middleware", () => ({
+  initSupertokens: jest.fn(),
+  getSession: jest.fn(),
+}));
+
+describe("AuthController", () => {
+  describe("login", () => {
+    const mockUser: WithId<Schema_User> = {
+      _id: new ObjectId(),
+      google: {
+        gRefreshToken: "old-token",
+      },
+    } as WithId<Schema_User>;
+
+    const mockGcal = {} as gCalendar;
+    const mockNewRefreshToken = "new-token";
+
+    beforeEach(() => {
+      jest.clearAllMocks();
+      // Mock mongoService
+      (mongoService.user.findOneAndUpdate as jest.Mock).mockResolvedValue({
+        value: mockUser,
+      });
+    });
+
+    it("should resync google data when NoSyncToken error occurs", async () => {
+      // Mock syncService to throw NoSyncToken error
+      (syncService.importIncremental as jest.Mock).mockRejectedValue(
+        new Error(SyncError.NoSyncToken.description),
+      );
+
+      // Mock userService methods
+      (userService.reSyncGoogleData as jest.Mock).mockResolvedValue(undefined);
+      (userService.saveTimeFor as jest.Mock).mockResolvedValue(undefined);
+
+      const { cUserId } = await authController.login(
+        mockUser,
+        mockGcal,
+        mockNewRefreshToken,
+      );
+
+      // Verify reSyncGoogleData was called
+      expect(userService.reSyncGoogleData).toHaveBeenCalledWith(
+        mockUser._id.toString(),
+      );
+
+      // Verify it still returns the user id after resyncing
+      expect(cUserId).toBe(mockUser._id.toString());
+    });
+  });
+});