- Updated Dockerfile to include hardcoded environment variables for Next.js build. - Enhanced Google Calendar API integration by extracting user email from id_token and adding scopes for OpenID and email access. - Modified credential management to delete existing credentials before creating new ones in n8n. - Updated dashboard to display connected Google Calendar email and credential details. Story: 4.2 - Melhorar integração com Google Calendar e atualizar Dockerfile 🤖 Generated with [Claude Code](https://claude.com/claude-code)
304 lines
8.2 KiB
TypeScript
304 lines
8.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
// Mock environment variables BEFORE importing module
|
|
process.env.N8N_API_BASE_URL = "https://n8n.test/api/v1";
|
|
process.env.N8N_API_KEY = "test-api-key";
|
|
process.env.N8N_GOOGLE_CREDENTIAL_ID = "cred-123";
|
|
|
|
import {
|
|
createGoogleCredential,
|
|
deleteCredential,
|
|
updateCredentialTokens,
|
|
upsertGoogleCredential,
|
|
} from "../n8n-api";
|
|
|
|
// Mock global fetch
|
|
global.fetch = vi.fn();
|
|
|
|
describe("n8n-api", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("deleteCredential", () => {
|
|
it("deve deletar credencial com sucesso", async () => {
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: true,
|
|
});
|
|
|
|
const result = await deleteCredential("cred-123");
|
|
|
|
expect(result).toBe(true);
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("/credentials/cred-123"),
|
|
expect.objectContaining({
|
|
method: "DELETE",
|
|
headers: expect.objectContaining({
|
|
"X-N8N-API-KEY": expect.any(String),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("deve retornar false quando credencial não existe", async () => {
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: false,
|
|
statusText: "Not Found",
|
|
});
|
|
|
|
const result = await deleteCredential("invalid-id");
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("upsertGoogleCredential", () => {
|
|
it("deve deletar credencial existente e criar nova (DELETE + POST)", async () => {
|
|
const mockResponse = {
|
|
id: "new-cred-456",
|
|
name: "refugio",
|
|
type: "googleCalendarOAuth2Api",
|
|
};
|
|
|
|
// DELETE sucede
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: true,
|
|
});
|
|
|
|
// POST sucede
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => mockResponse,
|
|
});
|
|
|
|
const result = await upsertGoogleCredential(
|
|
"refugio",
|
|
"client-id",
|
|
"client-secret",
|
|
"https://www.googleapis.com/auth/calendar.events",
|
|
"access-token",
|
|
"refresh-token",
|
|
3599,
|
|
);
|
|
|
|
expect(result.id).toBe("new-cred-456");
|
|
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
// Primeiro DELETE
|
|
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.stringContaining("/credentials/cred-123"),
|
|
expect.objectContaining({ method: "DELETE" }),
|
|
);
|
|
// Depois POST
|
|
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.stringContaining("/credentials"),
|
|
expect.objectContaining({ method: "POST" }),
|
|
);
|
|
});
|
|
|
|
it("deve criar credencial via POST quando não existe N8N_GOOGLE_CREDENTIAL_ID", async () => {
|
|
// Temporariamente remove credential ID
|
|
const originalId = process.env.N8N_GOOGLE_CREDENTIAL_ID;
|
|
delete process.env.N8N_GOOGLE_CREDENTIAL_ID;
|
|
|
|
const mockResponse = {
|
|
id: "new-cred-789",
|
|
name: "refugio",
|
|
type: "googleCalendarOAuth2Api",
|
|
};
|
|
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => mockResponse,
|
|
});
|
|
|
|
const result = await upsertGoogleCredential(
|
|
"refugio",
|
|
"client-id",
|
|
"client-secret",
|
|
"https://www.googleapis.com/auth/calendar.events",
|
|
);
|
|
|
|
expect(result.id).toBe("new-cred-789");
|
|
expect(global.fetch).toHaveBeenCalledTimes(1); // Apenas POST
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("/credentials"),
|
|
expect.objectContaining({ method: "POST" }),
|
|
);
|
|
|
|
// Restaura credential ID
|
|
process.env.N8N_GOOGLE_CREDENTIAL_ID = originalId;
|
|
});
|
|
});
|
|
|
|
describe("createGoogleCredential", () => {
|
|
it("deve criar credencial com dados corretos", async () => {
|
|
const mockResponse = {
|
|
id: "new-cred-123",
|
|
name: "refugio",
|
|
type: "googleOAuth2Api",
|
|
};
|
|
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => mockResponse,
|
|
});
|
|
|
|
const result = await createGoogleCredential(
|
|
"refugio",
|
|
"client-id",
|
|
"client-secret",
|
|
"https://www.googleapis.com/auth/calendar.events",
|
|
);
|
|
|
|
expect(result.id).toBe("new-cred-123");
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("/credentials"),
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
headers: expect.objectContaining({
|
|
"X-N8N-API-KEY": expect.any(String),
|
|
"Content-Type": "application/json",
|
|
}),
|
|
body: expect.stringContaining('"name":"refugio"'),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("updateCredentialTokens", () => {
|
|
it("deve atualizar tokens com sucesso", async () => {
|
|
const mockResponse = {
|
|
id: "cred-123",
|
|
name: "refugio",
|
|
};
|
|
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => mockResponse,
|
|
});
|
|
|
|
const result = await updateCredentialTokens(
|
|
"cred-123",
|
|
"access-token",
|
|
"refresh-token",
|
|
3599,
|
|
);
|
|
|
|
expect(result.id).toBe("cred-123");
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("/credentials/cred-123"),
|
|
expect.objectContaining({
|
|
method: "PUT",
|
|
body: expect.stringContaining('"access_token":"access-token"'),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("deve lançar erro quando credencial não existe (404)", async () => {
|
|
const mockError = { message: "Credential not found" };
|
|
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: false,
|
|
json: async () => mockError,
|
|
});
|
|
|
|
await expect(
|
|
updateCredentialTokens("invalid-id", "token", "refresh", 3599),
|
|
).rejects.toThrow("Failed to update credential");
|
|
});
|
|
});
|
|
|
|
describe("createGoogleCredential - error scenarios", () => {
|
|
it("deve lançar erro quando schema está inválido (400)", async () => {
|
|
const mockError = {
|
|
message: "Validation error",
|
|
errors: ["Missing required field: clientId"],
|
|
};
|
|
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: false,
|
|
json: async () => mockError,
|
|
});
|
|
|
|
await expect(
|
|
createGoogleCredential("test", "", "", "scopes"),
|
|
).rejects.toThrow("Failed to create credential");
|
|
});
|
|
|
|
it("deve lançar erro quando API key é inválida (401)", async () => {
|
|
const mockError = { message: "Unauthorized" };
|
|
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
json: async () => mockError,
|
|
});
|
|
|
|
await expect(
|
|
createGoogleCredential("test", "id", "secret", "scopes"),
|
|
).rejects.toThrow("Failed to create credential");
|
|
});
|
|
});
|
|
|
|
describe("upsertGoogleCredential - error scenarios", () => {
|
|
it("deve lançar erro quando POST falha após DELETE", async () => {
|
|
const mockError = { message: "Internal server error" };
|
|
|
|
// DELETE sucede
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: true,
|
|
});
|
|
|
|
// POST falha
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: false,
|
|
json: async () => mockError,
|
|
});
|
|
|
|
await expect(
|
|
upsertGoogleCredential(
|
|
"test",
|
|
"id",
|
|
"secret",
|
|
"scopes",
|
|
"token",
|
|
"refresh",
|
|
3599,
|
|
),
|
|
).rejects.toThrow("Failed to create credential");
|
|
});
|
|
|
|
it("deve continuar tentando criar mesmo se DELETE falhar", async () => {
|
|
const mockResponse = {
|
|
id: "new-cred-999",
|
|
name: "refugio",
|
|
type: "googleCalendarOAuth2Api",
|
|
};
|
|
|
|
// DELETE falha (credencial não existe)
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: false,
|
|
statusText: "Not Found",
|
|
});
|
|
|
|
// POST sucede
|
|
(global.fetch as any).mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => mockResponse,
|
|
});
|
|
|
|
const result = await upsertGoogleCredential(
|
|
"refugio",
|
|
"client-id",
|
|
"client-secret",
|
|
"https://www.googleapis.com/auth/calendar.events",
|
|
);
|
|
|
|
expect(result.id).toBe("new-cred-999");
|
|
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
});
|