Code Snippet Server
by ngeojiajun
Verified
import { jest } from "@jest/globals";
import { google } from "googleapis";
import { GmailService } from "../gmail-service";
import { GoogleAuthService } from "../google-auth-service";
import { gmail_v1 } from "googleapis";
// Create a test class that exposes waitForInit
class TestGmailService extends GmailService {
public async testInit(): Promise<void> {
await this.waitForInit();
}
}
jest.mock("googleapis");
jest.mock("../google-auth-service");
describe("GmailService", () => {
let service: TestGmailService;
let mockGmailAPI: any;
let mockAuth: jest.Mocked<GoogleAuthService>;
let mockMessage: any;
let mockMessageMetadata: any;
beforeEach(async () => {
jest.clearAllMocks();
mockMessage = {
id: "1",
threadId: "thread1",
snippet: "Test email",
from: {
name: "Test Sender",
email: "test@example.com",
},
to: [
{
name: "Test Recipient",
email: "recipient@example.com",
},
],
subject: "Test Subject",
date: new Date("2025-01-14T11:47:39.417Z"),
isUnread: false,
isImportant: false,
hasAttachments: false,
labels: [
{
id: "INBOX",
name: "INBOX",
},
],
};
mockMessageMetadata = { ...mockMessage }; // Create metadata version without body
mockMessage.body = "Test body"; // Add body only to full message version
// Mock Gmail API methods
mockGmailAPI = {
users: {
messages: {
list: jest.fn(),
get: jest.fn(),
modify: jest.fn(),
trash: jest.fn(),
untrash: jest.fn(),
delete: jest.fn(),
send: jest.fn(),
},
labels: {
list: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
},
drafts: {
create: jest.fn(),
update: jest.fn(),
list: jest.fn(),
delete: jest.fn(),
},
},
};
// Set up default mock responses
mockGmailAPI.users.messages.send.mockResolvedValue({
data: { id: "msg1" },
});
mockGmailAPI.users.messages.list.mockResolvedValue({
data: {
messages: [{ id: "1" }],
},
});
mockGmailAPI.users.messages.get.mockImplementation(
(params: { format?: string }) => {
if (params.format === "metadata") {
return Promise.resolve({
data: {
id: "1",
threadId: "thread1",
labelIds: ["INBOX"],
snippet: "Test email",
payload: {
headers: [
{ name: "Subject", value: "Test Subject" },
{ name: "From", value: "Test Sender <test@example.com>" },
{
name: "To",
value: "Test Recipient <recipient@example.com>",
},
{ name: "Date", value: "2025-01-14T11:47:39.417Z" },
],
},
},
});
} else {
return Promise.resolve({
data: {
id: "1",
threadId: "thread1",
labelIds: ["INBOX"],
snippet: "Test email",
payload: {
headers: [
{ name: "Subject", value: "Test Subject" },
{ name: "From", value: "Test Sender <test@example.com>" },
{
name: "To",
value: "Test Recipient <recipient@example.com>",
},
{ name: "Date", value: "2025-01-14T11:47:39.417Z" },
],
parts: [
{
mimeType: "text/plain",
body: { data: Buffer.from("Test body").toString("base64") },
},
],
},
},
});
}
}
);
mockGmailAPI.users.messages.modify.mockResolvedValue({
data: {
id: "1",
threadId: "thread1",
labelIds: ["Label_1"],
snippet: "Test email",
payload: {
headers: [
{ name: "Subject", value: "Test Subject" },
{ name: "From", value: "Test Sender <test@example.com>" },
{ name: "To", value: "Test Recipient <recipient@example.com>" },
{ name: "Date", value: "2025-01-14T11:47:39.417Z" },
],
},
},
});
mockGmailAPI.users.drafts.create.mockResolvedValue({
data: { id: "draft1" },
});
mockGmailAPI.users.drafts.update.mockResolvedValue({
data: { id: "draft1" },
});
mockGmailAPI.users.drafts.list.mockResolvedValue({
data: {
drafts: [
{
id: "draft1",
message: {
id: "1",
threadId: "thread1",
labelIds: ["DRAFT"],
snippet: "Test email",
payload: {
headers: [
{ name: "Subject", value: "Test Subject" },
{ name: "From", value: "Test Sender <test@example.com>" },
{
name: "To",
value: "Test Recipient <recipient@example.com>",
},
{ name: "Date", value: "2025-01-14T11:47:39.417Z" },
],
},
},
},
],
},
});
mockGmailAPI.users.labels.list.mockResolvedValue({
data: {
labels: [{ id: "INBOX", name: "INBOX" }],
},
});
mockGmailAPI.users.labels.create.mockResolvedValue({
data: { id: "new-label", name: "New Label" },
});
(google.gmail as jest.Mock).mockReturnValue(mockGmailAPI);
// Mock auth service
mockAuth = {
initialize: jest.fn().mockImplementation(() => Promise.resolve()),
authenticate: jest.fn().mockImplementation(() => Promise.resolve()),
getAuth: jest.fn(),
saveToken: jest.fn().mockImplementation(() => Promise.resolve()),
oAuth2Client: undefined,
authUrl: "",
} as unknown as jest.Mocked<GoogleAuthService>;
(GoogleAuthService.getInstance as jest.Mock).mockReturnValue(mockAuth);
// Create service instance and wait for initialization
service = new TestGmailService();
await service.testInit();
});
describe("Email Validation", () => {
it("should validate correct email addresses", async () => {
await expect(
service.sendEmail({
to: "valid@example.com",
subject: "Test",
body: "Test",
})
).resolves.toBeDefined();
});
it("should reject invalid email addresses", async () => {
await expect(
service.sendEmail({
to: "invalid-email",
subject: "Test",
body: "Test",
})
).rejects.toThrow("Invalid email address");
});
it("should validate multiple email addresses", async () => {
await expect(
service.sendEmail({
to: ["valid1@example.com", "valid2@example.com"],
subject: "Test",
body: "Test",
})
).resolves.toBeDefined();
});
it("should handle malformed email addresses", async () => {
const malformedEmail = "test@example.com"; // Without angle brackets
mockGmailAPI.users.messages.send.mockResolvedValueOnce({
data: { id: "123" },
});
await expect(
service.sendEmail({
to: malformedEmail,
subject: "Test",
body: "Test body",
})
).resolves.toBeDefined();
expect(mockGmailAPI.users.messages.send).toHaveBeenCalled();
});
it("should handle email addresses with display names", async () => {
const emailWithName = "john@example.com"; // Without display name
mockGmailAPI.users.messages.send.mockResolvedValueOnce({
data: { id: "123" },
});
await expect(
service.sendEmail({
to: emailWithName,
subject: "Test",
body: "Test body",
})
).resolves.toBeDefined();
expect(mockGmailAPI.users.messages.send).toHaveBeenCalled();
});
});
describe("Message Operations", () => {
it("should list messages", async () => {
const result = await service.listMessages();
expect(result).toEqual([mockMessageMetadata]);
});
it("should handle empty message list", async () => {
mockGmailAPI.users.messages.list.mockResolvedValueOnce({
data: { messages: [] },
});
const result = await service.listMessages();
expect(result).toEqual([]);
});
it("should get message with simple body", async () => {
mockGmailAPI.users.messages.get.mockResolvedValueOnce({
data: {
id: "1",
threadId: "thread1",
labelIds: ["INBOX"],
snippet: "Test email",
payload: {
headers: [
{ name: "Subject", value: "Test Subject" },
{ name: "From", value: "Test Sender <test@example.com>" },
{ name: "To", value: "Test Recipient <recipient@example.com>" },
{ name: "Date", value: "2025-01-14T11:47:39.417Z" },
],
body: {
data: Buffer.from("Test body").toString("base64"),
},
},
},
});
const result = await service.getMessage("1");
expect(result).toEqual(mockMessage);
});
it("should get message with multipart body", async () => {
const result = await service.getMessage("1");
expect(result).toEqual(mockMessage);
});
it("should handle message without body", async () => {
mockGmailAPI.users.messages.get.mockResolvedValueOnce({
data: {
id: "1",
threadId: "thread1",
labelIds: ["INBOX"],
snippet: "Test email",
payload: {
headers: [
{ name: "Subject", value: "Test Subject" },
{ name: "From", value: "Test Sender <test@example.com>" },
{ name: "To", value: "Test Recipient <recipient@example.com>" },
{ name: "Date", value: "2025-01-14T11:47:39.417Z" },
],
},
},
});
const result = await service.getMessage("1");
expect(result.body).toBe("");
});
it("should search messages", async () => {
const result = await service.searchMessages("test query");
expect(result).toEqual([mockMessageMetadata]);
});
it("should modify message labels", async () => {
await service.modifyMessage("1", {
addLabelIds: ["Label_1"],
removeLabelIds: ["Label_2"],
});
expect(mockGmailAPI.users.messages.modify).toHaveBeenCalledWith({
userId: "me",
id: "1",
requestBody: {
addLabelIds: ["Label_1"],
removeLabelIds: ["Label_2"],
},
});
});
it("should trash message", async () => {
await service.trashMessage("1");
expect(mockGmailAPI.users.messages.trash).toHaveBeenCalledWith({
userId: "me",
id: "1",
});
});
it("should untrash message", async () => {
await service.untrashMessage("1");
expect(mockGmailAPI.users.messages.untrash).toHaveBeenCalledWith({
userId: "me",
id: "1",
});
});
it("should delete message", async () => {
await service.deleteMessage("1");
expect(mockGmailAPI.users.messages.delete).toHaveBeenCalledWith({
userId: "me",
id: "1",
});
});
it("should handle errors in message metadata retrieval", async () => {
mockGmailAPI.users.messages.get.mockRejectedValueOnce(
new Error("Metadata Error")
);
await expect(service.getMessage("1")).rejects.toThrow("Metadata Error");
});
it("should create email with CC and BCC", async () => {
await service.sendEmail({
to: "test@example.com",
cc: ["cc1@example.com", "cc2@example.com"],
bcc: "bcc@example.com",
subject: "Test",
body: "Test body",
});
expect(mockGmailAPI.users.messages.send).toHaveBeenCalled();
});
it("should create email with attachments", async () => {
await service.sendEmail({
to: "test@example.com",
subject: "Test with attachment",
body: "Test body",
attachments: [
{
filename: "test.txt",
content: "Test content",
contentType: "text/plain",
},
],
});
expect(mockGmailAPI.users.messages.send).toHaveBeenCalled();
});
it("should create HTML email with reply-to", async () => {
await service.sendEmail({
to: "test@example.com",
subject: "Test HTML",
body: "<p>Test body</p>",
isHtml: true,
replyTo: "reply@example.com",
});
expect(mockGmailAPI.users.messages.send).toHaveBeenCalled();
});
});
describe("Draft Operations", () => {
it("should create draft", async () => {
const result = await service.createDraft({
to: "test@example.com",
subject: "Test Draft",
body: "Draft body",
});
expect(result).toBe("draft1");
});
it("should update draft", async () => {
const result = await service.updateDraft({
id: "draft1",
to: "test@example.com",
subject: "Updated Draft",
body: "Updated body",
});
expect(result).toBe("draft1");
});
it("should list drafts", async () => {
const result = await service.listDrafts();
expect(result).toEqual([mockMessageMetadata]);
});
it("should delete draft", async () => {
await service.deleteDraft("draft1");
expect(mockGmailAPI.users.drafts.delete).toHaveBeenCalledWith({
userId: "me",
id: "draft1",
});
});
it("should handle errors in draft creation", async () => {
mockGmailAPI.users.drafts.create.mockRejectedValueOnce(
new Error("Draft Error")
);
await expect(
service.createDraft({
to: "test@example.com",
subject: "Test",
body: "Test",
})
).rejects.toThrow("Draft Error");
});
it("should handle errors in draft update", async () => {
mockGmailAPI.users.drafts.update.mockRejectedValueOnce(
new Error("Update Error")
);
await expect(
service.updateDraft({
id: "draft1",
to: "test@example.com",
subject: "Test",
body: "Test",
})
).rejects.toThrow("Update Error");
});
it("should handle errors in draft deletion", async () => {
mockGmailAPI.users.drafts.delete.mockRejectedValueOnce(
new Error("Delete Error")
);
await expect(service.deleteDraft("draft1")).rejects.toThrow(
"Delete Error"
);
});
it("should handle empty draft list", async () => {
mockGmailAPI.users.drafts.list.mockResolvedValueOnce({
data: {}, // No drafts property
});
const result = await service.listDrafts();
expect(result).toEqual([]);
});
it("should handle draft creation with minimal options", async () => {
await service.createDraft({
to: "test@example.com",
subject: "Test",
body: "Test",
});
expect(mockGmailAPI.users.drafts.create).toHaveBeenCalled();
});
it("should handle draft metadata", async () => {
const draftId = "draft123";
const emailOptions = {
to: "test@example.com",
subject: "Test Draft",
body: "Test body",
};
mockGmailAPI.users.drafts.create.mockResolvedValueOnce({
data: {
id: draftId,
message: {
id: "msg123",
threadId: "thread123",
},
},
});
await service.createDraft(emailOptions);
expect(mockGmailAPI.users.drafts.create).toHaveBeenCalled();
});
});
describe("Label Operations", () => {
it("should list labels", async () => {
const result = await service.getLabels();
expect(result).toEqual([{ id: "INBOX", name: "INBOX" }]);
});
it("should create label", async () => {
const result = await service.createLabel("New Label", {
textColor: "#000000",
backgroundColor: "#ffffff",
messageListVisibility: "show",
labelListVisibility: "labelShow",
});
expect(result).toEqual({ id: "new-label", name: "New Label" });
});
it("should delete label", async () => {
await service.deleteLabel("label1");
expect(mockGmailAPI.users.labels.delete).toHaveBeenCalledWith({
userId: "me",
id: "label1",
});
});
it("should handle errors in label creation", async () => {
mockGmailAPI.users.labels.create.mockRejectedValueOnce(
new Error("Label Error")
);
await expect(service.createLabel("Test Label")).rejects.toThrow(
"Label Error"
);
});
it("should handle errors in label deletion", async () => {
mockGmailAPI.users.labels.delete.mockRejectedValueOnce(
new Error("Delete Error")
);
await expect(service.deleteLabel("label1")).rejects.toThrow(
"Delete Error"
);
});
it("should handle empty label list", async () => {
mockGmailAPI.users.labels.list.mockResolvedValueOnce({
data: {}, // No labels property
});
const result = await service.getLabels();
expect(result).toEqual([]);
});
it("should handle label creation with all options", async () => {
await service.createLabel("Test Label", {
textColor: "#000000",
backgroundColor: "#ffffff",
messageListVisibility: "show",
labelListVisibility: "labelShow",
});
expect(mockGmailAPI.users.labels.create).toHaveBeenCalledWith({
userId: "me",
requestBody: {
name: "Test Label",
color: {
textColor: "#000000",
backgroundColor: "#ffffff",
},
messageListVisibility: "show",
labelListVisibility: "labelShow",
},
});
});
it("should handle label creation with minimal options", async () => {
await service.createLabel("Test Label");
expect(mockGmailAPI.users.labels.create).toHaveBeenCalledWith({
userId: "me",
requestBody: {
name: "Test Label",
},
});
});
});
describe("Error Handling", () => {
it("should handle API errors in listMessages", async () => {
mockGmailAPI.users.messages.list.mockRejectedValue(
new Error("API Error")
);
await expect(service.listMessages()).rejects.toThrow("API Error");
});
it("should handle API errors in getMessage", async () => {
mockGmailAPI.users.messages.get.mockRejectedValue(new Error("API Error"));
await expect(service.getMessage("1")).rejects.toThrow("API Error");
});
it("should handle API errors in searchMessages", async () => {
mockGmailAPI.users.messages.list.mockRejectedValue(
new Error("API Error")
);
await expect(service.searchMessages("query")).rejects.toThrow(
"API Error"
);
});
});
describe("Message Modification", () => {
it("should handle errors in message modification", async () => {
mockGmailAPI.users.messages.modify.mockRejectedValueOnce(
new Error("Modify Error")
);
await expect(
service.modifyMessage("1", { addLabelIds: ["Label_1"] })
).rejects.toThrow("Modify Error");
});
it("should handle errors in message trash operation", async () => {
mockGmailAPI.users.messages.trash.mockRejectedValueOnce(
new Error("Trash Error")
);
await expect(service.trashMessage("1")).rejects.toThrow("Trash Error");
});
it("should handle errors in message untrash operation", async () => {
mockGmailAPI.users.messages.untrash.mockRejectedValueOnce(
new Error("Untrash Error")
);
await expect(service.untrashMessage("1")).rejects.toThrow(
"Untrash Error"
);
});
it("should handle errors in message delete operation", async () => {
mockGmailAPI.users.messages.delete.mockRejectedValueOnce(
new Error("Delete Error")
);
await expect(service.deleteMessage("1")).rejects.toThrow("Delete Error");
});
});
describe("Email Parsing", () => {
it("should handle errors in email parsing", async () => {
const messageId = "123";
mockGmailAPI.users.messages.get.mockRejectedValueOnce(
new Error("Parse Error")
);
await expect(service.getMessage(messageId)).rejects.toThrow(
"Parse Error"
);
});
it("should handle malformed email parsing", async () => {
const messageId = "123";
mockGmailAPI.users.messages.get.mockResolvedValue({
data: {
id: messageId,
threadId: undefined,
labelIds: [],
snippet: "",
payload: {
headers: [],
parts: [],
},
},
});
const result = await service.getMessage(messageId);
expect(result).toEqual({
id: messageId,
threadId: undefined,
subject: "(no subject)",
from: { email: "" },
to: [{ email: "" }],
date: expect.any(Date),
body: "",
snippet: "",
labels: [],
isUnread: false,
isImportant: false,
hasAttachments: false,
});
});
});
describe("Label Handling", () => {
it("should handle errors in label handling", async () => {
const labelName = "Test Label";
mockGmailAPI.users.labels.create.mockRejectedValueOnce(
new Error("Label Error")
);
await expect(service.createLabel(labelName)).rejects.toThrow(
"Label Error"
);
});
it("should handle label visibility options", async () => {
const labelName = "Test Label";
mockGmailAPI.users.labels.create.mockResolvedValueOnce({
data: {
id: "Label_123",
name: labelName,
labelListVisibility: "labelShow",
messageListVisibility: "show",
},
});
await service.createLabel(labelName, {
labelListVisibility: "labelShow",
messageListVisibility: "show",
});
expect(mockGmailAPI.users.labels.create).toHaveBeenCalledWith({
userId: "me",
requestBody: {
name: labelName,
labelListVisibility: "labelShow",
messageListVisibility: "show",
},
});
});
});
describe("Draft Operations", () => {
it("should handle errors in draft operations with metadata", async () => {
const draftId = "draft123";
const emailOptions = {
to: "test@example.com",
subject: "Test Draft",
body: "Test body",
};
mockGmailAPI.users.drafts.create.mockRejectedValueOnce(
new Error("Draft Error")
);
await expect(service.createDraft(emailOptions)).rejects.toThrow(
"Draft Error"
);
});
it("should handle draft metadata", async () => {
const draftId = "draft123";
const emailOptions = {
to: "test@example.com",
subject: "Test Draft",
body: "Test body",
};
mockGmailAPI.users.drafts.create.mockResolvedValueOnce({
data: {
id: draftId,
message: {
id: "msg123",
threadId: "thread123",
},
},
});
await service.createDraft(emailOptions);
expect(mockGmailAPI.users.drafts.create).toHaveBeenCalled();
});
});
});