-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Dont use stale label ids on outlook. e2e tests #1107
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
213 changes: 213 additions & 0 deletions
213
apps/web/__tests__/e2e/labeling/gmail-thread-label-removal.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| /** | ||
| * E2E tests for Gmail thread label removal | ||
| * | ||
| * These tests verify that conversation status labels (To Reply, Awaiting Reply, FYI, Actioned) | ||
| * are mutually exclusive within a thread - when applying a new label, existing conflicting | ||
| * labels should be removed from ALL messages in the thread. | ||
| * | ||
| * Usage: | ||
| * pnpm test-e2e gmail-thread-label-removal | ||
| */ | ||
|
|
||
| import { describe, test, expect, beforeAll, afterAll, vi } from "vitest"; | ||
| import prisma from "@/utils/prisma"; | ||
| import { createEmailProvider } from "@/utils/email/provider"; | ||
| import type { EmailProvider } from "@/utils/email/types"; | ||
| import type { ParsedMessage } from "@/utils/types"; | ||
| import { getRuleLabel } from "@/utils/rule/consts"; | ||
| import { SystemType } from "@/generated/prisma/enums"; | ||
| import { removeConflictingThreadStatusLabels } from "@/utils/reply-tracker/label-helpers"; | ||
| import { createScopedLogger } from "@/utils/logger"; | ||
| import { findThreadWithMultipleMessages } from "./helpers"; | ||
|
|
||
| const RUN_E2E_TESTS = process.env.RUN_E2E_TESTS; | ||
| const TEST_GMAIL_EMAIL = process.env.TEST_GMAIL_EMAIL; | ||
|
|
||
| vi.mock("server-only", () => ({})); | ||
|
|
||
| describe.skipIf(!RUN_E2E_TESTS)("Gmail Thread Label Removal E2E Tests", () => { | ||
| let provider: EmailProvider; | ||
| let emailAccountId: string; | ||
| let testThreadId: string; | ||
| let testMessages: ParsedMessage[]; | ||
| const createdTestLabels: string[] = []; | ||
| const logger = createScopedLogger("e2e-test"); | ||
|
|
||
| beforeAll(async () => { | ||
| if (!TEST_GMAIL_EMAIL) { | ||
| throw new Error("TEST_GMAIL_EMAIL env var is required"); | ||
| } | ||
|
|
||
| const emailAccount = await prisma.emailAccount.findFirst({ | ||
| where: { | ||
| email: TEST_GMAIL_EMAIL, | ||
| account: { provider: "google" }, | ||
| }, | ||
| include: { account: true }, | ||
| }); | ||
|
|
||
| if (!emailAccount) { | ||
| throw new Error(`No Gmail account found for ${TEST_GMAIL_EMAIL}`); | ||
| } | ||
|
|
||
| emailAccountId = emailAccount.id; | ||
| provider = await createEmailProvider({ | ||
| emailAccountId: emailAccount.id, | ||
| provider: "google", | ||
| }); | ||
|
|
||
| // Find a suitable test thread with 2+ messages | ||
| const { threadId, messages } = await findThreadWithMultipleMessages( | ||
| provider, | ||
| 2, | ||
| ); | ||
| testThreadId = threadId; | ||
| testMessages = messages; | ||
| }, 60_000); | ||
|
|
||
| afterAll(async () => { | ||
| // Clean up test labels | ||
| for (const labelName of createdTestLabels) { | ||
| try { | ||
| const label = await provider.getLabelByName(labelName); | ||
| if (label) { | ||
| await provider.removeThreadLabel(testThreadId, label.id); | ||
| await provider.deleteLabel(label.id); | ||
| } | ||
| } catch { | ||
| // Ignore cleanup errors | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| // ============================================ | ||
| // TEST 1: Provider Level - removeThreadLabels() | ||
| // ============================================ | ||
| describe("Provider Level: removeThreadLabels()", () => { | ||
| test("should remove labels from thread", async () => { | ||
| expect( | ||
| testMessages.length, | ||
| "Test requires a thread with 2+ messages. Reply to an email in the test inbox to create one.", | ||
| ).toBeGreaterThanOrEqual(2); | ||
|
|
||
| // Create test label | ||
| const testLabelName = `E2E-ThreadRemoval-${Date.now()}`; | ||
| createdTestLabels.push(testLabelName); | ||
| const label = await provider.createLabel(testLabelName); | ||
|
|
||
| // Apply label to the thread (Gmail applies to all messages in thread) | ||
| await provider.labelMessage({ | ||
| messageId: testMessages[0].id, | ||
| labelId: label.id, | ||
| labelName: label.name, | ||
| }); | ||
|
|
||
| // Verify the thread has the label | ||
| const msgBefore = await provider.getMessage(testMessages[0].id); | ||
| expect(msgBefore.labelIds).toContain(label.id); | ||
|
|
||
| // Remove the label from the thread using removeThreadLabels | ||
| await provider.removeThreadLabels(testThreadId, [label.id]); | ||
|
|
||
| // Verify the thread no longer has the label | ||
| const msgAfter = await provider.getMessage(testMessages[0].id); | ||
| expect(msgAfter.labelIds).not.toContain(label.id); | ||
| }, 60_000); | ||
|
|
||
| test("should remove multiple labels from thread", async () => { | ||
| expect( | ||
| testMessages.length, | ||
| "Test requires a thread with 2+ messages. Reply to an email in the test inbox to create one.", | ||
| ).toBeGreaterThanOrEqual(2); | ||
|
|
||
| // Create multiple test labels | ||
| const label1Name = `E2E-Multi1-${Date.now()}`; | ||
| const label2Name = `E2E-Multi2-${Date.now()}`; | ||
| createdTestLabels.push(label1Name, label2Name); | ||
|
|
||
| const label1 = await provider.createLabel(label1Name); | ||
| const label2 = await provider.createLabel(label2Name); | ||
|
|
||
| // Apply both labels to the thread | ||
| await provider.labelMessage({ | ||
| messageId: testMessages[0].id, | ||
| labelId: label1.id, | ||
| labelName: label1.name, | ||
| }); | ||
| await provider.labelMessage({ | ||
| messageId: testMessages[0].id, | ||
| labelId: label2.id, | ||
| labelName: label2.name, | ||
| }); | ||
|
|
||
| // Verify thread has both labels | ||
| const msgBefore = await provider.getMessage(testMessages[0].id); | ||
| expect(msgBefore.labelIds).toContain(label1.id); | ||
| expect(msgBefore.labelIds).toContain(label2.id); | ||
|
|
||
| // Remove both labels from the thread | ||
| await provider.removeThreadLabels(testThreadId, [label1.id, label2.id]); | ||
|
|
||
| // Verify thread has neither label | ||
| const msgAfter = await provider.getMessage(testMessages[0].id); | ||
| expect(msgAfter.labelIds).not.toContain(label1.id); | ||
| expect(msgAfter.labelIds).not.toContain(label2.id); | ||
| }, 60_000); | ||
| }); | ||
|
|
||
| // ============================================ | ||
| // TEST 2: Label Helpers Level - removeConflictingThreadStatusLabels() | ||
| // ============================================ | ||
| describe("Label Helpers Level: removeConflictingThreadStatusLabels()", () => { | ||
| test("should remove conflicting conversation status labels when applying a new status", async () => { | ||
| expect( | ||
| testMessages.length, | ||
| "Test requires a thread with 2+ messages. Reply to an email in the test inbox to create one.", | ||
| ).toBeGreaterThanOrEqual(2); | ||
|
|
||
| // Create conversation status labels | ||
| const toReplyLabelName = getRuleLabel(SystemType.TO_REPLY); | ||
| const awaitingReplyLabelName = getRuleLabel(SystemType.AWAITING_REPLY); | ||
| createdTestLabels.push(toReplyLabelName, awaitingReplyLabelName); | ||
|
|
||
| const toReplyLabel = await provider.createLabel(toReplyLabelName); | ||
| const awaitingReplyLabel = await provider.createLabel( | ||
| awaitingReplyLabelName, | ||
| ); | ||
|
|
||
| // Apply "To Reply" label to thread | ||
| await provider.labelMessage({ | ||
| messageId: testMessages[0].id, | ||
| labelId: toReplyLabel.id, | ||
| labelName: toReplyLabel.name, | ||
| }); | ||
|
|
||
| // Apply "Awaiting Reply" label to thread | ||
| await provider.labelMessage({ | ||
| messageId: testMessages[0].id, | ||
| labelId: awaitingReplyLabel.id, | ||
| labelName: awaitingReplyLabel.name, | ||
| }); | ||
|
|
||
| // Verify labels are applied | ||
| const msgBefore = await provider.getMessage(testMessages[0].id); | ||
| expect(msgBefore.labelIds).toContain(toReplyLabel.id); | ||
| expect(msgBefore.labelIds).toContain(awaitingReplyLabel.id); | ||
|
|
||
| // Call removeConflictingThreadStatusLabels with FYI status | ||
| // This should remove TO_REPLY and AWAITING_REPLY labels from the thread | ||
| await removeConflictingThreadStatusLabels({ | ||
| emailAccountId, | ||
| threadId: testThreadId, | ||
| systemType: SystemType.FYI, | ||
| provider, | ||
| logger, | ||
| }); | ||
|
|
||
| // Verify conflicting labels are removed | ||
| const msgAfter = await provider.getMessage(testMessages[0].id); | ||
| expect(msgAfter.labelIds).not.toContain(toReplyLabel.id); | ||
| expect(msgAfter.labelIds).not.toContain(awaitingReplyLabel.id); | ||
| }, 60_000); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import type { EmailProvider } from "@/utils/email/types"; | ||
| import type { ParsedMessage } from "@/utils/types"; | ||
|
|
||
| /** | ||
| * Finds a thread with at least minMessages messages from the inbox. | ||
| * Looks through recent inbox messages and finds one with multiple messages in thread. | ||
| */ | ||
| export async function findThreadWithMultipleMessages( | ||
| provider: EmailProvider, | ||
| minMessages = 2, | ||
| ): Promise<{ threadId: string; messages: ParsedMessage[] }> { | ||
| const inboxMessages = await provider.getInboxMessages(50); | ||
|
|
||
| // Group by threadId and find one with enough messages | ||
| const threadIds = [...new Set(inboxMessages.map((m) => m.threadId))]; | ||
|
|
||
| for (const threadId of threadIds) { | ||
| const messages = await provider.getThreadMessages(threadId); | ||
| if (messages.length >= minMessages) { | ||
| return { threadId, messages }; | ||
| } | ||
| } | ||
|
|
||
| throw new Error( | ||
| `TEST PREREQUISITE NOT MET: No thread found with ${minMessages}+ messages. ` + | ||
| "Send an email to the test account and reply to it to create a multi-message thread.", | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.