mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Fix main team switch handling when leaving/deleting team + add secondary team tests
This commit is contained in:
parent
182d2fa3a7
commit
0932e8a5b5
|
|
@ -3,5 +3,6 @@ export const ADMIN_TEST_AVATAR = "6fc41a44b069a0d2152ac06d1e496c6c";
|
|||
export const NZAP_TEST_DISCORD_ID = "455039198672453645";
|
||||
export const NZAP_TEST_AVATAR = "f809176af93132c3db5f0a5019e96339"; // https://cdn.discordapp.com/avatars/455039198672453645/f809176af93132c3db5f0a5019e96339.webp?size=160
|
||||
export const NZAP_TEST_ID = 2;
|
||||
export const REGULAR_USER_TEST_ID = 2;
|
||||
|
||||
export const AMOUNT_OF_CALENDAR_EVENTS = 200;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { sub } from "date-fns";
|
||||
import { type NotNull, sql } from "kysely";
|
||||
import { type NotNull, type Transaction, sql } from "kysely";
|
||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||
import { db } from "~/db/sql";
|
||||
import type { TablesInsertable } from "~/db/tables";
|
||||
import type { DB, TablesInsertable } from "~/db/tables";
|
||||
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
|
||||
import { LFG } from "./lfg-constants";
|
||||
|
|
@ -143,6 +143,9 @@ export function deletePost(id: number) {
|
|||
return db.deleteFrom("LFGPost").where("id", "=", id).execute();
|
||||
}
|
||||
|
||||
export function deletePostsByTeamId(teamId: number) {
|
||||
return db.deleteFrom("LFGPost").where("teamId", "=", teamId).execute();
|
||||
export function deletePostsByTeamId(teamId: number, trx?: Transaction<DB>) {
|
||||
return (trx ?? db)
|
||||
.deleteFrom("LFGPost")
|
||||
.where("teamId", "=", teamId)
|
||||
.execute();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,9 +204,10 @@ describe("Private user note sorting", () => {
|
|||
});
|
||||
const matchAction = wrappedAction<typeof matchSchema>({
|
||||
action: rawMatchAction,
|
||||
params: { id: "1" },
|
||||
});
|
||||
|
||||
const matchActionParams = { id: "1" };
|
||||
|
||||
test("users with positive note sorted first", async () => {
|
||||
await matchAction(
|
||||
{
|
||||
|
|
@ -215,7 +216,7 @@ describe("Private user note sorting", () => {
|
|||
sentiment: "POSITIVE",
|
||||
comment: "test",
|
||||
},
|
||||
{ user: "admin" },
|
||||
{ user: "admin", params: matchActionParams },
|
||||
);
|
||||
|
||||
const data = await lookingLoader({ user: "admin" });
|
||||
|
|
@ -231,7 +232,7 @@ describe("Private user note sorting", () => {
|
|||
sentiment: "NEGATIVE",
|
||||
comment: "test",
|
||||
},
|
||||
{ user: "admin" },
|
||||
{ user: "admin", params: matchActionParams },
|
||||
);
|
||||
|
||||
const data = await lookingLoader({ user: "admin" });
|
||||
|
|
@ -249,7 +250,7 @@ describe("Private user note sorting", () => {
|
|||
sentiment: "POSITIVE",
|
||||
comment: "test",
|
||||
},
|
||||
{ user: "admin" },
|
||||
{ user: "admin", params: matchActionParams },
|
||||
);
|
||||
await matchAction(
|
||||
{
|
||||
|
|
@ -258,7 +259,7 @@ describe("Private user note sorting", () => {
|
|||
sentiment: "NEGATIVE",
|
||||
comment: "test",
|
||||
},
|
||||
{ user: "admin" },
|
||||
{ user: "admin", params: matchActionParams },
|
||||
);
|
||||
|
||||
const data = await lookingLoader({ user: "admin" });
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { nanoid } from "nanoid";
|
|||
import { INVITE_CODE_LENGTH } from "~/constants";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB, Tables } from "~/db/tables";
|
||||
import * as LFGRepository from "~/features/lfg/LFGRepository.server";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
|
||||
|
|
@ -107,6 +108,7 @@ export async function teamsByMemberUserId(
|
|||
.select([
|
||||
"TeamMemberWithSecondary.teamId as id",
|
||||
"TeamMemberWithSecondary.isOwner",
|
||||
"TeamMemberWithSecondary.isMainTeam",
|
||||
])
|
||||
.where("userId", "=", userId)
|
||||
.execute();
|
||||
|
|
@ -200,6 +202,52 @@ export function switchMainTeam({
|
|||
});
|
||||
}
|
||||
|
||||
export function del(teamId: number) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const members = await trx
|
||||
.selectFrom("TeamMember")
|
||||
.select(["TeamMember.userId"])
|
||||
.where("teamId", "=", teamId)
|
||||
.execute();
|
||||
|
||||
// switch main team to another if they at least one secondary team
|
||||
for (const member of members) {
|
||||
const currentTeams = await teamsByMemberUserId(member.userId, trx);
|
||||
|
||||
const teamToSwitchTo = currentTeams.find((team) => team.id !== teamId);
|
||||
|
||||
if (!teamToSwitchTo) continue;
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isMainTeam: 1,
|
||||
})
|
||||
.where("userId", "=", member.userId)
|
||||
.where("teamId", "=", teamToSwitchTo.id)
|
||||
.execute();
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
isMainTeam: 0,
|
||||
})
|
||||
.where("AllTeamMember.teamId", "=", teamId)
|
||||
.execute();
|
||||
|
||||
await LFGRepository.deletePostsByTeamId(teamId, trx);
|
||||
|
||||
await trx
|
||||
.updateTable("AllTeam")
|
||||
.set({
|
||||
deletedAt: databaseTimestampNow(),
|
||||
})
|
||||
.where("id", "=", teamId)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
export function addNewTeamMember({
|
||||
userId,
|
||||
teamId,
|
||||
|
|
@ -245,8 +293,9 @@ export function removeTeamMember({
|
|||
invariant(teamToLeave, "User is not a member of this team");
|
||||
invariant(!teamToLeave.isOwner, "Owner cannot leave the team");
|
||||
|
||||
const wasMainTeam = teamToLeave.isMainTeam;
|
||||
const newMainTeam = currentTeams.find((team) => team.id !== teamId);
|
||||
if (newMainTeam) {
|
||||
if (wasMainTeam && newMainTeam) {
|
||||
await trx
|
||||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
|
|
@ -261,6 +310,7 @@ export function removeTeamMember({
|
|||
.updateTable("AllTeamMember")
|
||||
.set({
|
||||
leftAt: databaseTimestampNow(),
|
||||
isMainTeam: 0,
|
||||
})
|
||||
.where("userId", "=", userId)
|
||||
.where("teamId", "=", teamId)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
teamId: team.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case "MAKE_MAIN_TEAM": {
|
||||
|
|
@ -37,6 +38,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
userId: user.id,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
|
||||
const stm = sql.prepare(/* sql */ `
|
||||
update "AllTeam"
|
||||
set "deletedAt" = strftime('%s', 'now')
|
||||
where "id" = @id
|
||||
`);
|
||||
|
||||
export function deleteTeam(id: number) {
|
||||
stm.run({ id });
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ import { Label } from "~/components/Label";
|
|||
import { Main } from "~/components/Main";
|
||||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { requireUserId } from "~/features/auth/core/user.server";
|
||||
import * as LFGRepository from "~/features/lfg/LFGRepository.server";
|
||||
import { isAdmin } from "~/permissions";
|
||||
import {
|
||||
type SendouRouteHandle,
|
||||
|
|
@ -36,7 +35,6 @@ import {
|
|||
uploadImagePage,
|
||||
} from "~/utils/urls";
|
||||
import * as TeamRepository from "../TeamRepository.server";
|
||||
import { deleteTeam } from "../queries/deleteTeam.server";
|
||||
import { TEAM } from "../team-constants";
|
||||
import { editTeamSchema, teamParamsSchema } from "../team-schemas.server";
|
||||
import { canAddCustomizedColors, isTeamOwner } from "../team-utils";
|
||||
|
|
@ -89,8 +87,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
|
||||
switch (data._action) {
|
||||
case "DELETE": {
|
||||
await LFGRepository.deletePostsByTeamId(team.id);
|
||||
deleteTeam(team.id);
|
||||
await TeamRepository.del(team.id);
|
||||
|
||||
throw redirect(TEAM_SEARCH_PAGE);
|
||||
}
|
||||
|
|
|
|||
182
app/features/team/routes/t.$customUrl.test.ts
Normal file
182
app/features/team/routes/t.$customUrl.test.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import type { SerializeFrom } from "@remix-run/node";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { REGULAR_USER_TEST_ID } from "~/db/seed/constants";
|
||||
import { db } from "~/db/sql";
|
||||
import {
|
||||
dbInsertUsers,
|
||||
dbReset,
|
||||
wrappedAction,
|
||||
wrappedLoader,
|
||||
} from "~/utils/Test";
|
||||
import { loader as userProfileLoader } from "../../user-page/loaders/u.$identifier.index.server";
|
||||
import * as TeamRepository from "../TeamRepository.server";
|
||||
import { action as _teamPageAction } from "../actions/t.$customUrl.server";
|
||||
import { action as teamIndexPageAction } from "../actions/t.server";
|
||||
import { action as _editTeamAction } from "../routes/t.$customUrl.edit";
|
||||
import type {
|
||||
createTeamSchema,
|
||||
editTeamSchema,
|
||||
teamProfilePageActionSchema,
|
||||
} from "../team-schemas.server";
|
||||
|
||||
const loadUserTeamLoader = wrappedLoader<
|
||||
SerializeFrom<typeof userProfileLoader>
|
||||
>({
|
||||
loader: userProfileLoader,
|
||||
});
|
||||
|
||||
const createTeamAction = wrappedAction<typeof createTeamSchema>({
|
||||
action: teamIndexPageAction,
|
||||
});
|
||||
const teamPageAction = wrappedAction<typeof teamProfilePageActionSchema>({
|
||||
action: _teamPageAction,
|
||||
});
|
||||
const editTeamAction = wrappedAction<typeof editTeamSchema>({
|
||||
action: _editTeamAction,
|
||||
});
|
||||
|
||||
async function loadTeams() {
|
||||
const data = await loadUserTeamLoader({
|
||||
user: "regular",
|
||||
params: {
|
||||
identifier: String(REGULAR_USER_TEST_ID),
|
||||
},
|
||||
});
|
||||
|
||||
return { team: data.user.team, secondaryTeams: data.user.secondaryTeams };
|
||||
}
|
||||
|
||||
describe("Secondary teams", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers(2);
|
||||
});
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
it("first team created becomes main team", async () => {
|
||||
const { team, secondaryTeams } = await loadTeams();
|
||||
|
||||
expect(team!.name).toBe("Team 1");
|
||||
expect(secondaryTeams).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("second team created becomes secondary", async () => {
|
||||
await createTeamAction({ name: "Team 1" }, { user: "regular" });
|
||||
await createTeamAction({ name: "Team 2" }, { user: "regular" });
|
||||
|
||||
const { team, secondaryTeams } = await loadTeams();
|
||||
|
||||
expect(team!.name).toBe("Team 1");
|
||||
expect(secondaryTeams[0].name).toBe("Team 2");
|
||||
});
|
||||
|
||||
it("makes secondary team main team", async () => {
|
||||
await createTeamAction({ name: "Team 1" }, { user: "regular" });
|
||||
await createTeamAction({ name: "Team 2" }, { user: "regular" });
|
||||
|
||||
const { team, secondaryTeams } = await loadTeams();
|
||||
|
||||
expect(team!.name).toBe("Team 1");
|
||||
expect(secondaryTeams[0].name).toBe("Team 2");
|
||||
});
|
||||
|
||||
it("sets main team (2 team)", async () => {
|
||||
await createTeamAction({ name: "Team 1" }, { user: "regular" });
|
||||
await createTeamAction({ name: "Team 2" }, { user: "regular" });
|
||||
|
||||
await teamPageAction(
|
||||
{ _action: "MAKE_MAIN_TEAM" },
|
||||
{ user: "regular", params: { customUrl: "team-2" } },
|
||||
);
|
||||
|
||||
const { team } = await loadTeams();
|
||||
|
||||
expect(team!.name).toBe("Team 2");
|
||||
});
|
||||
|
||||
it("when deleting the main team, the secondary team becomes main", async () => {
|
||||
await createTeamAction({ name: "Team 1" }, { user: "regular" });
|
||||
await createTeamAction({ name: "Team 2" }, { user: "regular" });
|
||||
|
||||
await editTeamAction(
|
||||
{
|
||||
_action: "DELETE",
|
||||
},
|
||||
{
|
||||
user: "regular",
|
||||
params: { customUrl: "team-1" },
|
||||
},
|
||||
);
|
||||
|
||||
const { team, secondaryTeams } = await loadTeams();
|
||||
|
||||
expect(team!.name).toBe("Team 2");
|
||||
expect(secondaryTeams).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("when leaving the main team, the secondary team becomes main", async () => {
|
||||
// has to be made by "admin" because can't leave team you own
|
||||
await createTeamAction({ name: "Team 1" }, { user: "admin" });
|
||||
await createTeamAction({ name: "Team 2" }, { user: "admin" });
|
||||
|
||||
await TeamRepository.addNewTeamMember({
|
||||
userId: REGULAR_USER_TEST_ID,
|
||||
teamId: 1,
|
||||
maxTeamsAllowed: 2,
|
||||
});
|
||||
await TeamRepository.addNewTeamMember({
|
||||
userId: REGULAR_USER_TEST_ID,
|
||||
teamId: 2,
|
||||
maxTeamsAllowed: 2,
|
||||
});
|
||||
|
||||
const { team, secondaryTeams } = await loadTeams();
|
||||
|
||||
expect(team!.name).toBe("Team 1");
|
||||
expect(secondaryTeams[0].name).toBe("Team 2");
|
||||
|
||||
await teamPageAction(
|
||||
{
|
||||
_action: "LEAVE_TEAM",
|
||||
},
|
||||
{
|
||||
user: "regular",
|
||||
params: { customUrl: "team-1" },
|
||||
},
|
||||
);
|
||||
|
||||
const { team: newTeam, secondaryTeams: newSecondaryTeams } =
|
||||
await loadTeams();
|
||||
|
||||
expect(newTeam!.name).toBe("Team 2");
|
||||
expect(newSecondaryTeams).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("creates max 2 teams as non-patron", async () => {
|
||||
await createTeamAction({ name: "Team 1" }, { user: "regular" });
|
||||
await createTeamAction({ name: "Team 2" }, { user: "regular" });
|
||||
|
||||
expect(
|
||||
createTeamAction({ name: "Team 3" }, { user: "regular" }),
|
||||
).rejects.toThrow("status code: 400");
|
||||
});
|
||||
|
||||
const makeUserPatron = () =>
|
||||
db
|
||||
.updateTable("User")
|
||||
.set({ patronTier: 2 })
|
||||
.where("id", "=", REGULAR_USER_TEST_ID)
|
||||
.execute();
|
||||
|
||||
it("creates more than 2 teams as patron", async () => {
|
||||
await makeUserPatron();
|
||||
|
||||
await createTeamAction({ name: "Team 1" }, { user: "regular" });
|
||||
await createTeamAction({ name: "Team 2" }, { user: "regular" });
|
||||
await createTeamAction({ name: "Team 3" }, { user: "regular" });
|
||||
|
||||
const { secondaryTeams } = await loadTeams();
|
||||
expect(secondaryTeams).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import type { Params } from "@remix-run/react";
|
||||
import type { z } from "zod";
|
||||
import { ADMIN_ID } from "~/constants";
|
||||
import { NZAP_TEST_ID } from "~/db/seed/constants";
|
||||
import { REGULAR_USER_TEST_ID } from "~/db/seed/constants";
|
||||
import { db, sql } from "~/db/sql";
|
||||
import { SESSION_KEY } from "~/features/auth/core/authenticator.server";
|
||||
import { authSessionStorage } from "~/features/auth/core/session.server";
|
||||
|
|
@ -14,15 +15,16 @@ export function arrayContainsSameItems<T>(arr1: T[], arr2: T[]) {
|
|||
|
||||
export function wrappedAction<T extends z.ZodTypeAny>({
|
||||
action,
|
||||
params = {},
|
||||
}: {
|
||||
// TODO: strongly type this
|
||||
action: (args: ActionFunctionArgs) => any;
|
||||
params?: ActionFunctionArgs["params"];
|
||||
}) {
|
||||
return async (
|
||||
args: z.infer<T>,
|
||||
{ user }: { user?: "admin" | "regular" } = {},
|
||||
{
|
||||
user,
|
||||
params = {},
|
||||
}: { user?: "admin" | "regular"; params?: Params<string> } = {},
|
||||
) => {
|
||||
const body = new URLSearchParams(args);
|
||||
const request = new Request("http://app.com/path", {
|
||||
|
|
@ -58,7 +60,10 @@ export function wrappedLoader<T>({
|
|||
// TODO: strongly type this
|
||||
loader: (args: LoaderFunctionArgs) => any;
|
||||
}) {
|
||||
return async ({ user }: { user?: "admin" | "regular" } = {}) => {
|
||||
return async ({
|
||||
user,
|
||||
params = {},
|
||||
}: { user?: "admin" | "regular"; params?: Params<string> } = {}) => {
|
||||
const request = new Request("http://app.com/path", {
|
||||
method: "GET",
|
||||
headers: await authHeader(user),
|
||||
|
|
@ -67,7 +72,7 @@ export function wrappedLoader<T>({
|
|||
try {
|
||||
const data = await loader({
|
||||
request,
|
||||
params: {},
|
||||
params,
|
||||
context: {},
|
||||
});
|
||||
|
||||
|
|
@ -87,7 +92,7 @@ async function authHeader(user?: "admin" | "regular"): Promise<HeadersInit> {
|
|||
|
||||
const session = await authSessionStorage.getSession();
|
||||
|
||||
session.set(SESSION_KEY, user === "admin" ? ADMIN_ID : NZAP_TEST_ID);
|
||||
session.set(SESSION_KEY, user === "admin" ? ADMIN_ID : REGULAR_USER_TEST_ID);
|
||||
|
||||
return [["Cookie", await authSessionStorage.commitSession(session)]];
|
||||
}
|
||||
|
|
@ -106,11 +111,12 @@ export const dbReset = () => {
|
|||
sql.prepare("PRAGMA foreign_keys = ON").run();
|
||||
};
|
||||
|
||||
export const dbInsertUsers = (count: number) =>
|
||||
export const dbInsertUsers = (count?: number) =>
|
||||
db
|
||||
.insertInto("User")
|
||||
.values(
|
||||
Array.from({ length: count }).map((_, i) => ({
|
||||
// defaults to 2 = admin & regular "NZAP"
|
||||
Array.from({ length: count ?? 2 }).map((_, i) => ({
|
||||
id: i + 1,
|
||||
discordName: `user${i + 1}`,
|
||||
discordId: String(i),
|
||||
|
|
|
|||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
Loading…
Reference in New Issue
Block a user