Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions app/api/routes-f/events/__tests__/buffer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { EventBufferManager } from "../_lib/buffer";

describe("EventBufferManager", () => {
let buffer: EventBufferManager;

beforeEach(() => {
buffer = new EventBufferManager();
});

describe("addEvents", () => {
it("should add events to buffer", () => {
const events = [
{ name: "test1", timestamp: Date.now() },
{ name: "test2", timestamp: Date.now() + 1 },
];

buffer.addEvents(events);
expect(buffer.getBufferSize()).toBe(2);
});

it("should handle buffer overflow by removing oldest events", () => {
// Fill buffer to capacity
const events = Array.from({ length: 10000 }, (_, i) => ({
name: `event${i}`,
timestamp: Date.now() + i,
}));

buffer.addEvents(events);
expect(buffer.getBufferSize()).toBe(10000);

// Add more events to trigger overflow
const newEvents = [
{ name: "new1", timestamp: Date.now() + 10001 },
{ name: "new2", timestamp: Date.now() + 10002 },
];

buffer.addEvents(newEvents);
expect(buffer.getBufferSize()).toBe(10000);

// Verify oldest events were removed
const result = buffer.getEvents({ page: 1, limit: 10000 });
expect(result.events).toHaveLength(10000);
expect(result.events[0].name).toBe("event2"); // event0 should be removed
expect(result.events[9999].name).toBe("new2");
});

it("should handle single event addition", () => {
const event = { name: "single", timestamp: Date.now() };
buffer.addEvents([event]);
expect(buffer.getBufferSize()).toBe(1);
});
});

describe("getEvents", () => {
beforeEach(() => {
// Add test events
const events = Array.from({ length: 10 }, (_, i) => ({
name: `event${i}`,
timestamp: Date.now() + i,
}));
buffer.addEvents(events);
});

it("should return events sorted by timestamp (newest first)", () => {
const result = buffer.getEvents({ page: 1, limit: 5 });
expect(result.events).toHaveLength(5);
expect(result.events[0].name).toBe("event9"); // newest
expect(result.events[4].name).toBe("event5");
});

it("should handle pagination correctly", () => {
const page1 = buffer.getEvents({ page: 1, limit: 3 });
const page2 = buffer.getEvents({ page: 2, limit: 3 });

expect(page1.events).toHaveLength(3);
expect(page2.events).toHaveLength(3);
expect(page1.events[0].name).toBe("event9");
expect(page2.events[0].name).toBe("event6");

expect(page1.pagination.page).toBe(1);
expect(page1.pagination.hasNext).toBe(true);
expect(page1.pagination.hasPrev).toBe(false);

expect(page2.pagination.page).toBe(2);
expect(page2.pagination.hasNext).toBe(true);
expect(page2.pagination.hasPrev).toBe(true);
});

it("should handle last page correctly", () => {
const result = buffer.getEvents({ page: 4, limit: 3 });
expect(result.events).toHaveLength(1);
expect(result.pagination.hasNext).toBe(false);
expect(result.pagination.hasPrev).toBe(true);
});

it("should return empty result for page beyond range", () => {
const result = buffer.getEvents({ page: 10, limit: 5 });
expect(result.events).toHaveLength(0);
expect(result.pagination.hasNext).toBe(false);
expect(result.pagination.hasPrev).toBe(true);
});

it("should use default pagination parameters", () => {
const result = buffer.getEvents({});
expect(result.pagination.page).toBe(1);
expect(result.pagination.limit).toBe(50);
});
});

describe("buffer management", () => {
it("should clear buffer", () => {
buffer.addEvents([{ name: "test", timestamp: Date.now() }]);
expect(buffer.getBufferSize()).toBe(1);

buffer.clearBuffer();
expect(buffer.getBufferSize()).toBe(0);
});

it("should return correct buffer size", () => {
expect(buffer.getBufferSize()).toBe(0);
buffer.addEvents([{ name: "test", timestamp: Date.now() }]);
expect(buffer.getBufferSize()).toBe(1);
});

it("should return buffer statistics", () => {
const events = Array.from({ length: 50 }, (_, i) => ({
name: `event${i}`,
timestamp: Date.now() + i,
}));
buffer.addEvents(events);

const stats = buffer.getStats();
expect(stats.size).toBe(50);
expect(stats.maxSize).toBe(10000);
expect(stats.utilization).toBe(0.5); // 50/10000
});
});
});
254 changes: 254 additions & 0 deletions app/api/routes-f/events/__tests__/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import { NextRequest } from "next/server";
import { POST, GET } from "../route";
import { eventBuffer } from "../_lib/buffer";

// Mock the buffer
jest.mock("../_lib/buffer", () => ({
eventBuffer: {
addEvents: jest.fn(),
getEvents: jest.fn(),
getBufferSize: jest.fn(),
clearBuffer: jest.fn(),
},
}));

describe("/api/routes-f/events", () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe("POST /api/routes-f/events", () => {
it("should accept a single valid event", async () => {
const mockEvent = {
name: "user_login",
timestamp: Date.now(),
properties: { userId: "123" },
};

const request = new NextRequest("http://localhost/api/routes-f/events", {
method: "POST",
body: JSON.stringify({ event: mockEvent }),
headers: { "Content-Type": "application/json" },
});

(eventBuffer.addEvents as jest.Mock).mockReturnValue(undefined);

const response = await POST(request);
const data = await response.json();

expect(response.status).toBe(201);
expect(data.success).toBe(true);
expect(data.data.received).toBe(1);
expect(eventBuffer.addEvents).toHaveBeenCalledWith([mockEvent]);
});

it("should accept a batch of valid events", async () => {
const mockEvents = [
{ name: "user_login", timestamp: Date.now() },
{ name: "page_view", timestamp: Date.now() + 1 },
];

const request = new NextRequest("http://localhost/api/routes-f/events", {
method: "POST",
body: JSON.stringify({ events: mockEvents }),
headers: { "Content-Type": "application/json" },
});

(eventBuffer.addEvents as jest.Mock).mockReturnValue(undefined);

const response = await POST(request);
const data = await response.json();

expect(response.status).toBe(201);
expect(data.success).toBe(true);
expect(data.data.received).toBe(2);
expect(eventBuffer.addEvents).toHaveBeenCalledWith(mockEvents);
});

it("should reject requests with neither event nor events", async () => {
const request = new NextRequest("http://localhost/api/routes-f/events", {
method: "POST",
body: JSON.stringify({}),
headers: { "Content-Type": "application/json" },
});

const response = await POST(request);
const data = await response.json();

expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toContain("Either 'event' or 'events' must be provided");
});

it("should reject requests with both event and events", async () => {
const request = new NextRequest("http://localhost/api/routes-f/events", {
method: "POST",
body: JSON.stringify({
event: { name: "test", timestamp: Date.now() },
events: [{ name: "test", timestamp: Date.now() }],
}),
headers: { "Content-Type": "application/json" },
});

const response = await POST(request);
const data = await response.json();

expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toContain("Cannot provide both 'event' and 'events'");
});

it("should reject batches larger than 100 events", async () => {
const largeBatch = Array.from({ length: 101 }, (_, i) => ({
name: "test_event",
timestamp: Date.now() + i,
}));

const request = new NextRequest("http://localhost/api/routes-f/events", {
method: "POST",
body: JSON.stringify({ events: largeBatch }),
headers: { "Content-Type": "application/json" },
});

const response = await POST(request);
const data = await response.json();

expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toContain("Batch size cannot exceed 100 events");
});

it("should reject events with missing required fields", async () => {
const request = new NextRequest("http://localhost/api/routes-f/events", {
method: "POST",
body: JSON.stringify({
event: { name: "", timestamp: Date.now() },
}),
headers: { "Content-Type": "application/json" },
});

const response = await POST(request);
const data = await response.json();

expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toContain("Event name is required");
});

it("should reject events with invalid timestamp", async () => {
const request = new NextRequest("http://localhost/api/routes-f/events", {
method: "POST",
body: JSON.stringify({
event: { name: "test", timestamp: -1 },
}),
headers: { "Content-Type": "application/json" },
});

const response = await POST(request);
const data = await response.json();

expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toContain("Timestamp must be a positive integer");
});

it("should handle malformed JSON", async () => {
const request = new NextRequest("http://localhost/api/routes-f/events", {
method: "POST",
body: "invalid json",
headers: { "Content-Type": "application/json" },
});

const response = await POST(request);
const data = await response.json();

expect(response.status).toBe(500);
expect(data.success).toBe(false);
expect(data.error).toContain("Internal server error");
});
});

describe("GET /api/routes-f/events", () => {
it("should return paginated events with default parameters", async () => {
const mockEvents = [
{ name: "event1", timestamp: Date.now() },
{ name: "event2", timestamp: Date.now() - 1000 },
];

const mockResult = {
events: mockEvents,
pagination: {
page: 1,
limit: 50,
total: 2,
hasNext: false,
hasPrev: false,
},
};

(eventBuffer.getEvents as jest.Mock).mockReturnValue(mockResult);

const request = new NextRequest("http://localhost/api/routes-f/events");
const response = await GET(request);
const data = await response.json();

expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.data).toEqual(mockResult);
expect(eventBuffer.getEvents).toHaveBeenCalledWith({ page: 1, limit: 50 });
});

it("should return paginated events with custom parameters", async () => {
const mockResult = {
events: [],
pagination: {
page: 2,
limit: 25,
total: 100,
hasNext: true,
hasPrev: true,
},
};

(eventBuffer.getEvents as jest.Mock).mockReturnValue(mockResult);

const request = new NextRequest(
"http://localhost/api/routes-f/events?page=2&limit=25"
);
const response = await GET(request);
const data = await response.json();

expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.data).toEqual(mockResult);
expect(eventBuffer.getEvents).toHaveBeenCalledWith({ page: 2, limit: 25 });
});

it("should validate pagination parameters", async () => {
const request = new NextRequest(
"http://localhost/api/routes-f/events?page=0&limit=101"
);
const response = await GET(request);
const data = await response.json();

expect(response.status).toBe(200);
expect(data.success).toBe(true);
// Should default to valid values due to schema validation
expect(eventBuffer.getEvents).toHaveBeenCalledWith({ page: 1, limit: 50 });
});

it("should handle errors gracefully", async () => {
(eventBuffer.getEvents as jest.Mock).mockImplementation(() => {
throw new Error("Buffer error");
});

const request = new NextRequest("http://localhost/api/routes-f/events");
const response = await GET(request);
const data = await response.json();

expect(response.status).toBe(500);
expect(data.success).toBe(false);
expect(data.error).toContain("Internal server error");
});
});
});
Loading