1287 lines
39 KiB
TypeScript
1287 lines
39 KiB
TypeScript
import { expect } from "chai";
|
|
import { afterEach, beforeEach, describe, it } from "mocha";
|
|
import sinon from "sinon";
|
|
import {
|
|
createMockContext,
|
|
createMockS3Client,
|
|
createMockStreamChatClient,
|
|
createMockSupabaseClient,
|
|
createMockTransporter,
|
|
mockEnvVars,
|
|
mockEvent,
|
|
mockProfile,
|
|
mockTablo,
|
|
mockUser,
|
|
} from "./test-utils.js";
|
|
|
|
describe("Tablo Router", () => {
|
|
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
|
let mockSupabase: any;
|
|
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
|
let mockStreamChat: any;
|
|
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
|
let mockChannel: any;
|
|
// biome-ignore lint/suspicious/noExplicitAny: Mock client types
|
|
let mockS3: any;
|
|
let restoreEnv: () => void;
|
|
|
|
beforeEach(() => {
|
|
restoreEnv = mockEnvVars();
|
|
mockSupabase = createMockSupabaseClient();
|
|
const streamMocks = createMockStreamChatClient();
|
|
mockStreamChat = streamMocks.mockStreamChat;
|
|
mockChannel = streamMocks.mockChannel;
|
|
mockS3 = createMockS3Client();
|
|
});
|
|
|
|
afterEach(() => {
|
|
sinon.restore();
|
|
restoreEnv();
|
|
});
|
|
|
|
describe("POST /create", () => {
|
|
it("should create a new tablo with events", async () => {
|
|
const mockContext = createMockContext();
|
|
const payload = {
|
|
name: "New Tablo",
|
|
color: "bg-blue-500",
|
|
status: "todo",
|
|
events: [
|
|
{
|
|
title: "Event 1",
|
|
description: "Test event",
|
|
start_date: "2024-01-16",
|
|
start_time: "10:00",
|
|
end_time: "11:00",
|
|
},
|
|
],
|
|
};
|
|
|
|
mockContext.req.json.resolves(payload);
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
mockContext.get.withArgs("streamServerClient").returns(mockStreamChat);
|
|
|
|
// Mock Supabase insert
|
|
mockSupabase
|
|
.from()
|
|
.insert()
|
|
.select()
|
|
.single.resolves({ data: mockTablo, error: null });
|
|
|
|
// Mock events insert
|
|
const eventsBuilder = {
|
|
insert: sinon.stub().resolves({ data: [], error: null }),
|
|
};
|
|
mockSupabase.from.withArgs("events").returns(eventsBuilder);
|
|
|
|
// Create test handler
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const data = await c.req.json();
|
|
|
|
const { data: insertedTablo, error } = await supabase
|
|
.from("tablos")
|
|
.insert({
|
|
...data,
|
|
owner_id: user.id,
|
|
events: undefined,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error || !insertedTablo) {
|
|
return c.json(
|
|
{ error: error?.message || "Failed to create tablo" },
|
|
500
|
|
);
|
|
}
|
|
|
|
const streamServerClient = c.get("streamServerClient");
|
|
const channel = streamServerClient.channel(
|
|
"messaging",
|
|
insertedTablo.id,
|
|
{
|
|
name: insertedTablo.name,
|
|
created_by_id: user.id,
|
|
members: [user.id],
|
|
}
|
|
);
|
|
await channel.create();
|
|
|
|
if (data.events) {
|
|
// biome-ignore lint/suspicious/noExplicitAny: Event type varies
|
|
const eventsToInsert = data.events.map((event: any) => ({
|
|
...event,
|
|
tablo_id: insertedTablo.id,
|
|
created_by: user.id,
|
|
}));
|
|
|
|
await supabase.from("events").insert(eventsToInsert);
|
|
}
|
|
|
|
return c.json({ message: "Tablo created successfully" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(mockStreamChat.channel.calledOnce).to.be.true;
|
|
expect(mockChannel.create.calledOnce).to.be.true;
|
|
expect(result).to.deep.equal({ message: "Tablo created successfully" });
|
|
});
|
|
|
|
it("should return 500 if tablo creation fails", async () => {
|
|
const mockContext = createMockContext();
|
|
const payload = {
|
|
name: "New Tablo",
|
|
color: "bg-blue-500",
|
|
status: "todo",
|
|
};
|
|
|
|
mockContext.req.json.resolves(payload);
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
|
|
// Mock Supabase error
|
|
mockSupabase
|
|
.from()
|
|
.insert()
|
|
.select()
|
|
.single.resolves({ data: null, error: { message: "Insert failed" } });
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const data = await c.req.json();
|
|
|
|
const { error } = await supabase
|
|
.from("tablos")
|
|
.insert({
|
|
...data,
|
|
owner_id: user.id,
|
|
events: undefined,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
|
|
return c.json({ message: "Tablo created successfully" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ error: "Insert failed" });
|
|
});
|
|
});
|
|
|
|
describe("POST /create-and-invite", () => {
|
|
it("should create tablo and grant access to invited user", async () => {
|
|
const mockContext = createMockContext();
|
|
const ownerProfile = {
|
|
...mockProfile,
|
|
id: "owner-id",
|
|
short_user_id: "owner123",
|
|
};
|
|
const invitedProfile = { ...mockProfile, id: "invited-id" };
|
|
|
|
const payload = {
|
|
owner_short_id: "owner123",
|
|
event: {
|
|
title: "Meeting",
|
|
description: "Test meeting",
|
|
start_date: "2024-01-16",
|
|
start_time: "10:00",
|
|
end_time: "11:00",
|
|
},
|
|
};
|
|
|
|
mockContext.req.json.resolves(payload);
|
|
mockContext.get
|
|
.withArgs("user")
|
|
.returns({ ...mockUser, id: "invited-id" });
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
mockContext.get.withArgs("streamServerClient").returns(mockStreamChat);
|
|
|
|
// Mock owner lookup
|
|
const ownerBuilder = {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
single: sinon.stub().resolves({ data: ownerProfile, error: null }),
|
|
};
|
|
|
|
// Mock invited user lookup
|
|
const invitedBuilder = {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
single: sinon.stub().resolves({ data: invitedProfile, error: null }),
|
|
};
|
|
|
|
// Mock existing tablo check
|
|
const existingTabloBuilder = {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
is: sinon.stub().returnsThis(),
|
|
limit: sinon.stub().resolves({ data: [], error: null }),
|
|
};
|
|
|
|
// Mock tablo creation
|
|
const createTabloBuilder = {
|
|
insert: sinon.stub().returnsThis(),
|
|
select: sinon.stub().returnsThis(),
|
|
single: sinon.stub().resolves({ data: mockTablo, error: null }),
|
|
};
|
|
|
|
// Mock tablo access insert
|
|
const accessBuilder = {
|
|
insert: sinon.stub().resolves({ error: null }),
|
|
};
|
|
|
|
// Mock event insert
|
|
const eventBuilder = {
|
|
insert: sinon.stub().resolves({ error: null }),
|
|
};
|
|
|
|
let callCount = 0;
|
|
mockSupabase.from.callsFake((table: string) => {
|
|
callCount++;
|
|
if (table === "profiles" && callCount === 1) return ownerBuilder;
|
|
if (table === "profiles" && callCount === 2) return invitedBuilder;
|
|
if (table === "tablos" && callCount === 3) return existingTabloBuilder;
|
|
if (table === "tablos" && callCount === 4) return createTabloBuilder;
|
|
if (table === "tablo_access") return accessBuilder;
|
|
if (table === "events") return eventBuilder;
|
|
return createTabloBuilder;
|
|
});
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const streamServerClient = c.get("streamServerClient");
|
|
const data = await c.req.json();
|
|
|
|
if (!data.owner_short_id) {
|
|
return c.json({ error: "owner_id is required" }, 400);
|
|
}
|
|
|
|
if (!data.event) {
|
|
return c.json({ error: "event is required" }, 400);
|
|
}
|
|
|
|
const { data: ownerData, error: ownerError } = await supabase
|
|
.from("profiles")
|
|
.select("id, name, email")
|
|
.eq("short_user_id", data.owner_short_id)
|
|
.single();
|
|
|
|
const { data: invitedUser, error: invitedUserError } = await supabase
|
|
.from("profiles")
|
|
.select("id, name, email")
|
|
.eq("id", user.id)
|
|
.single();
|
|
|
|
if (ownerError || !ownerData || invitedUserError || !invitedUser) {
|
|
return c.json(
|
|
{ error: "owner_id or invited_user_id is incorrect" },
|
|
400
|
|
);
|
|
}
|
|
|
|
const ownerId = ownerData.id;
|
|
|
|
const { data: existingTablo, error: existingTabloError } =
|
|
await supabase
|
|
.from("tablos")
|
|
.select(
|
|
`
|
|
id,
|
|
name,
|
|
owner_id,
|
|
tablo_access!inner(user_id)
|
|
`
|
|
)
|
|
.eq("owner_id", ownerId)
|
|
.eq("tablo_access.user_id", user.id)
|
|
.is("deleted_at", null)
|
|
.limit(1);
|
|
|
|
if (existingTabloError) {
|
|
return c.json({ error: existingTabloError.message }, 500);
|
|
}
|
|
|
|
let tabloData: typeof mockTablo;
|
|
|
|
if (!existingTablo.length) {
|
|
const { data: insertedTablo, error } = await supabase
|
|
.from("tablos")
|
|
.insert({
|
|
name: `${invitedUser.name || "Invité"} / ${
|
|
ownerData.name || "Propriétaire"
|
|
}`,
|
|
color: "bg-blue-500",
|
|
status: "todo",
|
|
owner_id: ownerId,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (error || !insertedTablo) {
|
|
return c.json(
|
|
{ error: error?.message || "Failed to create tablo" },
|
|
500
|
|
);
|
|
}
|
|
|
|
tabloData = insertedTablo;
|
|
} else {
|
|
tabloData = existingTablo[0];
|
|
}
|
|
|
|
const { error: tabloAccessError } = await supabase
|
|
.from("tablo_access")
|
|
.insert({
|
|
tablo_id: tabloData.id,
|
|
user_id: user.id,
|
|
is_admin: false,
|
|
is_active: true,
|
|
granted_by: ownerId,
|
|
});
|
|
|
|
if (tabloAccessError) {
|
|
return c.json({ error: tabloAccessError.message }, 500);
|
|
}
|
|
|
|
const channel = streamServerClient.channel("messaging", tabloData.id, {
|
|
name: tabloData.name,
|
|
created_by_id: ownerId,
|
|
members: [ownerId, user.id],
|
|
});
|
|
await channel.create();
|
|
|
|
await channel.sendMessage({
|
|
text: `🎉 Bienvenue dans votre nouveau tablo "${tabloData.name}" !`,
|
|
user_id: ownerId,
|
|
});
|
|
|
|
await supabase.from("events").insert({
|
|
...data.event,
|
|
tablo_id: tabloData.id,
|
|
created_by: ownerId,
|
|
});
|
|
|
|
return c.json({ id: tabloData.id });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ id: mockTablo.id });
|
|
expect(mockChannel.create.calledOnce).to.be.true;
|
|
expect(mockChannel.sendMessage.calledOnce).to.be.true;
|
|
});
|
|
|
|
it("should return 400 if owner_short_id is missing", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({ event: {} });
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const data = await c.req.json();
|
|
|
|
if (!data.owner_short_id) {
|
|
return c.json({ error: "owner_id is required" }, 400);
|
|
}
|
|
|
|
return c.json({ message: "Success" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ error: "owner_id is required" });
|
|
});
|
|
|
|
it("should return 400 if event is missing", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({ owner_short_id: "owner123" });
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const data = await c.req.json();
|
|
|
|
if (!data.owner_short_id) {
|
|
return c.json({ error: "owner_id is required" }, 400);
|
|
}
|
|
|
|
if (!data.event) {
|
|
return c.json({ error: "event is required" }, 400);
|
|
}
|
|
|
|
return c.json({ message: "Success" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ error: "event is required" });
|
|
});
|
|
});
|
|
|
|
describe("PATCH /update", () => {
|
|
it("should update tablo successfully", async () => {
|
|
const mockContext = createMockContext();
|
|
const updateData = {
|
|
id: mockTablo.id,
|
|
name: "Updated Tablo Name",
|
|
};
|
|
|
|
mockContext.req.json.resolves(updateData);
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
mockContext.get.withArgs("streamServerClient").returns(mockStreamChat);
|
|
|
|
const updatedTablo = { ...mockTablo, name: "Updated Tablo Name" };
|
|
|
|
mockSupabase
|
|
.from()
|
|
.update()
|
|
.eq()
|
|
.select()
|
|
.single.resolves({ data: updatedTablo, error: null });
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const streamServerClient = c.get("streamServerClient");
|
|
const data = await c.req.json();
|
|
|
|
const { id, ...tablo } = data;
|
|
|
|
const { data: update, error } = await supabase
|
|
.from("tablos")
|
|
.update(tablo)
|
|
.eq("id", id)
|
|
.eq("owner_id", user.id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error || !update) {
|
|
return c.json({ error: error?.message || "Failed to update" }, 500);
|
|
}
|
|
|
|
const isUpdatingName =
|
|
tablo.name !== undefined && tablo.name !== update.name;
|
|
|
|
if (isUpdatingName) {
|
|
const channel = streamServerClient.channel("messaging", update.id);
|
|
try {
|
|
await channel.update({
|
|
name: update.name,
|
|
});
|
|
} catch (error) {
|
|
console.error("error updating channel", error);
|
|
}
|
|
}
|
|
|
|
return c.json({ message: "Tablo updated successfully" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ message: "Tablo updated successfully" });
|
|
});
|
|
|
|
it("should return 500 if update fails", async () => {
|
|
const mockContext = createMockContext();
|
|
const updateData = {
|
|
id: mockTablo.id,
|
|
name: "Updated Name",
|
|
};
|
|
|
|
mockContext.req.json.resolves(updateData);
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
|
|
mockSupabase
|
|
.from()
|
|
.update()
|
|
.eq()
|
|
.select()
|
|
.single.resolves({ data: null, error: { message: "Update failed" } });
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const data = await c.req.json();
|
|
|
|
const { id, ...tablo } = data;
|
|
|
|
const { error } = await supabase
|
|
.from("tablos")
|
|
.update(tablo)
|
|
.eq("id", id)
|
|
.eq("owner_id", user.id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
|
|
return c.json({ message: "Tablo updated successfully" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ error: "Update failed" });
|
|
});
|
|
});
|
|
|
|
describe("DELETE /delete", () => {
|
|
it("should soft delete tablo successfully", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({ id: mockTablo.id });
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
mockContext.get.withArgs("streamServerClient").returns(mockStreamChat);
|
|
|
|
const updateBuilder = {
|
|
update: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
};
|
|
updateBuilder.eq.onCall(1).resolves({ error: null });
|
|
|
|
mockSupabase.from.withArgs("tablos").returns(updateBuilder);
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const streamServerClient = c.get("streamServerClient");
|
|
const data = await c.req.json();
|
|
|
|
const { id } = data;
|
|
|
|
const { error } = await supabase
|
|
.from("tablos")
|
|
.update({ deleted_at: new Date().toISOString() })
|
|
.eq("id", id)
|
|
.eq("owner_id", user.id);
|
|
|
|
if (error) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
|
|
const channel = streamServerClient.channel("messaging", id);
|
|
try {
|
|
await channel.delete();
|
|
} catch (error) {
|
|
console.error("error deleting channel", error);
|
|
}
|
|
|
|
return c.json({ message: "Tablo deleted successfully" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ message: "Tablo deleted successfully" });
|
|
expect(mockChannel.delete.calledOnce).to.be.true;
|
|
});
|
|
|
|
it("should return 500 if delete fails", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({ id: mockTablo.id });
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
|
|
const updateBuilder = {
|
|
update: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
};
|
|
updateBuilder.eq
|
|
.onCall(1)
|
|
.resolves({ error: { message: "Delete failed" } });
|
|
|
|
mockSupabase.from.withArgs("tablos").returns(updateBuilder);
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const data = await c.req.json();
|
|
|
|
const { id } = data;
|
|
|
|
const { error } = await supabase
|
|
.from("tablos")
|
|
.update({ deleted_at: new Date().toISOString() })
|
|
.eq("id", id)
|
|
.eq("owner_id", user.id);
|
|
|
|
if (error) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
|
|
return c.json({ message: "Tablo deleted successfully" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ error: "Delete failed" });
|
|
});
|
|
});
|
|
|
|
describe("POST /invite", () => {
|
|
it("should send invite successfully", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({
|
|
email: "invitee@example.com",
|
|
tablo_id: mockTablo.id,
|
|
});
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
|
|
// Mock tablo lookup
|
|
mockSupabase
|
|
.from()
|
|
.select()
|
|
.eq()
|
|
.single.resolves({ data: mockTablo, error: null });
|
|
|
|
// Mock invite insert
|
|
const inviteBuilder = {
|
|
insert: sinon.stub().resolves({ error: null }),
|
|
};
|
|
mockSupabase.from.withArgs("tablo_invites").returns(inviteBuilder);
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const sender = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const { tablo_id, email } = await c.req.json();
|
|
|
|
const { data, error: tabloError } = await supabase
|
|
.from("tablos")
|
|
.select("*")
|
|
.eq("id", tablo_id)
|
|
.single();
|
|
|
|
if (tabloError) {
|
|
return c.json({ error: tabloError.message }, 500);
|
|
}
|
|
|
|
if (!data) {
|
|
return c.json({ error: "Tablo not found" }, 404);
|
|
}
|
|
|
|
if (data.owner_id !== sender.id) {
|
|
return c.json(
|
|
{ error: "You are not allowed to invite users to this tablo" },
|
|
400
|
|
);
|
|
}
|
|
|
|
const { error } = await supabase.from("tablo_invites").insert({
|
|
invited_email: email,
|
|
tablo_id: tablo_id,
|
|
invited_by: sender.id,
|
|
invite_token: "mock-token",
|
|
});
|
|
|
|
if (error) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
|
|
return c.json({
|
|
message: "Invite sent successfully",
|
|
});
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ message: "Invite sent successfully" });
|
|
});
|
|
|
|
it("should return 404 if tablo not found", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({
|
|
email: "invitee@example.com",
|
|
tablo_id: "non-existent",
|
|
});
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
|
|
mockSupabase
|
|
.from()
|
|
.select()
|
|
.eq()
|
|
.single.resolves({ data: null, error: null });
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const _sender = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const { tablo_id } = await c.req.json();
|
|
|
|
const { data, error: tabloError } = await supabase
|
|
.from("tablos")
|
|
.select("*")
|
|
.eq("id", tablo_id)
|
|
.single();
|
|
|
|
if (tabloError) {
|
|
return c.json({ error: tabloError.message }, 500);
|
|
}
|
|
|
|
if (!data) {
|
|
return c.json({ error: "Tablo not found" }, 404);
|
|
}
|
|
|
|
return c.json({ message: "Success" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ error: "Tablo not found" });
|
|
});
|
|
|
|
it("should return 400 if user is not owner", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({
|
|
email: "invitee@example.com",
|
|
tablo_id: mockTablo.id,
|
|
});
|
|
mockContext.get
|
|
.withArgs("user")
|
|
.returns({ ...mockUser, id: "different-user" });
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
|
|
mockSupabase
|
|
.from()
|
|
.select()
|
|
.eq()
|
|
.single.resolves({ data: mockTablo, error: null });
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const sender = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const { tablo_id } = await c.req.json();
|
|
|
|
const { data, error: tabloError } = await supabase
|
|
.from("tablos")
|
|
.select("*")
|
|
.eq("id", tablo_id)
|
|
.single();
|
|
|
|
if (tabloError) {
|
|
return c.json({ error: tabloError.message }, 500);
|
|
}
|
|
|
|
if (!data) {
|
|
return c.json({ error: "Tablo not found" }, 404);
|
|
}
|
|
|
|
if (data.owner_id !== sender.id) {
|
|
return c.json(
|
|
{ error: "You are not allowed to invite users to this tablo" },
|
|
400
|
|
);
|
|
}
|
|
|
|
return c.json({ message: "Success" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({
|
|
error: "You are not allowed to invite users to this tablo",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("POST /join", () => {
|
|
it("should join tablo successfully with valid token", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({ token: "valid-token" });
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
mockContext.get.withArgs("streamServerClient").returns(mockStreamChat);
|
|
|
|
const inviteData = {
|
|
id: "invite-id",
|
|
tablo_id: mockTablo.id,
|
|
invited_by: "inviter-id",
|
|
};
|
|
|
|
// Mock invite lookup
|
|
const inviteSelectBuilder = {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
single: sinon.stub().resolves({ data: inviteData, error: null }),
|
|
};
|
|
|
|
// Mock tablo access insert
|
|
const accessBuilder = {
|
|
insert: sinon.stub().resolves({ error: null }),
|
|
};
|
|
|
|
// Mock invite delete
|
|
const deleteBuilder = {
|
|
delete: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().resolves({ error: null }),
|
|
};
|
|
|
|
let callCount = 0;
|
|
mockSupabase.from.callsFake((table: string) => {
|
|
callCount++;
|
|
if (table === "tablo_invites" && callCount === 1) {
|
|
return inviteSelectBuilder;
|
|
}
|
|
if (table === "tablo_access") return accessBuilder;
|
|
if (table === "tablo_invites" && callCount > 1) return deleteBuilder;
|
|
return mockSupabase.from();
|
|
});
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const { token } = await c.req.json();
|
|
|
|
const joiner = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const streamServerClient = c.get("streamServerClient");
|
|
|
|
const { data: inviteData, error } = await supabase
|
|
.from("tablo_invites")
|
|
.select("id, tablo_id, invited_by")
|
|
.eq("invite_token", token)
|
|
.eq("invited_email", joiner.email)
|
|
.single();
|
|
|
|
if (error) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
|
|
if (!inviteData) {
|
|
return c.json({ error: "Invalid token or email" }, 400);
|
|
}
|
|
|
|
const { id: invite_id, tablo_id, invited_by } = inviteData;
|
|
|
|
const { error: tabloAccessError } = await supabase
|
|
.from("tablo_access")
|
|
.insert({
|
|
tablo_id,
|
|
user_id: joiner.id,
|
|
is_admin: false,
|
|
is_active: true,
|
|
granted_by: invited_by,
|
|
});
|
|
|
|
if (tabloAccessError) {
|
|
return c.json({ error: tabloAccessError.message }, 500);
|
|
}
|
|
|
|
await supabase.from("tablo_invites").delete().eq("id", invite_id);
|
|
|
|
const channel = streamServerClient.channel("messaging", tablo_id);
|
|
await channel.addMembers([joiner.id]);
|
|
|
|
return c.json({ message: "Tablo joined successfully" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ message: "Tablo joined successfully" });
|
|
expect(mockChannel.addMembers.calledOnce).to.be.true;
|
|
});
|
|
|
|
it("should return 400 for invalid token", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({ token: "invalid-token" });
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
|
|
mockSupabase
|
|
.from()
|
|
.select()
|
|
.eq()
|
|
.single.resolves({ data: null, error: null });
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const { token } = await c.req.json();
|
|
const joiner = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
|
|
const { data: inviteData, error } = await supabase
|
|
.from("tablo_invites")
|
|
.select("id, tablo_id, invited_by")
|
|
.eq("invite_token", token)
|
|
.eq("invited_email", joiner.email)
|
|
.single();
|
|
|
|
if (error) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
|
|
if (!inviteData) {
|
|
return c.json({ error: "Invalid token or email" }, 400);
|
|
}
|
|
|
|
return c.json({ message: "Success" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ error: "Invalid token or email" });
|
|
});
|
|
});
|
|
|
|
describe("GET /members/:tablo_id", () => {
|
|
it("should return tablo members", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.param.withArgs("tablo_id").returns(mockTablo.id);
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
|
|
const members = [
|
|
{ is_admin: true, profiles: { id: "user1", name: "User 1" } },
|
|
{ is_admin: false, profiles: { id: "user2", name: "User 2" } },
|
|
];
|
|
|
|
// Mock user_tablos check
|
|
const userTablosBuilder = {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
};
|
|
// The second eq() call should resolve
|
|
userTablosBuilder.eq
|
|
.onCall(1)
|
|
.resolves({ data: [mockTablo], error: null });
|
|
|
|
// Mock tablo_access query
|
|
const accessBuilder = {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
};
|
|
// The second eq() call should resolve
|
|
accessBuilder.eq.onCall(1).resolves({ data: members, error: null });
|
|
|
|
mockSupabase.from.callsFake((table: string) => {
|
|
if (table === "user_tablos") return userTablosBuilder;
|
|
if (table === "tablo_access") return accessBuilder;
|
|
return mockSupabase.from();
|
|
});
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const tablo_id = c.req.param("tablo_id");
|
|
|
|
const { data: tabloData, error: tabloError } = await supabase
|
|
.from("user_tablos")
|
|
.select("*")
|
|
.eq("id", tablo_id)
|
|
.eq("user_id", user.id);
|
|
|
|
if (!tabloData || tabloData.length === 0) {
|
|
return c.json({ error: "You are not a member of this tablo" }, 403);
|
|
}
|
|
|
|
if (tabloError) {
|
|
return c.json({ error: "Internal server error" }, 500);
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from("tablo_access")
|
|
.select("is_admin, profiles(id, name)")
|
|
.eq("tablo_id", tablo_id)
|
|
.eq("is_active", true);
|
|
|
|
if (error) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
|
|
return c.json({
|
|
// biome-ignore lint/suspicious/noExplicitAny: Member type from DB
|
|
members: data.map((member: any) => ({
|
|
...member.profiles,
|
|
is_admin: member.is_admin,
|
|
})),
|
|
});
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result.members).to.have.length(2);
|
|
expect(result.members[0]).to.deep.equal({
|
|
id: "user1",
|
|
name: "User 1",
|
|
is_admin: true,
|
|
});
|
|
});
|
|
|
|
it("should return 403 if user is not a member", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.param.withArgs("tablo_id").returns(mockTablo.id);
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
|
|
const userTablosBuilder = {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
single: sinon.stub().resolves({ data: [], error: null }),
|
|
};
|
|
|
|
mockSupabase.from.withArgs("user_tablos").returns(userTablosBuilder);
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const tablo_id = c.req.param("tablo_id");
|
|
|
|
const { data: tabloData } = await supabase
|
|
.from("user_tablos")
|
|
.select("*")
|
|
.eq("id", tablo_id)
|
|
.eq("user_id", user.id);
|
|
|
|
if (!tabloData || tabloData.length === 0) {
|
|
return c.json({ error: "You are not a member of this tablo" }, 403);
|
|
}
|
|
|
|
return c.json({ message: "Success" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({
|
|
error: "You are not a member of this tablo",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("POST /leave", () => {
|
|
it("should leave tablo successfully", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({ tablo_id: mockTablo.id });
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
mockContext.get.withArgs("streamServerClient").returns(mockStreamChat);
|
|
|
|
const updateBuilder = {
|
|
update: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
};
|
|
// The second eq() call should resolve
|
|
updateBuilder.eq.onCall(1).resolves({ error: null });
|
|
|
|
mockSupabase.from.withArgs("tablo_access").returns(updateBuilder);
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
const streamServerClient = c.get("streamServerClient");
|
|
const { tablo_id } = await c.req.json();
|
|
|
|
const channel = streamServerClient.channel("messaging", tablo_id);
|
|
await channel.removeMembers([user.id]);
|
|
|
|
const { error } = await supabase
|
|
.from("tablo_access")
|
|
.update({ is_active: false })
|
|
.eq("tablo_id", tablo_id)
|
|
.eq("user_id", user.id);
|
|
|
|
if (error) {
|
|
return c.json({ error: error.message }, 500);
|
|
}
|
|
|
|
return c.json({ message: "Tablo left successfully" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ message: "Tablo left successfully" });
|
|
expect(mockChannel.removeMembers.calledOnce).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe("POST /webcal/generate-url", () => {
|
|
it("should generate webcal URL for tablo", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({ tablo_id: mockTablo.id });
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
mockContext.get.withArgs("s3_client").returns(mockS3);
|
|
|
|
// Mock tablo lookup
|
|
const tabloBuilder = {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
single: sinon.stub().resolves({ data: mockTablo, error: null }),
|
|
};
|
|
|
|
// Mock access check
|
|
const accessBuilder = {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
single: sinon
|
|
.stub()
|
|
.resolves({ data: { id: mockTablo.id }, error: null }),
|
|
};
|
|
|
|
// Mock subscription check (no existing subscription)
|
|
const subscriptionBuilder = {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().returnsThis(),
|
|
single: sinon.stub().resolves({ data: null, error: null }),
|
|
};
|
|
|
|
// Mock subscription insert
|
|
const insertBuilder = {
|
|
insert: sinon.stub().resolves({ error: null }),
|
|
};
|
|
|
|
let callCount = 0;
|
|
mockSupabase.from.callsFake((table: string) => {
|
|
callCount++;
|
|
if (table === "tablos") return tabloBuilder;
|
|
if (table === "user_tablos") return accessBuilder;
|
|
if (table === "calendar_subscriptions" && callCount === 3)
|
|
return subscriptionBuilder;
|
|
if (table === "calendar_subscriptions" && callCount === 4)
|
|
return insertBuilder;
|
|
if (table === "events_and_tablos") {
|
|
return {
|
|
select: sinon.stub().returnsThis(),
|
|
eq: sinon.stub().resolves({ data: [], error: null }),
|
|
};
|
|
}
|
|
return mockSupabase.from();
|
|
});
|
|
|
|
mockS3.send.resolves({});
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
|
|
const { tablo_id } = await c.req.json();
|
|
|
|
if (tablo_id === null) {
|
|
return c.json({ error: "All tablos are not supported" }, 400);
|
|
}
|
|
|
|
const { data: tabloData, error: tabloError } = await supabase
|
|
.from("tablos")
|
|
.select("name")
|
|
.eq("id", tablo_id)
|
|
.single();
|
|
|
|
if (tabloError || !tabloData) {
|
|
return c.json({ error: "Tablo not found" }, 404);
|
|
}
|
|
|
|
const { data: accessData, error: accessError } = await supabase
|
|
.from("user_tablos")
|
|
.select("id")
|
|
.eq("id", tablo_id)
|
|
.eq("user_id", user.id)
|
|
.single();
|
|
|
|
if (accessError || !accessData) {
|
|
return c.json({ error: "Access denied to this tablo" }, 403);
|
|
}
|
|
|
|
const { data: subscriptionData } = await supabase
|
|
.from("calendar_subscriptions")
|
|
.select("*")
|
|
.eq("tablo_id", tablo_id)
|
|
.single();
|
|
|
|
if (subscriptionData) {
|
|
const token = subscriptionData.token;
|
|
const tabloName = tabloData.name.replace(/ /g, "_");
|
|
const httpUrl = `https://calendar.xtablo.com/${token}/${tabloName}.ics`;
|
|
|
|
return c.json({
|
|
webcal_url: null,
|
|
http_url: httpUrl,
|
|
});
|
|
}
|
|
|
|
const token = "mock-token";
|
|
|
|
const { error } = await supabase.from("calendar_subscriptions").insert({
|
|
tablo_id: tablo_id,
|
|
token: token,
|
|
});
|
|
|
|
if (error) {
|
|
return c.json({ error: "Failed to generate token" }, 500);
|
|
}
|
|
|
|
const tabloName = tabloData.name.replace(/ /g, "_");
|
|
const httpUrl = `https://calendar.xtablo.com/${token}/${tabloName}.ics`;
|
|
|
|
return c.json({
|
|
webcal_url: null,
|
|
http_url: httpUrl,
|
|
});
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result.http_url).to.include("https://calendar.xtablo.com/");
|
|
expect(result.http_url).to.include(".ics");
|
|
});
|
|
|
|
it("should return 404 if tablo not found", async () => {
|
|
const mockContext = createMockContext();
|
|
mockContext.req.json.resolves({ tablo_id: "non-existent" });
|
|
mockContext.get.withArgs("user").returns(mockUser);
|
|
mockContext.get.withArgs("supabase").returns(mockSupabase);
|
|
|
|
mockSupabase
|
|
.from()
|
|
.select()
|
|
.eq()
|
|
.single.resolves({ data: null, error: { message: "Not found" } });
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: Test handler with dynamic context
|
|
const handler = async (c: any) => {
|
|
const _user = c.get("user");
|
|
const supabase = c.get("supabase");
|
|
|
|
const { tablo_id } = await c.req.json();
|
|
|
|
const { data: tabloData, error: tabloError } = await supabase
|
|
.from("tablos")
|
|
.select("name")
|
|
.eq("id", tablo_id)
|
|
.single();
|
|
|
|
if (tabloError || !tabloData) {
|
|
return c.json({ error: "Tablo not found" }, 404);
|
|
}
|
|
|
|
return c.json({ message: "Success" });
|
|
};
|
|
|
|
const result = await handler(mockContext);
|
|
|
|
expect(result).to.deep.equal({ error: "Tablo not found" });
|
|
});
|
|
});
|
|
});
|