- Updated init.js, ai-services-unified.js, user-management.js, telemetry-submission.js, and .taskmasterconfig to support Supabase authentication flow and authenticated gateway calls
402 lines
12 KiB
JavaScript
402 lines
12 KiB
JavaScript
/**
|
|
* Unit Tests for Telemetry Submission Service - Task 90.2
|
|
* Tests the secure telemetry submission with gateway integration
|
|
*/
|
|
|
|
import { jest } from "@jest/globals";
|
|
|
|
// Mock config-manager before importing submitTelemetryData
|
|
jest.unstable_mockModule(
|
|
"../../../../scripts/modules/config-manager.js",
|
|
() => ({
|
|
getConfig: jest.fn(),
|
|
getDebugFlag: jest.fn(() => false),
|
|
getLogLevel: jest.fn(() => "info"),
|
|
getMainProvider: jest.fn(() => "openai"),
|
|
getMainModelId: jest.fn(() => "gpt-4"),
|
|
getResearchProvider: jest.fn(() => "openai"),
|
|
getResearchModelId: jest.fn(() => "gpt-4"),
|
|
getFallbackProvider: jest.fn(() => "openai"),
|
|
getFallbackModelId: jest.fn(() => "gpt-3.5-turbo"),
|
|
getParametersForRole: jest.fn(() => ({
|
|
maxTokens: 4000,
|
|
temperature: 0.7,
|
|
})),
|
|
getUserId: jest.fn(() => "test-user-id"),
|
|
MODEL_MAP: {},
|
|
getBaseUrlForRole: jest.fn(() => null),
|
|
isApiKeySet: jest.fn(() => true),
|
|
getOllamaBaseURL: jest.fn(() => "http://localhost:11434/api"),
|
|
getAzureBaseURL: jest.fn(() => null),
|
|
getVertexProjectId: jest.fn(() => null),
|
|
getVertexLocation: jest.fn(() => null),
|
|
getDefaultSubtasks: jest.fn(() => 5),
|
|
getProjectName: jest.fn(() => "Test Project"),
|
|
getDefaultPriority: jest.fn(() => "medium"),
|
|
getDefaultNumTasks: jest.fn(() => 10),
|
|
getTelemetryEnabled: jest.fn(() => true),
|
|
})
|
|
);
|
|
|
|
// Mock fetch globally
|
|
global.fetch = jest.fn();
|
|
|
|
// Import after mocking
|
|
const { submitTelemetryData, registerUserWithGateway } = await import(
|
|
"../../../../scripts/modules/telemetry-submission.js"
|
|
);
|
|
const { getConfig } = await import(
|
|
"../../../../scripts/modules/config-manager.js"
|
|
);
|
|
|
|
describe("Telemetry Submission Service", () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
global.fetch.mockClear();
|
|
});
|
|
|
|
describe("should send telemetry data to remote database endpoint", () => {
|
|
it("should successfully submit telemetry data to hardcoded gateway endpoint", async () => {
|
|
// Mock successful config with proper structure
|
|
getConfig.mockReturnValue({
|
|
account: {
|
|
userId: "test-user-id",
|
|
email: "test@example.com",
|
|
},
|
|
});
|
|
|
|
// Mock environment variables for telemetry config
|
|
process.env.TASKMASTER_API_KEY = "test-api-key";
|
|
|
|
// Mock successful response
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({ id: "telemetry-123" }),
|
|
});
|
|
|
|
const telemetryData = {
|
|
timestamp: new Date().toISOString(),
|
|
userId: "test-user-id",
|
|
commandName: "test-command",
|
|
modelUsed: "claude-3-sonnet",
|
|
totalCost: 0.001,
|
|
currency: "USD",
|
|
commandArgs: { secret: "should-be-sent" },
|
|
fullOutput: { debug: "should-be-sent" },
|
|
};
|
|
|
|
const result = await submitTelemetryData(telemetryData);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.id).toBe("telemetry-123");
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
"http://localhost:4444/api/v1/telemetry", // Hardcoded endpoint
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-taskmaster-service-id": "98fb3198-2dfc-42d1-af53-07b99e4f3bde",
|
|
Authorization: "Bearer test-api-key",
|
|
"X-User-Email": "test@example.com",
|
|
},
|
|
body: expect.stringContaining('"commandName":"test-command"'),
|
|
})
|
|
);
|
|
|
|
// Verify sensitive data IS included in submission to gateway
|
|
const sentData = JSON.parse(global.fetch.mock.calls[0][1].body);
|
|
expect(sentData.commandArgs).toEqual({ secret: "should-be-sent" });
|
|
expect(sentData.fullOutput).toEqual({ debug: "should-be-sent" });
|
|
|
|
// Clean up
|
|
delete process.env.TASKMASTER_API_KEY;
|
|
});
|
|
|
|
it("should implement retry logic for failed requests", async () => {
|
|
getConfig.mockReturnValue({
|
|
account: {
|
|
userId: "test-user-id",
|
|
email: "test@example.com",
|
|
},
|
|
});
|
|
|
|
// Mock environment variables
|
|
process.env.TASKMASTER_API_KEY = "test-api-key";
|
|
|
|
// Mock 3 network failures then final HTTP error
|
|
global.fetch
|
|
.mockRejectedValueOnce(new Error("Network error"))
|
|
.mockRejectedValueOnce(new Error("Network error"))
|
|
.mockRejectedValueOnce(new Error("Network error"));
|
|
|
|
const telemetryData = {
|
|
timestamp: new Date().toISOString(),
|
|
userId: "test-user-id",
|
|
commandName: "test-command",
|
|
totalCost: 0.001,
|
|
currency: "USD",
|
|
};
|
|
|
|
const result = await submitTelemetryData(telemetryData);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Network error");
|
|
expect(global.fetch).toHaveBeenCalledTimes(3);
|
|
|
|
// Clean up
|
|
delete process.env.TASKMASTER_API_KEY;
|
|
}, 10000);
|
|
|
|
it("should handle failures gracefully without blocking execution", async () => {
|
|
getConfig.mockReturnValue({
|
|
account: {
|
|
userId: "test-user-id",
|
|
email: "test@example.com",
|
|
},
|
|
});
|
|
|
|
// Mock environment variables
|
|
process.env.TASKMASTER_API_KEY = "test-api-key";
|
|
|
|
global.fetch.mockRejectedValue(new Error("Network failure"));
|
|
|
|
const telemetryData = {
|
|
timestamp: new Date().toISOString(),
|
|
userId: "test-user-id",
|
|
commandName: "test-command",
|
|
totalCost: 0.001,
|
|
currency: "USD",
|
|
};
|
|
|
|
const result = await submitTelemetryData(telemetryData);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Network failure");
|
|
expect(global.fetch).toHaveBeenCalledTimes(3); // All retries attempted
|
|
|
|
// Clean up
|
|
delete process.env.TASKMASTER_API_KEY;
|
|
}, 10000);
|
|
|
|
it("should respect user opt-out preferences", async () => {
|
|
// Mock getTelemetryEnabled to return false for this test
|
|
const { getTelemetryEnabled } = await import(
|
|
"../../../../scripts/modules/config-manager.js"
|
|
);
|
|
getTelemetryEnabled.mockReturnValue(false);
|
|
|
|
getConfig.mockReturnValue({
|
|
account: {
|
|
telemetryEnabled: false,
|
|
},
|
|
});
|
|
|
|
const telemetryData = {
|
|
timestamp: new Date().toISOString(),
|
|
userId: "test-user-id",
|
|
commandName: "test-command",
|
|
totalCost: 0.001,
|
|
currency: "USD",
|
|
};
|
|
|
|
const result = await submitTelemetryData(telemetryData);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.skipped).toBe(true);
|
|
expect(result.reason).toBe("Telemetry disabled by user preference");
|
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
|
|
// Reset the mock for other tests
|
|
getTelemetryEnabled.mockReturnValue(true);
|
|
});
|
|
|
|
it("should validate telemetry data before submission", async () => {
|
|
getConfig.mockReturnValue({
|
|
account: {
|
|
userId: "test-user-id",
|
|
email: "test@example.com",
|
|
},
|
|
});
|
|
|
|
// Mock environment variables so config is valid
|
|
process.env.TASKMASTER_API_KEY = "test-api-key";
|
|
|
|
const invalidTelemetryData = {
|
|
// Missing required fields
|
|
commandName: "test-command",
|
|
};
|
|
|
|
const result = await submitTelemetryData(invalidTelemetryData);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Telemetry data validation failed");
|
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
|
|
// Clean up
|
|
delete process.env.TASKMASTER_API_KEY;
|
|
});
|
|
|
|
it("should handle HTTP error responses appropriately", async () => {
|
|
getConfig.mockReturnValue({
|
|
account: {
|
|
userId: "test-user-id",
|
|
email: "test@example.com",
|
|
},
|
|
});
|
|
|
|
// Mock environment variables with invalid API key
|
|
process.env.TASKMASTER_API_KEY = "invalid-key";
|
|
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
statusText: "Unauthorized",
|
|
json: async () => ({}),
|
|
});
|
|
|
|
const telemetryData = {
|
|
timestamp: new Date().toISOString(),
|
|
userId: "test-user-id",
|
|
commandName: "test-command",
|
|
totalCost: 0.001,
|
|
currency: "USD",
|
|
};
|
|
|
|
const result = await submitTelemetryData(telemetryData);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.statusCode).toBe(401);
|
|
expect(global.fetch).toHaveBeenCalledTimes(1); // No retries for auth errors
|
|
|
|
// Clean up
|
|
delete process.env.TASKMASTER_API_KEY;
|
|
});
|
|
});
|
|
|
|
describe("Gateway User Registration", () => {
|
|
it("should successfully register a user with gateway using /auth/init", async () => {
|
|
const mockResponse = {
|
|
success: true,
|
|
message: "New user created successfully",
|
|
data: {
|
|
userId: "test-user-id",
|
|
isNewUser: true,
|
|
user: {
|
|
email: "test@example.com",
|
|
planType: "free",
|
|
creditsBalance: 0,
|
|
},
|
|
token: "test-api-key",
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => mockResponse,
|
|
});
|
|
|
|
const result = await registerUserWithGateway("test@example.com");
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
apiKey: "test-api-key",
|
|
userId: "test-user-id",
|
|
email: "test@example.com",
|
|
isNewUser: true,
|
|
});
|
|
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
"http://localhost:4444/auth/init",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ email: "test@example.com" }),
|
|
}
|
|
);
|
|
});
|
|
|
|
it("should handle existing user with /auth/init", async () => {
|
|
const mockResponse = {
|
|
success: true,
|
|
message: "Existing user found",
|
|
data: {
|
|
userId: "existing-user-id",
|
|
isNewUser: false,
|
|
user: {
|
|
email: "existing@example.com",
|
|
planType: "free",
|
|
creditsBalance: 20,
|
|
},
|
|
token: "existing-api-key",
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => mockResponse,
|
|
});
|
|
|
|
const result = await registerUserWithGateway("existing@example.com");
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
apiKey: "existing-api-key",
|
|
userId: "existing-user-id",
|
|
email: "existing@example.com",
|
|
isNewUser: false,
|
|
});
|
|
});
|
|
|
|
it("should handle registration failures gracefully", async () => {
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
statusText: "Internal Server Error",
|
|
});
|
|
|
|
const result = await registerUserWithGateway("test@example.com");
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: "Gateway registration failed: 500 Internal Server Error",
|
|
});
|
|
});
|
|
|
|
it("should handle network errors during registration", async () => {
|
|
global.fetch.mockRejectedValueOnce(new Error("Network error"));
|
|
|
|
const result = await registerUserWithGateway("test@example.com");
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: "Gateway registration error: Network error",
|
|
});
|
|
});
|
|
|
|
it("should handle invalid response format from /auth/init", async () => {
|
|
const mockResponse = {
|
|
success: false,
|
|
error: "Invalid email format",
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
global.fetch.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 401,
|
|
statusText: "Unauthorized",
|
|
});
|
|
|
|
const result = await registerUserWithGateway("invalid-email");
|
|
|
|
expect(result).toEqual({
|
|
success: false,
|
|
error: "Gateway registration failed: 401 Unauthorized",
|
|
});
|
|
});
|
|
});
|
|
});
|