Public API Act
Fetch “/api/tournament/1” , method “OPTIONS” → response
const response = await page.request.fetch ("/api/tournament/1" , {
method: "OPTIONS" ,
});Assert
response.status () is 204
expect (response.status ()).toBe (204 );response.headers ()["access-control-allow-origin" ] is “*”
expect (response.headers ()["access-control-allow-origin" ]).toBe ("*" );response.headers ()["access-control-allow-methods" ] contains “GET”
expect (response.headers ()["access-control-allow-methods" ]).toContain ("GET" );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" ,
);
})Act
Fetch “/api/tournament/1” → response
const response = await page.request.fetch ("/api/tournament/1" );Assert
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 Act
Fetch `/api/user/${ADMIN_ID}/ids` → response
const response = await page.request.fetch (`/api/user/${ADMIN_ID}/ids` );Assert
response.status () is 200
expect (response.status ()).toBe (200 );Act
Json → data
const data = await response.json ();Assert
data.id is Sendou
expect (data.id).toBe (ADMIN_ID );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
Api page Arrange
Grant user Sendou roles “API_ACCESSER”
await factories.UserFactory .grant (ADMIN_ID , { roles: ["API_ACCESSER" ] });Api page Act
Open the api page
await api.goto ();Generate token “read” → token
const token = await api.generateToken ("read" );Assert
token has 20 items
expect (token).toHaveLength (TOKEN_LENGTH );Act
Fetch `/api/user/${ADMIN_ID}` , headers authorized (token) → response
const response = await page.request.fetch (`/api/user/${ADMIN_ID}` , {
headers: authorized (token),
});Assert
response.status () is 200
expect (response.status ()).toBe (200 );Act
Json → data
const data = await response.json ();Assert
data.id is Sendou
expect (data.id).toBe (ADMIN_ID );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 Arrange
Create FULL_GROUP_SIZE × user → alpha
const alpha = await factories.UserFactory .createMany (FULL_GROUP_SIZE );Create FULL_GROUP_SIZE × user → bravo
const bravo = await factories.UserFactory .createMany (FULL_GROUP_SIZE );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),
});Read token Sendou → token
const token = await readToken (factories, ADMIN_ID );Act
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
response.status () is 200
expect (response.status ()).toBe (200 );Act
Json → data
const data = await response.json ();Assert
data.matchId is match.id
expect (data.matchId).toBe (match.id);data.lobby is “sendouq”
expect (data.lobby).toBe ("sendouq" );data.tournamentId is null
expect (data.tournamentId).toBeNull ();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 Arrange
Create TEAM_COUNT * ROSTER_SIZE × user → players
const players = await factories.UserFactory .createMany (
TEAM_COUNT * ROSTER_SIZE ,
);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),
);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 },
);Read token Sendou → token
const token = await readToken (factories, ADMIN_ID );Act
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
response.status () is 200
expect (response.status ()).toBe (200 );Act
Json → teams
const teams = await response.json ();Assert
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,
);winner.placement is 1
expect (winner.placement).toBe (1 );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 ,
});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,
);firstRoundLoser.placement is 3
expect (firstRoundLoser.placement).toBe (3 );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.
Arrange
Create TEAM_COUNT * ROSTER_SIZE × user → players
const players = await factories.UserFactory .createMany (
TEAM_COUNT * ROSTER_SIZE ,
);startedAt = subHours (new Date (), 2 )
const startedAt = subHours (new Date (), 2 );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),
),
},
);tournamentMatchId = tournament.matches[0 ].id
const tournamentMatchId = tournament.matches[0 ].id;Cast match tournament tournament id tournament.id match id tournamentMatchId twitch account “sendouq”
await factories.TournamentFactory .castMatch ({
tournamentId: tournament.id,
matchId: tournamentMatchId,
twitchAccount: "sendouq" ,
});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),
});Create tournament organization owner Sendou → organization
const organization = await factories.TournamentOrganizationFactory .create ({
ownerId: ADMIN_ID ,
});Create FULL_GROUP_SIZE × user → alpha
const alpha = await factories.UserFactory .createMany (FULL_GROUP_SIZE );Create FULL_GROUP_SIZE × user → bravo
const bravo = await factories.UserFactory .createMany (FULL_GROUP_SIZE );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),
});Read token Sendou → token
const token = await readToken (factories, ADMIN_ID );Act
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}` ,
];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 );
}Fetch path, headers authorized (token) → response
const response = await page.request.fetch (path, {
headers: authorized (token),
});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 Arrange
Organized tournament with private team info true → { tournamentId, token }
const { tournamentId, token } = await organizedTournament (factories, {
withPrivateTeamInfo: true ,
});Create user → outsider
const outsider = await factories.UserFactory .create ();Read token outsider.id → outsiderToken
const outsiderToken = await readToken (factories, outsider.id);Act
Fetch teams token, tournamentId → asOrganizer
const asOrganizer = await fetchTeams (page, token, tournamentId);Fetch teams outsiderToken, tournamentId → asOutsider
const asOutsider = await fetchTeams (page, outsiderToken, tournamentId);Assert
asOrganizer[0 ].mapPool has 2 items
expect (asOrganizer[0 ].mapPool).toHaveLength (2 );asOrganizer[0 ].logoUrl equals expect.any (String )
expect (asOrganizer[0 ].logoUrl).toEqual (expect.any (String ));asOrganizer[0 ].members[0 ].friendCode equals expect.any (String )
expect (asOrganizer[0 ].members[0 ].friendCode).toEqual (expect.any (String ));asOutsider[0 ].mapPool is null
expect (asOutsider[0 ].mapPool).toBeNull ();asOutsider[0 ].logoUrl is null
expect (asOutsider[0 ].logoUrl).toBeNull ();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
Tournament team page Arrange
Organized tournament → { tournamentId, teamId, token }
const { tournamentId, teamId, token } =
await organizedTournament (factories);Create user → newMember
const newMember = await factories.UserFactory .create ();Act
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
response.status () is 200
expect (response.status ()).toBe (200 );Tournament team page Act
Open the tournament team page tournamentId, teamId
await teamPage.goto (tournamentId, teamId);Assert
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
Tournament team page Arrange
Organized tournament → { tournamentId, teamId, token }
const { tournamentId, teamId, token } =
await organizedTournament (factories);Create user → newMember
const newMember = await factories.UserFactory .create ();Act
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
Open the tournament team page tournamentId, teamId
await teamPage.goto (tournamentId, teamId);Assert
member names count is ROSTER_SIZE + 1
await expect (teamPage.locators.memberNames).toHaveCount (ROSTER_SIZE + 1 );Act
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
response.status () is 200
expect (response.status ()).toBe (200 );Act
Open the tournament team page tournamentId, teamId
await teamPage.goto (tournamentId, teamId);Assert
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 Arrange
Organized tournament → { tournamentId, teamId }
const { tournamentId, teamId } = await organizedTournament (factories);Create user → newMember
const newMember = await factories.UserFactory .create ();Act
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
response.status () is 401
expect (response.status ()).toBe (401 );Act
Json → data
const data = await response.json ();Assert
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 Arrange
Organized tournament → { tournamentId, teamId }
const { tournamentId, teamId } = await organizedTournament (factories);Create user → newMember
const newMember = await factories.UserFactory .create ();Read token Sendou → token
const token = await readToken (factories, ADMIN_ID );Act
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
response.status () is 403
expect (response.status ()).toBe (403 );Act
Json → data
const data = await response.json ();Assert
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 Arrange
Organized tournament team count 3 → { tournamentId, token }
const { tournamentId, token } = await organizedTournament (factories, {
teamCount: 3 ,
});Act
Fetch `/api/tournament/${tournamentId}/teams` , headers authorized (token) → teamsResponse
const teamsResponse = await page.request.fetch (
`/api/tournament/${tournamentId}/teams` ,
{ headers: authorized (token) },
);Assert
teamsResponse.status () is 200
expect (teamsResponse.status ()).toBe (200 );Act
Json → teams
const teams = await teamsResponse.json ();reversedSeeds = teams .map ((team: { id: number }) => team.id) .reverse ()
const reversedSeeds = teams
.map ((team: { id: number }) => team.id)
.reverse ();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
response.status () is 200
expect (response.status ()).toBe (200 );Act
Fetch `/api/tournament/${tournamentId}/teams` , headers authorized (token) → updatedTeamsResponse
const updatedTeamsResponse = await page.request.fetch (
`/api/tournament/${tournamentId}/teams` ,
{ headers: authorized (token) },
);Json → updatedTeams
const updatedTeams = await updatedTeamsResponse.json ();Assert
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 );
}team = updatedTeams.find ( (candidate: { id: number }) => candidate.id === tournament…
const team = updatedTeams.find (
(candidate: { id: number }) => candidate.id === tournamentTeamId,
);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 Arrange
Organized tournament → { tournamentId, teamId, token }
const { tournamentId, teamId, token } =
await organizedTournament (factories);Act
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
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 Arrange
Organized tournament → { tournamentId, token }
const { tournamentId, token } = await organizedTournament (factories);Create 4 × user → roster
const roster = await factories.UserFactory .createMany (ROSTER_SIZE );Act
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
createResponse.status () is 200
expect (createResponse.status ()).toBe (200 );Act
Team by name token, tournamentId name “Api Pickup” → createdTeam
const createdTeam = await teamByName (page, token, {
tournamentId,
name: "Api Pickup" ,
});Assert
createdTeam.members has 4 items
expect (createdTeam.members).toHaveLength (ROSTER_SIZE );Act
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
editResponse.status () is 200
expect (editResponse.status ()).toBe (200 );Act
Team by name token, tournamentId name “Api Pickup Edited” → editedTeam
const editedTeam = await teamByName (page, token, {
tournamentId,
name: "Api Pickup Edited" ,
});Assert
editedTeam.id is createdTeam.id
expect (editedTeam.id).toBe (createdTeam.id);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 Arrange
Organized tournament → { tournamentId, token }
const { tournamentId, token } = await organizedTournament (factories);Create user → owner
const owner = await factories.UserFactory .create ();Act
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
response.status () is 400
expect (response.status ()).toBe (400 );Act
Json → data
const data = await response.json ();Assert
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 Arrange
Organized tournament → { tournamentId, teamId, memberUserIds, token }
const { tournamentId, teamId, memberUserIds, token } =
await organizedTournament (factories);Act
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
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 Arrange
Organized tournament → { tournamentId, teamId }
const { tournamentId, teamId } = await organizedTournament (factories);Create user null roles “API_ACCESSER” → outsider
const outsider = await factories.UserFactory .create (null , {
roles: ["API_ACCESSER" ],
});Create user → newMember
const newMember = await factories.UserFactory .create ();Create api token user id outsider.id type “write” → { token }
const { token } = await factories.ApiTokenFactory .create ({
userId: outsider.id,
type : "write" ,
});Act
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
response.status () is 400
expect (response.status ()).toBe (400 );Act
Json → data
const data = await response.json ();Assert
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" );
})ApiPage.gotoe2e/pages/api/api-page.ts:25
No JSDoc
async goto () {
await navigate ({ page: this .page, url: API_PAGE });
}ApiPage.generateTokene2e/pages/api/api-page.ts:29
No JSDoc
async generateToken (type : ApiTokenType ) {
await this .section (type )
.getByRole ("button" , { name: "Generate Token" })
.click ();
await this .page.waitForURL (API_PAGE );
return this .revealToken (type );
}ApiPagee2e/pages/api/api-page.ts:12
readTokene2e/api-public.spec.ts:620
No JSDoc
async function readToken (factories: Factories , userId: number ) {
await factories.UserFactory .grant (userId, { roles: ["API_ACCESSER" ] });
const { token } = await factories.ApiTokenFactory .create ({
userId,
type : "read" ,
});
return token;
}organizedTournamente2e/api-public.spec.ts:554
async function organizedTournament (
factories: Factories ,
{
teamCount = 1 ,
withPrivateTeamInfo = false ,
}: { teamCount?: number ; withPrivateTeamInfo?: boolean } = {},
) {
await factories.UserFactory .grant (ADMIN_ID , { roles: ["API_ACCESSER" ] });
const tournament = await factories.TournamentFactory .create ({
authorId: ADMIN_ID ,
startTimes: [dateToDatabaseTimestamp (addHours (new Date (), 2 ))],
});
// … fetchTeamse2e/api-public.spec.ts:598
No JSDoc
async function fetchTeams (page: Page , token: string , tournamentId: number ) {
const response = await page.request.fetch (
`/api/tournament/${tournamentId}/teams` ,
{ headers: authorized (token) },
);
expect (response.status ()).toBe (200 );
return (await response.json ()) as GetTournamentTeamsResponse ;
}TournamentTeamPage.gotoe2e/pages/tournament/tournament-team-page.ts:17
No JSDoc
async goto (tournamentId: number , tournamentTeamId: number ) {
await navigate ({
page: this .page,
url: tournamentTeamPage ({ tournamentId, tournamentTeamId }),
});
}TournamentTeamPage.locators.memberNamese2e/pages/tournament/tournament-team-page.ts:13
No JSDoc
page.getByTestId ("team-member-name" ) TournamentTeamPagee2e/pages/tournament/tournament-team-page.ts:6
No JSDoc
teamByNamee2e/api-public.spec.ts:608
No JSDoc
async function teamByName (
page: Page ,
token: string ,
{ tournamentId, name }: { tournamentId: number ; name: string },
) {
const teams = await fetchTeams (page, token, tournamentId);
const team = teams.find ((candidate) => candidate.name === name);
invariant (team, `No team named ${name} in tournament ${tournamentId}` );
return team;
}