Skip to content

Public API · E2E tests

0 modules · 0 exports · 0 repositories · 18 e2e tests · 23 routes

  • SSendou
  • Users a test creates itself are coloured per test
  • Arrange data setup
  • Act what the user does
  • Assert what is expected

Public API

OPTIONS preflight request returns 204 with CORS headers

LOLogged out
Act
  1. Fetch “/api/tournament/1”, method “OPTIONS” → response
    const response = await page.request.fetch("/api/tournament/1", {
    	method: "OPTIONS",
    });
Assert
  1. response.status() is 204
    expect(response.status()).toBe(204);
  2. response.headers()["access-control-allow-origin"] is “*”
    expect(response.headers()["access-control-allow-origin"]).toBe("*");
  3. response.headers()["access-control-allow-methods"] contains “GET”
    expect(response.headers()["access-control-allow-methods"]).toContain("GET");
  4. response.headers()["access-control-allow-headers"] contains “Authorization”
    expect(response.headers()["access-control-allow-headers"]).toContain(
    	"Authorization",
    );
Source
test("OPTIONS preflight request returns 204 with CORS headers", async ({
	page,
}) => {
	const response = await page.request.fetch("/api/tournament/1", {
		method: "OPTIONS",
	});

	expect(response.status()).toBe(204);
	expect(response.headers()["access-control-allow-origin"]).toBe("*");
	expect(response.headers()["access-control-allow-methods"]).toContain("GET");
	expect(response.headers()["access-control-allow-headers"]).toContain(
		"Authorization",
	);
})

GET request includes CORS headers in response

LOLogged out
Act
  1. Fetch “/api/tournament/1” → response
    const response = await page.request.fetch("/api/tournament/1");
Assert
  1. response.headers()["access-control-allow-origin"] is “*”
    expect(response.headers()["access-control-allow-origin"]).toBe("*");
Source
test("GET request includes CORS headers in response", async ({ page }) => {
	const response = await page.request.fetch("/api/tournament/1");

	expect(response.headers()["access-control-allow-origin"]).toBe("*");
})

GET user IDs endpoint works without authentication

LOLogged out
Act
  1. Fetch `/api/user/${ADMIN_ID}/ids` → response
    const response = await page.request.fetch(`/api/user/${ADMIN_ID}/ids`);
Assert
  1. response.status() is 200
    expect(response.status()).toBe(200);
Act
  1. Json → data
    const data = await response.json();
Assert
  1. data.id is Sendou
    expect(data.id).toBe(ADMIN_ID);
  2. data.discordId is truthy
    expect(data.discordId).toBeTruthy();
Source
test("GET user IDs endpoint works without authentication", async ({
	page,
}) => {
	const response = await page.request.fetch(`/api/user/${ADMIN_ID}/ids`);

	expect(response.status()).toBe(200);
	const data = await response.json();
	expect(data.id).toBe(ADMIN_ID);
	expect(data.discordId).toBeTruthy();
})

creates read API token and calls public endpoint

Renders/api

Setup
Arrange
  1. Grant user Sendou roles “API_ACCESSER”
    await factories.UserFactory.grant(ADMIN_ID, { roles: ["API_ACCESSER"] });
SSendou
Api page
Act
  1. Open the api page
    await api.goto();
  2. Generate token “read” → token
    const token = await api.generateToken("read");
Assert
  1. token has 20 items
    expect(token).toHaveLength(TOKEN_LENGTH);
Act
  1. Fetch `/api/user/${ADMIN_ID}`, headers authorized(token) → response
    const response = await page.request.fetch(`/api/user/${ADMIN_ID}`, {
    	headers: authorized(token),
    });
Assert
  1. response.status() is 200
    expect(response.status()).toBe(200);
Act
  1. Json → data
    const data = await response.json();
Assert
  1. data.id is Sendou
    expect(data.id).toBe(ADMIN_ID);
  2. data.name is “Sendou”
    expect(data.name).toBe("Sendou");
Source
test("creates read API token and calls public endpoint", async ({
	page,
	factories,
}) => {
	await factories.UserFactory.grant(ADMIN_ID, { roles: ["API_ACCESSER"] });

	await impersonate(page);

	const api = new ApiPage(page);
	await api.goto();

	const token = await api.generateToken("read");
	expect(token).toHaveLength(TOKEN_LENGTH);

	const response = await page.request.fetch(`/api/user/${ADMIN_ID}`, {
		headers: authorized(token),
	});

	expect(response.status()).toBe(200);
	const data = await response.json();
	expect(data.id).toBe(ADMIN_ID);
	expect(data.name).toBe("Sendou");
})

returns active SendouQ match for user

Setup
Arrange
  1. Create FULL_GROUP_SIZE × user → alpha
    const alpha = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
  2. Create FULL_GROUP_SIZE × user → bravo
    const bravo = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
  3. Create SQ match alpha user ids alpha.map((user) => user.id) bravo user ids bravo.map((user) => user.id) → match
    const match = await factories.SQMatchFactory.create({
    	alphaUserIds: alpha.map((user) => user.id),
    	bravoUserIds: bravo.map((user) => user.id),
    });
  4. Read token Sendou → token
    const token = await readToken(factories, ADMIN_ID);
SSendou
Act
  1. Fetch `/api/user/${alpha[0].id}/active-match`, headers authorized(token) → response
    const response = await page.request.fetch(
    	`/api/user/${alpha[0].id}/active-match`,
    	{ headers: authorized(token) },
    );
Assert
  1. response.status() is 200
    expect(response.status()).toBe(200);
Act
  1. Json → data
    const data = await response.json();
Assert
  1. data.matchId is match.id
    expect(data.matchId).toBe(match.id);
  2. data.lobby is “sendouq”
    expect(data.lobby).toBe("sendouq");
  3. data.tournamentId is null
    expect(data.tournamentId).toBeNull();
  4. data.bracketIdx is null
    expect(data.bracketIdx).toBeNull();
Source
test("returns active SendouQ match for user", async ({ page, factories }) => {
	const alpha = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
	const bravo = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
	const match = await factories.SQMatchFactory.create({
		alphaUserIds: alpha.map((user) => user.id),
		bravoUserIds: bravo.map((user) => user.id),
	});
	const token = await readToken(factories, ADMIN_ID);

	await impersonate(page);

	const response = await page.request.fetch(
		`/api/user/${alpha[0].id}/active-match`,
		{ headers: authorized(token) },
	);

	expect(response.status()).toBe(200);
	const data = await response.json();
	expect(data.matchId).toBe(match.id);
	expect(data.lobby).toBe("sendouq");
	expect(data.tournamentId).toBeNull();
	expect(data.bracketIdx).toBeNull();
})

returns set wins, map wins and placement of tournament teams

Setup
Arrange
  1. Create TEAM_COUNT * ROSTER_SIZE × user → players
    const players = await factories.UserFactory.createMany(
    	TEAM_COUNT * ROSTER_SIZE,
    );
  2. teamRosters = Array.from({ length: TEAM_COUNT }, (_, i) => players.slice(i * ROSTER_SIZE, (…
    const teamRosters = Array.from({ length: TEAM_COUNT }, (_, i) =>
    	players.slice(i * ROSTER_SIZE, (i + 1) * ROSTER_SIZE).map((p) => p.id),
    );
  3. Create played tournament author Sendou start times dateToDatabaseTimestamp(subHours(new Date(), 2)) teamRosters → tournament
    const tournament = await factories.TournamentFactory.createPlayed(
    	{
    		authorId: ADMIN_ID,
    		startTimes: [dateToDatabaseTimestamp(subHours(new Date(), 2))],
    	},
    	{ teamRosters },
    );
  4. Read token Sendou → token
    const token = await readToken(factories, ADMIN_ID);
SSendou
Act
  1. Fetch `/api/tournament/${tournament.id}/teams`, headers authorized(token) → response
    const response = await page.request.fetch(
    	`/api/tournament/${tournament.id}/teams`,
    	{ headers: authorized(token) },
    );
Assert
  1. response.status() is 200
    expect(response.status()).toBe(200);
Act
  1. Json → teams
    const teams = await response.json();
Assert
  1. winner = teams.find( (team: { id: number }) => team.id === tournament.teams[0].id, )
    const winner = teams.find(
    	(team: { id: number }) => team.id === tournament.teams[0].id,
    );
  2. winner.placement is 1
    expect(winner.placement).toBe(1);
  3. winner.stats equals set wins 2 set losses 0 map wins 4 map losses 0
    expect(winner.stats).toEqual({
    	setWins: 2,
    	setLosses: 0,
    	mapWins: 4,
    	mapLosses: 0,
    });
  4. firstRoundLoser = teams.find( (team: { id: number }) => team.id === tournament.teams[3].id, )
    const firstRoundLoser = teams.find(
    	(team: { id: number }) => team.id === tournament.teams[3].id,
    );
  5. firstRoundLoser.placement is 3
    expect(firstRoundLoser.placement).toBe(3);
  6. firstRoundLoser.stats equals set wins 0 set losses 1 map wins 0 map losses 2
    expect(firstRoundLoser.stats).toEqual({
    	setWins: 0,
    	setLosses: 1,
    	mapWins: 0,
    	mapLosses: 2,
    });
Source
test("returns set wins, map wins and placement of tournament teams", async ({
	page,
	factories,
}) => {
	const players = await factories.UserFactory.createMany(
		TEAM_COUNT * ROSTER_SIZE,
	);
	const teamRosters = Array.from({ length: TEAM_COUNT }, (_, i) =>
		players.slice(i * ROSTER_SIZE, (i + 1) * ROSTER_SIZE).map((p) => p.id),
	);
	const tournament = await factories.TournamentFactory.createPlayed(
		{
			authorId: ADMIN_ID,
			startTimes: [dateToDatabaseTimestamp(subHours(new Date(), 2))],
		},
		{ teamRosters },
	);
	const token = await readToken(factories, ADMIN_ID);

	await impersonate(page);

	const response = await page.request.fetch(
		`/api/tournament/${tournament.id}/teams`,
		{ headers: authorized(token) },
	);

	expect(response.status()).toBe(200);
	const teams = await response.json();
	const winner = teams.find(
		(team: { id: number }) => team.id === tournament.teams[0].id,
	);
	expect(winner.placement).toBe(1);
	expect(winner.stats).toEqual({
		setWins: 2,
		setLosses: 0,
		mapWins: 4,
		mapLosses: 0,
	});
	const firstRoundLoser = teams.find(
		(team: { id: number }) => team.id === tournament.teams[3].id,
	);
	expect(firstRoundLoser.placement).toBe(3);
	expect(firstRoundLoser.stats).toEqual({
		setWins: 0,
		setLosses: 1,
		mapWins: 0,
		mapLosses: 2,
	});
})

every read endpoint responds to a read token

Its requests are where the docs' example response of each endpoint comes from.

Setup
Arrange
  1. Create TEAM_COUNT * ROSTER_SIZE × user → players
    const players = await factories.UserFactory.createMany(
    	TEAM_COUNT * ROSTER_SIZE,
    );
  2. startedAt = subHours(new Date(), 2)
    const startedAt = subHours(new Date(), 2);
  3. Create played tournament author Sendou start times dateToDatabaseTimestamp(startedAt) team rosters Array.from({ length: TEAM_COUNT }, (_, i) => players .slice(i * ROSTER_SIZE, … → tournament
    const tournament = await factories.TournamentFactory.createPlayed(
    	{
    		authorId: ADMIN_ID,
    		startTimes: [dateToDatabaseTimestamp(startedAt)],
    	},
    	{
    		teamRosters: Array.from({ length: TEAM_COUNT }, (_, i) =>
    			players
    				.slice(i * ROSTER_SIZE, (i + 1) * ROSTER_SIZE)
    				.map((p) => p.id),
    		),
    	},
    );
  4. tournamentMatchId = tournament.matches[0].id
    const tournamentMatchId = tournament.matches[0].id;
  5. Cast match tournament tournament id tournament.id match id tournamentMatchId twitch account “sendouq”
    await factories.TournamentFactory.castMatch({
    	tournamentId: tournament.id,
    	matchId: tournamentMatchId,
    	twitchAccount: "sendouq",
    });
  6. Create team member user ids players.slice(0, ROSTER_SIZE).map((p) => p.id) → team
    const team = await factories.TeamFactory.create({
    	memberUserIds: players.slice(0, ROSTER_SIZE).map((p) => p.id),
    });
  7. Create tournament organization owner Sendou → organization
    const organization = await factories.TournamentOrganizationFactory.create({
    	ownerId: ADMIN_ID,
    });
  8. Create FULL_GROUP_SIZE × user → alpha
    const alpha = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
  9. Create FULL_GROUP_SIZE × user → bravo
    const bravo = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
  10. Create SQ match alpha user ids alpha.map((user) => user.id) bravo user ids bravo.map((user) => user.id) → sqMatch
    const sqMatch = await factories.SQMatchFactory.create({
    	alphaUserIds: alpha.map((user) => user.id),
    	bravoUserIds: bravo.map((user) => user.id),
    });
  11. Read token Sendou → token
    const token = await readToken(factories, ADMIN_ID);
SSendou
Act
  1. paths = [ `/api/tournament/${tournament.id}`, `/api/tournament/${tournament.id}/playe…
    const paths = [
    	`/api/tournament/${tournament.id}`,
    	`/api/tournament/${tournament.id}/players`,
    	`/api/tournament/${tournament.id}/casted`,
    	`/api/tournament/${tournament.id}/streams`,
    	`/api/tournament/${tournament.id}/brackets/0`,
    	`/api/tournament/${tournament.id}/brackets/0/standings`,
    	`/api/tournament-match/${tournamentMatchId}`,
    	`/api/calendar/${getISOWeekYear(startedAt)}/${getISOWeek(startedAt)}`,
    	`/api/team/${team.id}`,
    	`/api/org/${organization.id}`,
    	`/api/sendouq/active-match/${alpha[0].id}`,
    	`/api/sendouq/match/${sqMatch.id}`,
    ];
  2. For each path of paths
    for (const path of paths) {
    	const response = await page.request.fetch(path, {
    		headers: authorized(token),
    	});
    
    	expect(response.status(), path).toBe(200);
    }
    1. Fetch path, headers authorized(token) → response
      const response = await page.request.fetch(path, {
      	headers: authorized(token),
      });
    2. response.status() is 200
      expect(response.status(), path).toBe(200);
Source
test("every read endpoint responds to a read token", async ({
	page,
	factories,
}) => {
	const players = await factories.UserFactory.createMany(
		TEAM_COUNT * ROSTER_SIZE,
	);
	const startedAt = subHours(new Date(), 2);
	const tournament = await factories.TournamentFactory.createPlayed(
		{
			authorId: ADMIN_ID,
			startTimes: [dateToDatabaseTimestamp(startedAt)],
		},
		{
			teamRosters: Array.from({ length: TEAM_COUNT }, (_, i) =>
				players
					.slice(i * ROSTER_SIZE, (i + 1) * ROSTER_SIZE)
					.map((p) => p.id),
			),
		},
	);
	const tournamentMatchId = tournament.matches[0].id;
	await factories.TournamentFactory.castMatch({
		tournamentId: tournament.id,
		matchId: tournamentMatchId,
		twitchAccount: "sendouq",
	});
	const team = await factories.TeamFactory.create({
		memberUserIds: players.slice(0, ROSTER_SIZE).map((p) => p.id),
	});
	const organization = await factories.TournamentOrganizationFactory.create({
		ownerId: ADMIN_ID,
	});
	const alpha = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
	const bravo = await factories.UserFactory.createMany(FULL_GROUP_SIZE);
	const sqMatch = await factories.SQMatchFactory.create({
		alphaUserIds: alpha.map((user) => user.id),
		bravoUserIds: bravo.map((user) => user.id),
	});
	const token = await readToken(factories, ADMIN_ID);

	await impersonate(page);

	const paths = [
		`/api/tournament/${tournament.id}`,
		`/api/tournament/${tournament.id}/players`,
		`/api/tournament/${tournament.id}/casted`,
		`/api/tournament/${tournament.id}/streams`,
		`/api/tournament/${tournament.id}/brackets/0`,
		`/api/tournament/${tournament.id}/brackets/0/standings`,
		`/api/tournament-match/${tournamentMatchId}`,
		`/api/calendar/${getISOWeekYear(startedAt)}/${getISOWeek(startedAt)}`,
		`/api/team/${team.id}`,
		`/api/org/${organization.id}`,
		`/api/sendouq/active-match/${alpha[0].id}`,
		`/api/sendouq/match/${sqMatch.id}`,
	];
	for (const path of paths) {
		const response = await page.request.fetch(path, {
			headers: authorized(token),
		});

		expect(response.status(), path).toBe(200);
	}
})

hides private team fields from a read token that does not organize the tournament

Setup
Arrange
  1. Organized tournament with private team info true → { tournamentId, token }
    const { tournamentId, token } = await organizedTournament(factories, {
    	withPrivateTeamInfo: true,
    });
  2. Create user → outsider
    const outsider = await factories.UserFactory.create();
  3. Read token outsider.id → outsiderToken
    const outsiderToken = await readToken(factories, outsider.id);
Ooutsider
Act
  1. Fetch teams token, tournamentId → asOrganizer
    const asOrganizer = await fetchTeams(page, token, tournamentId);
  2. Fetch teams outsiderToken, tournamentId → asOutsider
    const asOutsider = await fetchTeams(page, outsiderToken, tournamentId);
Assert
  1. asOrganizer[0].mapPool has 2 items
    expect(asOrganizer[0].mapPool).toHaveLength(2);
  2. asOrganizer[0].logoUrl equals expect.any(String)
    expect(asOrganizer[0].logoUrl).toEqual(expect.any(String));
  3. asOrganizer[0].members[0].friendCode equals expect.any(String)
    expect(asOrganizer[0].members[0].friendCode).toEqual(expect.any(String));
  4. asOutsider[0].mapPool is null
    expect(asOutsider[0].mapPool).toBeNull();
  5. asOutsider[0].logoUrl is null
    expect(asOutsider[0].logoUrl).toBeNull();
  6. asOutsider[0].members[0].friendCode is null
    expect(asOutsider[0].members[0].friendCode).toBeNull();
Source
test("hides private team fields from a read token that does not organize the tournament", async ({
	page,
	factories,
}) => {
	const { tournamentId, token } = await organizedTournament(factories, {
		withPrivateTeamInfo: true,
	});
	const outsider = await factories.UserFactory.create();
	const outsiderToken = await readToken(factories, outsider.id);

	// signed in as the outsider, so the fields organizers see can only come from the token
	await impersonate(page, outsider.id);

	const asOrganizer = await fetchTeams(page, token, tournamentId);
	const asOutsider = await fetchTeams(page, outsiderToken, tournamentId);

	expect(asOrganizer[0].mapPool).toHaveLength(2);
	expect(asOrganizer[0].logoUrl).toEqual(expect.any(String));
	expect(asOrganizer[0].members[0].friendCode).toEqual(expect.any(String));

	expect(asOutsider[0].mapPool).toBeNull();
	expect(asOutsider[0].logoUrl).toBeNull();
	expect(asOutsider[0].members[0].friendCode).toBeNull();
})

Public API - Write endpoints

adds member to tournament team via API

Renders/to/:id/to/:id/teams/:tid

Setup
Arrange
  1. Organized tournament → { tournamentId, teamId, token }
    const { tournamentId, teamId, token } =
    	await organizedTournament(factories);
  2. Create user → newMember
    const newMember = await factories.UserFactory.create();
SSendou
Act
  1. Fetch `/api/tournament/${tournamentId}/teams/${teamId}/add-member`, method “POST” headers authorized(token) data user id newMember.id → response
    const response = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams/${teamId}/add-member`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: { userId: newMember.id },
    	},
    );
Assert
  1. response.status() is 200
    expect(response.status()).toBe(200);
Tournament team page
Act
  1. Open the tournament team page tournamentId, teamId
    await teamPage.goto(tournamentId, teamId);
Assert
  1. member names count is ROSTER_SIZE + 1
    await expect(teamPage.locators.memberNames).toHaveCount(ROSTER_SIZE + 1);
Source
test("adds member to tournament team via API", async ({
	page,
	factories,
}) => {
	const { tournamentId, teamId, token } =
		await organizedTournament(factories);
	const newMember = await factories.UserFactory.create();

	await impersonate(page, ADMIN_ID);

	const response = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams/${teamId}/add-member`,
		{
			method: "POST",
			headers: authorized(token),
			data: { userId: newMember.id },
		},
	);

	expect(response.status()).toBe(200);

	const teamPage = new TournamentTeamPage(page);
	await teamPage.goto(tournamentId, teamId);

	await expect(teamPage.locators.memberNames).toHaveCount(ROSTER_SIZE + 1);
})

removes member from tournament team via API

Renders/to/:id/to/:id/teams/:tid

Setup
Arrange
  1. Organized tournament → { tournamentId, teamId, token }
    const { tournamentId, teamId, token } =
    	await organizedTournament(factories);
  2. Create user → newMember
    const newMember = await factories.UserFactory.create();
SSendou
Act
  1. Fetch `/api/tournament/${tournamentId}/teams/${teamId}/add-member`, method “POST” headers authorized(token) data user id newMember.id
    await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams/${teamId}/add-member`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: { userId: newMember.id },
    	},
    );
Tournament team page
Act
  1. Open the tournament team page tournamentId, teamId
    await teamPage.goto(tournamentId, teamId);
Assert
  1. member names count is ROSTER_SIZE + 1
    await expect(teamPage.locators.memberNames).toHaveCount(ROSTER_SIZE + 1);
Act
  1. Fetch `/api/tournament/${tournamentId}/teams/${teamId}/remove-member`, method “POST” headers authorized(token) data user id newMember.id → response
    const response = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams/${teamId}/remove-member`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: { userId: newMember.id },
    	},
    );
Assert
  1. response.status() is 200
    expect(response.status()).toBe(200);
Act
  1. Open the tournament team page tournamentId, teamId
    await teamPage.goto(tournamentId, teamId);
Assert
  1. member names count is 4
    await expect(teamPage.locators.memberNames).toHaveCount(ROSTER_SIZE);
Source
test("removes member from tournament team via API", async ({
	page,
	factories,
}) => {
	const { tournamentId, teamId, token } =
		await organizedTournament(factories);
	const newMember = await factories.UserFactory.create();

	await impersonate(page, ADMIN_ID);

	await page.request.fetch(
		`/api/tournament/${tournamentId}/teams/${teamId}/add-member`,
		{
			method: "POST",
			headers: authorized(token),
			data: { userId: newMember.id },
		},
	);

	const teamPage = new TournamentTeamPage(page);
	await teamPage.goto(tournamentId, teamId);
	await expect(teamPage.locators.memberNames).toHaveCount(ROSTER_SIZE + 1);

	const response = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams/${teamId}/remove-member`,
		{
			method: "POST",
			headers: authorized(token),
			data: { userId: newMember.id },
		},
	);

	expect(response.status()).toBe(200);

	await teamPage.goto(tournamentId, teamId);
	await expect(teamPage.locators.memberNames).toHaveCount(ROSTER_SIZE);
})

returns 401 for invalid token

Setup
Arrange
  1. Organized tournament → { tournamentId, teamId }
    const { tournamentId, teamId } = await organizedTournament(factories);
  2. Create user → newMember
    const newMember = await factories.UserFactory.create();
SSendou
Act
  1. Fetch `/api/tournament/${tournamentId}/teams/${teamId}/add-member`, method “POST” headers authorized("invalid-token-12345") data user id newMember.id → response
    const response = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams/${teamId}/add-member`,
    	{
    		method: "POST",
    		headers: authorized("invalid-token-12345"),
    		data: { userId: newMember.id },
    	},
    );
Assert
  1. response.status() is 401
    expect(response.status()).toBe(401);
Act
  1. Json → data
    const data = await response.json();
Assert
  1. data.error is “Invalid token”
    expect(data.error).toBe("Invalid token");
Source
test("returns 401 for invalid token", async ({ page, factories }) => {
	const { tournamentId, teamId } = await organizedTournament(factories);
	const newMember = await factories.UserFactory.create();

	await impersonate(page, ADMIN_ID);

	const response = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams/${teamId}/add-member`,
		{
			method: "POST",
			headers: authorized("invalid-token-12345"),
			data: { userId: newMember.id },
		},
	);

	expect(response.status()).toBe(401);
	const data = await response.json();
	expect(data.error).toBe("Invalid token");
})

returns 403 when using read token for write endpoint

Setup
Arrange
  1. Organized tournament → { tournamentId, teamId }
    const { tournamentId, teamId } = await organizedTournament(factories);
  2. Create user → newMember
    const newMember = await factories.UserFactory.create();
  3. Read token Sendou → token
    const token = await readToken(factories, ADMIN_ID);
SSendou
Act
  1. Fetch `/api/tournament/${tournamentId}/teams/${teamId}/add-member`, method “POST” headers authorized(token) data user id newMember.id → response
    const response = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams/${teamId}/add-member`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: { userId: newMember.id },
    	},
    );
Assert
  1. response.status() is 403
    expect(response.status()).toBe(403);
Act
  1. Json → data
    const data = await response.json();
Assert
  1. data.error is “Write token required”
    expect(data.error).toBe("Write token required");
Source
test("returns 403 when using read token for write endpoint", async ({
	page,
	factories,
}) => {
	const { tournamentId, teamId } = await organizedTournament(factories);
	const newMember = await factories.UserFactory.create();
	const token = await readToken(factories, ADMIN_ID);

	await impersonate(page, ADMIN_ID);

	const response = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams/${teamId}/add-member`,
		{
			method: "POST",
			headers: authorized(token),
			data: { userId: newMember.id },
		},
	);

	expect(response.status()).toBe(403);
	const data = await response.json();
	expect(data.error).toBe("Write token required");
})

updates tournament seeds via API

Setup
Arrange
  1. Organized tournament team count 3 → { tournamentId, token }
    const { tournamentId, token } = await organizedTournament(factories, {
    	teamCount: 3,
    });
SSendou
Act
  1. Fetch `/api/tournament/${tournamentId}/teams`, headers authorized(token) → teamsResponse
    const teamsResponse = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams`,
    	{ headers: authorized(token) },
    );
Assert
  1. teamsResponse.status() is 200
    expect(teamsResponse.status()).toBe(200);
Act
  1. Json → teams
    const teams = await teamsResponse.json();
  2. reversedSeeds = teams .map((team: { id: number }) => team.id) .reverse()
    const reversedSeeds = teams
    	.map((team: { id: number }) => team.id)
    	.reverse();
  3. Fetch `/api/tournament/${tournamentId}/seeds`, method “POST” headers authorized(token) data tournament team ids reversedSeeds → response
    const response = await page.request.fetch(
    	`/api/tournament/${tournamentId}/seeds`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: { tournamentTeamIds: reversedSeeds },
    	},
    );
Assert
  1. response.status() is 200
    expect(response.status()).toBe(200);
Act
  1. Fetch `/api/tournament/${tournamentId}/teams`, headers authorized(token) → updatedTeamsResponse
    const updatedTeamsResponse = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams`,
    	{ headers: authorized(token) },
    );
  2. Json → updatedTeams
    const updatedTeams = await updatedTeamsResponse.json();
Assert
  1. For each [index, tournamentTeamId] of reversedSeeds.entries()
    for (const [index, tournamentTeamId] of reversedSeeds.entries()) {
    	const team = updatedTeams.find(
    		(candidate: { id: number }) => candidate.id === tournamentTeamId,
    	);
    	expect(team.seed).toBe(index + 1);
    }
    1. team = updatedTeams.find( (candidate: { id: number }) => candidate.id === tournament…
      const team = updatedTeams.find(
      	(candidate: { id: number }) => candidate.id === tournamentTeamId,
      );
    2. team.seed is index + 1
      expect(team.seed).toBe(index + 1);
Source
test("updates tournament seeds via API", async ({ page, factories }) => {
	const { tournamentId, token } = await organizedTournament(factories, {
		teamCount: 3,
	});

	await impersonate(page, ADMIN_ID);

	const teamsResponse = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams`,
		{ headers: authorized(token) },
	);
	expect(teamsResponse.status()).toBe(200);
	const teams = await teamsResponse.json();
	const reversedSeeds = teams
		.map((team: { id: number }) => team.id)
		.reverse();

	const response = await page.request.fetch(
		`/api/tournament/${tournamentId}/seeds`,
		{
			method: "POST",
			headers: authorized(token),
			data: { tournamentTeamIds: reversedSeeds },
		},
	);

	expect(response.status()).toBe(200);

	const updatedTeamsResponse = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams`,
		{ headers: authorized(token) },
	);
	const updatedTeams = await updatedTeamsResponse.json();

	for (const [index, tournamentTeamId] of reversedSeeds.entries()) {
		const team = updatedTeams.find(
			(candidate: { id: number }) => candidate.id === tournamentTeamId,
		);
		expect(team.seed).toBe(index + 1);
	}
})

updates tournament starting brackets via API

Setup
Arrange
  1. Organized tournament → { tournamentId, teamId, token }
    const { tournamentId, teamId, token } =
    	await organizedTournament(factories);
SSendou
Act
  1. Fetch `/api/tournament/${tournamentId}/starting-brackets`, method “POST” headers authorized(token) data starting brackets tournament team id teamId starting bracket idx 0 → response
    const response = await page.request.fetch(
    	`/api/tournament/${tournamentId}/starting-brackets`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: {
    			startingBrackets: [
    				{ tournamentTeamId: teamId, startingBracketIdx: 0 },
    			],
    		},
    	},
    );
Assert
  1. response.status() is 200
    expect(response.status()).toBe(200);
Source
test("updates tournament starting brackets via API", async ({
	page,
	factories,
}) => {
	const { tournamentId, teamId, token } =
		await organizedTournament(factories);

	await impersonate(page, ADMIN_ID);

	const response = await page.request.fetch(
		`/api/tournament/${tournamentId}/starting-brackets`,
		{
			method: "POST",
			headers: authorized(token),
			data: {
				startingBrackets: [
					{ tournamentTeamId: teamId, startingBracketIdx: 0 },
				],
			},
		},
	);

	expect(response.status()).toBe(200);
})

upserts tournament team registration via API

Setup
Arrange
  1. Organized tournament → { tournamentId, token }
    const { tournamentId, token } = await organizedTournament(factories);
  2. Create 4 × user → roster
    const roster = await factories.UserFactory.createMany(ROSTER_SIZE);
SSendou
Act
  1. Fetch `/api/tournament/${tournamentId}/teams/upsert`, method “POST” headers authorized(token) data name “Api Pickup” owner user id roster[0].id members roster.map((user) => ({ userId: user.id })) → createResponse
    const createResponse = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams/upsert`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: {
    			name: "Api Pickup",
    			ownerUserId: roster[0].id,
    			members: roster.map((user) => ({ userId: user.id })),
    		},
    	},
    );
Assert
  1. createResponse.status() is 200
    expect(createResponse.status()).toBe(200);
Act
  1. Team by name token, tournamentId name “Api Pickup” → createdTeam
    const createdTeam = await teamByName(page, token, {
    	tournamentId,
    	name: "Api Pickup",
    });
Assert
  1. createdTeam.members has 4 items
    expect(createdTeam.members).toHaveLength(ROSTER_SIZE);
Act
  1. Fetch `/api/tournament/${tournamentId}/teams/upsert`, method “POST” headers authorized(token) data tournament team id createdTeam.id name “Api Pickup Edited” owner user id roster[0].id members roster .slice(0, ROSTER_SIZE - 1) .map((user) => ({ userId: user.id })) → editResponse
    const editResponse = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams/upsert`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: {
    			tournamentTeamId: createdTeam.id,
    			name: "Api Pickup Edited",
    			ownerUserId: roster[0].id,
    			members: roster
    				.slice(0, ROSTER_SIZE - 1)
    				.map((user) => ({ userId: user.id })),
    		},
    	},
    );
Assert
  1. editResponse.status() is 200
    expect(editResponse.status()).toBe(200);
Act
  1. Team by name token, tournamentId name “Api Pickup Edited” → editedTeam
    const editedTeam = await teamByName(page, token, {
    	tournamentId,
    	name: "Api Pickup Edited",
    });
Assert
  1. editedTeam.id is createdTeam.id
    expect(editedTeam.id).toBe(createdTeam.id);
  2. editedTeam.members has ROSTER_SIZE - 1 items
    expect(editedTeam.members).toHaveLength(ROSTER_SIZE - 1);
Source
test("upserts tournament team registration via API", async ({
	page,
	factories,
}) => {
	const { tournamentId, token } = await organizedTournament(factories);
	const roster = await factories.UserFactory.createMany(ROSTER_SIZE);

	await impersonate(page, ADMIN_ID);

	const createResponse = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams/upsert`,
		{
			method: "POST",
			headers: authorized(token),
			data: {
				name: "Api Pickup",
				ownerUserId: roster[0].id,
				members: roster.map((user) => ({ userId: user.id })),
			},
		},
	);
	expect(createResponse.status()).toBe(200);

	const createdTeam = await teamByName(page, token, {
		tournamentId,
		name: "Api Pickup",
	});
	expect(createdTeam.members).toHaveLength(ROSTER_SIZE);

	const editResponse = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams/upsert`,
		{
			method: "POST",
			headers: authorized(token),
			data: {
				tournamentTeamId: createdTeam.id,
				name: "Api Pickup Edited",
				ownerUserId: roster[0].id,
				members: roster
					.slice(0, ROSTER_SIZE - 1)
					.map((user) => ({ userId: user.id })),
			},
		},
	);
	expect(editResponse.status()).toBe(200);

	const editedTeam = await teamByName(page, token, {
		tournamentId,
		name: "Api Pickup Edited",
	});
	expect(editedTeam.id).toBe(createdTeam.id);
	expect(editedTeam.members).toHaveLength(ROSTER_SIZE - 1);
})

returns 400 with field errors for invalid upsert registration body

Setup
Arrange
  1. Organized tournament → { tournamentId, token }
    const { tournamentId, token } = await organizedTournament(factories);
  2. Create user → owner
    const owner = await factories.UserFactory.create();
SSendou
Act
  1. Fetch `/api/tournament/${tournamentId}/teams/upsert`, method “POST” headers authorized(token) data owner user id owner.id members user id owner.id → response
    const response = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams/upsert`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: {
    			ownerUserId: owner.id,
    			members: [{ userId: owner.id }],
    		},
    	},
    );
Assert
  1. response.status() is 400
    expect(response.status()).toBe(400);
Act
  1. Json → data
    const data = await response.json();
Assert
  1. data.fieldErrors.pickUpName is truthy
    expect(data.fieldErrors.pickUpName).toBeTruthy();
Source
test("returns 400 with field errors for invalid upsert registration body", async ({
	page,
	factories,
}) => {
	const { tournamentId, token } = await organizedTournament(factories);
	const owner = await factories.UserFactory.create();

	await impersonate(page, ADMIN_ID);

	const response = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams/upsert`,
		{
			method: "POST",
			headers: authorized(token),
			data: {
				ownerUserId: owner.id,
				members: [{ userId: owner.id }],
			},
		},
	);

	expect(response.status()).toBe(400);
	const data = await response.json();
	expect(data.fieldErrors.pickUpName).toBeTruthy();
})

updates member IGN via API

Setup
Arrange
  1. Organized tournament → { tournamentId, teamId, memberUserIds, token }
    const { tournamentId, teamId, memberUserIds, token } =
    	await organizedTournament(factories);
SSendou
Act
  1. Fetch `/api/tournament/${tournamentId}/teams/${teamId}/update-member-ign`, method “POST” headers authorized(token) data user id memberUserIds[0] in game name “NewName#9999” → response
    const response = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams/${teamId}/update-member-ign`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: { userId: memberUserIds[0], inGameName: "NewName#9999" },
    	},
    );
Assert
  1. response.status() is 200
    expect(response.status()).toBe(200);
Source
test("updates member IGN via API", async ({ page, factories }) => {
	const { tournamentId, teamId, memberUserIds, token } =
		await organizedTournament(factories);

	await impersonate(page, ADMIN_ID);

	const response = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams/${teamId}/update-member-ign`,
		{
			method: "POST",
			headers: authorized(token),
			data: { userId: memberUserIds[0], inGameName: "NewName#9999" },
		},
	);

	expect(response.status()).toBe(200);
})

returns 400 when user is not the organizer of this tournament

Setup
Arrange
  1. Organized tournament → { tournamentId, teamId }
    const { tournamentId, teamId } = await organizedTournament(factories);
  2. Create user null roles “API_ACCESSER” → outsider
    const outsider = await factories.UserFactory.create(null, {
    	roles: ["API_ACCESSER"],
    });
  3. Create user → newMember
    const newMember = await factories.UserFactory.create();
  4. Create api token user id outsider.id type “write” → { token }
    const { token } = await factories.ApiTokenFactory.create({
    	userId: outsider.id,
    	type: "write",
    });
Ooutsider
Act
  1. Fetch `/api/tournament/${tournamentId}/teams/${teamId}/add-member`, method “POST” headers authorized(token) data user id newMember.id → response
    const response = await page.request.fetch(
    	`/api/tournament/${tournamentId}/teams/${teamId}/add-member`,
    	{
    		method: "POST",
    		headers: authorized(token),
    		data: { userId: newMember.id },
    	},
    );
Assert
  1. response.status() is 400
    expect(response.status()).toBe(400);
Act
  1. Json → data
    const data = await response.json();
Assert
  1. data.error is “Not an organizer”
    expect(data.error).toBe("Not an organizer");
Source
test("returns 400 when user is not the organizer of this tournament", async ({
	page,
	factories,
}) => {
	const { tournamentId, teamId } = await organizedTournament(factories);
	const outsider = await factories.UserFactory.create(null, {
		roles: ["API_ACCESSER"],
	});
	const newMember = await factories.UserFactory.create();
	const { token } = await factories.ApiTokenFactory.create({
		userId: outsider.id,
		type: "write",
	});

	await impersonate(page, outsider.id);

	const response = await page.request.fetch(
		`/api/tournament/${tournamentId}/teams/${teamId}/add-member`,
		{
			method: "POST",
			headers: authorized(token),
			data: { userId: newMember.id },
		},
	);

	expect(response.status()).toBe(400);
	const data = await response.json();
	expect(data.error).toBe("Not an organizer");
})