Skip to content
Binary file added docs/screenshots/team-switcher.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
128 changes: 124 additions & 4 deletions server/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,119 @@ describe("harness HTTP API", () => {
expect(after.body.bots.find((b: { id: string }) => b.id === bot.id)).toBeUndefined();
});

it("creates, switches, and exports a named team", async () => {
const created = await api("POST", "/api/teams", { name: " Field Ops " });
expect(created.status).toBe(201);
expect(created.body.team).toMatchObject({ name: "Field Ops" });
expect(created.body.activeTeamId).toBe(created.body.team.id);
expect((await api("POST", "/api/teams", { name: "field ops" })).status).toBe(409);
expect((await api("POST", "/api/teams", { name: "" })).status).toBe(400);
expect((await api("POST", "/api/teams", { name: "T".repeat(61) })).status).toBe(400);

const teamId = created.body.team.id as string;
const emptyExport = await api("POST", "/api/teams/export", { teamId });
expect(emptyExport.status).toBe(400);
expect(emptyExport.body.error).toBe("Field Ops has no bots to export");
const inTeam = (await api("POST", "/api/bots", { teamId })).body.bot;
expect(inTeam.teamId).toBe(teamId);
expect(inTeam.section).toBe("Field Ops");
const outsider = (await api("POST", "/api/bots")).body.bot;
expect(outsider.teamId).toBeNull();
expect((await api("POST", "/api/bots", { teamId: "missing" })).status).toBe(400);

const room = (
await api("POST", "/api/groups", { name: "Standup", memberIds: [inTeam.id], teamId })
).body.group;
expect(room.teamId).toBe(teamId);

const exported = await api("POST", "/api/teams/export", { teamId });
expect(exported.status).toBe(200);
expect(exported.body.team.name).toBe("Field Ops");
expect(exported.body.team.members.map((member: { name: string }) => member.name)).toEqual([inTeam.name]);

const renamed = await api("PATCH", `/api/teams/${teamId}`, { name: "Ops" });
expect(renamed.status).toBe(200);
expect(renamed.body.team.name).toBe("Ops");
expect((await api("GET", "/api/bots")).body.bots.find((bot: { id: string }) => bot.id === inTeam.id).section).toBe(
"Ops",
);

const mixed = await api("PATCH", `/api/bots/${inTeam.id}`, { teamId, section: "Stale Label" });
expect(mixed.status).toBe(200);
expect(mixed.body.bot).toMatchObject({ teamId, section: "Ops" });
const clearedTeam = await api("PATCH", `/api/bots/${outsider.id}`, { teamId: null, section: "Should Not Stick" });
expect(clearedTeam.status).toBe(200);
expect(clearedTeam.body.bot.teamId).toBeNull();
expect(clearedTeam.body.bot).not.toHaveProperty("section");

const all = await api("POST", "/api/teams/active", { id: null });
expect(all.status).toBe(200);
expect(all.body.activeTeamId).toBeNull();
expect((await api("GET", "/api/bots")).body.activeTeamId).toBeNull();

const removed = await api("DELETE", `/api/teams/${teamId}`);
expect(removed.status).toBe(200);
const state = (await api("GET", "/api/bots")).body;
expect(state.teams.find((team: { id: string }) => team.id === teamId)).toBeUndefined();
expect(state.bots.find((bot: { id: string }) => bot.id === inTeam.id).teamId).toBeNull();
expect(state.groups.find((group: { id: string }) => group.id === room.id).teamId).toBeNull();

await api("DELETE", `/api/bots/${inTeam.id}`);
await api("DELETE", `/api/bots/${outsider.id}`);
await api("DELETE", `/api/groups/${room.id}`);
});

it("import add and replace follow the requested team, not a lagging active team", async () => {
const home = (await api("POST", "/api/teams", { name: "Lag Home" })).body;
const other = (await api("POST", "/api/teams", { name: "Lag Other", activate: false })).body;
const homeBot = (await api("POST", "/api/bots", { name: "Lag Keeper", teamId: home.team.id })).body.bot;
const otherBot = (await api("POST", "/api/bots", { name: "Lag Guest", teamId: other.team.id })).body.bot;
expect((await api("POST", "/api/teams/active", { id: home.team.id })).status).toBe(200);

const exportedHome = await api("POST", "/api/teams/export", { teamId: home.team.id });
const added = await api("POST", `/api/teams/import?mode=add&teamId=${other.team.id}`, exportedHome.body);
expect(added.status).toBe(201);
expect(added.body.bots[0].teamId).toBe(other.team.id);
expect((await api("GET", "/api/bots")).body.bots.find((bot: { id: string }) => bot.id === homeBot.id).hidden).toBeFalsy();

const exported = await api("POST", "/api/teams/export", { teamId: other.team.id });
const replaced = await api("POST", `/api/teams/import?mode=replace&teamId=${other.team.id}`, exported.body);
expect(replaced.status).toBe(201);
expect(replaced.body.archived.map((bot: { id: string }) => bot.id).sort()).toEqual(
[otherBot.id, added.body.bots[0].id].sort(),
);
expect((await api("GET", "/api/bots")).body.bots.find((bot: { id: string }) => bot.id === homeBot.id).hidden).toBeFalsy();

for (const bot of [...added.body.bots, ...replaced.body.bots]) await api("DELETE", `/api/bots/${bot.id}`);
await api("DELETE", `/api/bots/${homeBot.id}`);
await api("DELETE", `/api/bots/${otherBot.id}`);
await api("DELETE", `/api/teams/${home.team.id}`);
await api("DELETE", `/api/teams/${other.team.id}`);
});

it("replace import archives only the active team", async () => {
const home = (await api("POST", "/api/teams", { name: "Home" })).body;
const visitor = (await api("POST", "/api/teams", { name: "Visitor", activate: false })).body;
const homeBot = (await api("POST", "/api/bots", { name: "Keeper", teamId: home.team.id })).body.bot;
const otherBot = (await api("POST", "/api/bots", { name: "Guest", teamId: visitor.team.id })).body.bot;
expect((await api("POST", "/api/teams/active", { id: home.team.id })).status).toBe(200);

const exported = await api("POST", "/api/teams/export", { teamId: home.team.id });
const replaced = await api("POST", "/api/teams/import?mode=replace", exported.body);
expect(replaced.status).toBe(201);
expect(replaced.body.archived.map((bot: { id: string }) => bot.id)).toEqual([homeBot.id]);

const state = (await api("GET", "/api/bots")).body;
expect(state.bots.find((bot: { id: string }) => bot.id === homeBot.id).hidden).toBe(true);
expect(state.bots.find((bot: { id: string }) => bot.id === otherBot.id).hidden).toBeFalsy();

for (const bot of replaced.body.bots) await api("DELETE", `/api/bots/${bot.id}`);
await api("DELETE", `/api/bots/${homeBot.id}`);
await api("DELETE", `/api/bots/${otherBot.id}`);
await api("DELETE", `/api/teams/${home.team.id}`);
await api("DELETE", `/api/teams/${visitor.team.id}`);
});

it("saves, serves, and guards image attachments", async () => {
// a real 1x1 PNG so the bytes round-trip intact
const png = Buffer.from(
Expand Down Expand Up @@ -701,8 +814,14 @@ describe("harness HTTP API", () => {
expect(invalid.status).toBe(400);
expect((await api("POST", "/api/teams/import?mode=erase", exported.body)).status).toBe(400);

const beforeReplace = (await api("GET", "/api/bots")).body.bots.filter(
(bot: { hidden?: boolean }) => !bot.hidden,
const mid = (await api("GET", "/api/bots")).body;
const beforeReplace = mid.bots.filter(
(bot: { hidden?: boolean; teamId?: string | null }) =>
!bot.hidden && bot.teamId === mid.activeTeamId,
);
const outsiders = mid.bots.filter(
(bot: { hidden?: boolean; teamId?: string | null }) =>
!bot.hidden && bot.teamId !== mid.activeTeamId,
);
const replaced = await api("POST", "/api/teams/import?mode=replace", exported.body);
expect(replaced.status).toBe(201);
Expand All @@ -711,8 +830,9 @@ describe("harness HTTP API", () => {
);
expect(replaced.body.archivedBots.every((bot: { hidden?: boolean }) => bot.hidden)).toBe(true);
const afterReplace = (await api("GET", "/api/bots")).body.bots;
expect(afterReplace.filter((bot: { hidden?: boolean }) => !bot.hidden).map((bot: { id: string }) => bot.id).sort()).toEqual(
replaced.body.bots.map((bot: { id: string }) => bot.id).sort(),
const visibleAfter = afterReplace.filter((bot: { hidden?: boolean }) => !bot.hidden).map((bot: { id: string }) => bot.id);
expect(visibleAfter.sort()).toEqual(
[...outsiders.map((bot: { id: string }) => bot.id), ...replaced.body.bots.map((bot: { id: string }) => bot.id)].sort(),
);
expect((await api("GET", "/api/bots")).body.groups).toHaveLength(roomsBefore);

Expand Down
Loading
Loading