mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
558 lines
17 KiB
TypeScript
558 lines
17 KiB
TypeScript
import { adminSecretsStore, env, SELF } from 'cloudflare:test'
|
||
import { beforeAll, describe, expect, it } from 'vitest'
|
||
|
||
import { RELATIONSHIP_SCHEMA_DDL, RelationshipType } from '@repo/domain'
|
||
|
||
import { SCHEMA_DDL } from '../../leaderboard-db'
|
||
|
||
beforeAll(async () => {
|
||
// Seed the shared JWT signing key into the local Secrets Store so .get() resolves.
|
||
await adminSecretsStore(env.JWT_SECRET).create(TEST_SECRET)
|
||
for (const stmt of SCHEMA_DDL) await env.DB.prepare(stmt).run()
|
||
// The `api` worker's relationship table — the friends filter reads it.
|
||
for (const stmt of RELATIONSHIP_SCHEMA_DDL) await env.DB.prepare(stmt).run()
|
||
})
|
||
|
||
const befriend = (a: number, b: number) =>
|
||
env.DB.prepare(
|
||
'INSERT INTO relationship (requester_id, target_id, relationship_type) VALUES (?1, ?2, ?3)'
|
||
)
|
||
.bind(a, b, RelationshipType.Friend)
|
||
.run()
|
||
|
||
// Mint a token the way the `auth` worker does, signing with the shared test key seeded
|
||
// into the JWT_SECRET store, so this worker's validation accepts it.
|
||
const TEST_SECRET = 'test-signing-key'
|
||
|
||
function b64url(input: ArrayBuffer | string): string {
|
||
const bytes = typeof input === 'string' ? new TextEncoder().encode(input) : new Uint8Array(input)
|
||
let binary = ''
|
||
for (const byte of bytes) binary += String.fromCharCode(byte)
|
||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||
}
|
||
|
||
async function bearer(sub: number): Promise<Record<string, string>> {
|
||
const now = Math.floor(Date.now() / 1000)
|
||
const signingInput = `${b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))}.${b64url(
|
||
JSON.stringify({ sub: String(sub), exp: now + 3600 })
|
||
)}`
|
||
const key = await crypto.subtle.importKey(
|
||
'raw',
|
||
new TextEncoder().encode(TEST_SECRET),
|
||
{ name: 'HMAC', hash: 'SHA-256' },
|
||
false,
|
||
['sign']
|
||
)
|
||
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(signingInput))
|
||
return { Authorization: `Bearer ${signingInput}.${b64url(sig)}` }
|
||
}
|
||
|
||
const post = (path: string, body: unknown, headers: Record<string, string> = {}) =>
|
||
SELF.fetch(`https://example.com/leaderboard/${path}`, {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json', ...headers },
|
||
body: JSON.stringify(body),
|
||
})
|
||
|
||
async function setStat(
|
||
player: number,
|
||
RoomId: number,
|
||
StatValue: number,
|
||
CurrentStatValue: number | null = null,
|
||
StatChannel = 2
|
||
) {
|
||
const res = await post(
|
||
'CheckAndSetStat',
|
||
{ StatChannel, RoomId, StatValue, CurrentStatValue },
|
||
await bearer(player)
|
||
)
|
||
expect(res.status).toBe(200)
|
||
// The whole body is the number — not an envelope, not `{ value: 0 }`.
|
||
expect(await res.text()).toBe('0')
|
||
}
|
||
|
||
const stored = (player: number, room: number, channel = 2) =>
|
||
env.DB.prepare(
|
||
'SELECT stat_value FROM leaderboard WHERE player_id = ?1 AND room_id = ?2 AND stat_channel = ?3'
|
||
)
|
||
.bind(player, room, channel)
|
||
.first<{ stat_value: number }>()
|
||
|
||
it('response with hello world', async () => {
|
||
const res = await SELF.fetch('https://example.com')
|
||
expect(res.status).toBe(200)
|
||
expect(await res.text()).toMatchInlineSnapshot(`"hello, world!"`)
|
||
})
|
||
|
||
describe('an empty board', () => {
|
||
it('answers GetNearbyScores with an empty row list', async () => {
|
||
const res = await post('GetNearbyScores', { PlayerId: 1, RoomId: 999, StatChannel: 1 })
|
||
expect(res.status).toBe(200)
|
||
expect(await res.json()).toEqual({ Rows: [] })
|
||
})
|
||
|
||
it('answers GetRanks with an empty row list', async () => {
|
||
const res = await post('GetRanks', {
|
||
RankStart: 0,
|
||
RankEnd: 9,
|
||
PlayerId: 2,
|
||
StatChannel: 1,
|
||
RoomId: 999,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
})
|
||
expect(res.status).toBe(200)
|
||
expect(await res.json()).toEqual({ Rows: [] })
|
||
})
|
||
|
||
it('answers GetPlayerRank with the unranked sentinel and the caller’s own id', async () => {
|
||
const res = await post('GetPlayerRank', {
|
||
PlayerId: 205,
|
||
StatChannel: 2,
|
||
RoomId: 999,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
})
|
||
expect(res.status).toBe(200)
|
||
// Three fields, no board selectors: the client pairs the answer with its own question.
|
||
// Ranks are 0-based — the client adds one to draw them — so 0 IS first place and the
|
||
// sentinel has to be a big number rather than 0.
|
||
expect(await res.json()).toEqual({ PlayerId: 205, Score: 0, Rank: 99999 })
|
||
})
|
||
|
||
it('answers GetPlayerRank even when the body is unreadable', async () => {
|
||
const res = await SELF.fetch('https://example.com/leaderboard/GetPlayerRank', {
|
||
method: 'POST',
|
||
body: 'not json',
|
||
})
|
||
// A board that fails to draw is worse than one that draws the player as unranked.
|
||
expect(res.status).toBe(200)
|
||
expect(await res.json()).toEqual({ PlayerId: 0, Score: 0, Rank: 99999 })
|
||
})
|
||
|
||
it('answers GetRanks with an empty board when the body is unreadable', async () => {
|
||
const res = await SELF.fetch('https://example.com/leaderboard/GetRanks', {
|
||
method: 'POST',
|
||
body: 'not json',
|
||
})
|
||
expect(res.status).toBe(200)
|
||
expect(await res.json()).toEqual({ Rows: [] })
|
||
})
|
||
})
|
||
|
||
describe('CheckAndSetStat', () => {
|
||
it('refuses a write with no bearer token', async () => {
|
||
const res = await post('CheckAndSetStat', {
|
||
StatChannel: 2,
|
||
RoomId: 14,
|
||
StatValue: 1,
|
||
CurrentStatValue: null,
|
||
})
|
||
expect(res.status).toBe(401)
|
||
})
|
||
|
||
it('stores the caller’s value on the board and answers a bare 0', async () => {
|
||
await setStat(42, 14, 3)
|
||
expect(await stored(42, 14)).toEqual({ stat_value: 3 })
|
||
})
|
||
|
||
it('overwrites when the client believes nothing is stored', async () => {
|
||
await setStat(43, 14, 3)
|
||
await setStat(43, 14, 7, null)
|
||
expect(await stored(43, 14)).toEqual({ stat_value: 7 })
|
||
})
|
||
|
||
it('writes only when CurrentStatValue matches what is stored', async () => {
|
||
await setStat(44, 14, 3)
|
||
await setStat(44, 14, 4, 3)
|
||
expect(await stored(44, 14)).toEqual({ stat_value: 4 })
|
||
// A stale client that still believes 3 is stored doesn't walk the board backwards.
|
||
await setStat(44, 14, 1, 3)
|
||
expect(await stored(44, 14)).toEqual({ stat_value: 4 })
|
||
})
|
||
|
||
it('keeps rooms and channels apart', async () => {
|
||
await setStat(45, 14, 3)
|
||
await setStat(45, 15, 9)
|
||
await setStat(45, 14, 5, null, 7)
|
||
expect(await stored(45, 14)).toEqual({ stat_value: 3 })
|
||
expect(await stored(45, 15)).toEqual({ stat_value: 9 })
|
||
expect(await stored(45, 14, 7)).toEqual({ stat_value: 5 })
|
||
})
|
||
})
|
||
|
||
describe('a scored board', () => {
|
||
// Room 100: player 1 has 10, player 2 has 30, player 3 has 20, player 4 has 20 (ties
|
||
// break on the lower id, so 3 ranks ahead of 4).
|
||
const ROOM = 100
|
||
beforeAll(async () => {
|
||
await setStat(1, ROOM, 10)
|
||
await setStat(2, ROOM, 30)
|
||
await setStat(3, ROOM, 20)
|
||
await setStat(4, ROOM, 20)
|
||
})
|
||
|
||
it('answers another channel of the same room as its own, empty board', async () => {
|
||
const res = await post('GetRanks', {
|
||
RankStart: 0,
|
||
RankEnd: 9,
|
||
PlayerId: 1,
|
||
StatChannel: 3,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
})
|
||
expect(await res.json()).toEqual({ Rows: [] })
|
||
})
|
||
|
||
it('ranks highest value first with 0-based ranks', async () => {
|
||
const res = await post('GetRanks', {
|
||
RankStart: 0,
|
||
RankEnd: 9,
|
||
PlayerId: 1,
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
})
|
||
expect(await res.json()).toEqual({
|
||
Rows: [
|
||
{ PlayerId: 2, Score: 30, Rank: 0 },
|
||
{ PlayerId: 3, Score: 20, Rank: 1 },
|
||
{ PlayerId: 4, Score: 20, Rank: 2 },
|
||
{ PlayerId: 1, Score: 10, Rank: 3 },
|
||
],
|
||
})
|
||
})
|
||
|
||
it('pages the board by rank, inclusive at both ends', async () => {
|
||
const res = await post('GetRanks', {
|
||
RankStart: 2,
|
||
RankEnd: 3,
|
||
PlayerId: 1,
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
})
|
||
expect(await res.json()).toEqual({
|
||
Rows: [
|
||
{ PlayerId: 4, Score: 20, Rank: 2 },
|
||
{ PlayerId: 1, Score: 10, Rank: 3 },
|
||
],
|
||
})
|
||
})
|
||
|
||
it('ranks lowest first when asked to sort ascending', async () => {
|
||
const res = await post('GetRanks', {
|
||
RankStart: 1,
|
||
RankEnd: 2,
|
||
PlayerId: 1,
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: true,
|
||
})
|
||
expect(await res.json()).toEqual({
|
||
Rows: [
|
||
{ PlayerId: 3, Score: 20, Rank: 1 },
|
||
{ PlayerId: 4, Score: 20, Rank: 2 },
|
||
],
|
||
})
|
||
})
|
||
|
||
it('answers a player’s own rank consistently with the page', async () => {
|
||
const res = await post('GetPlayerRank', {
|
||
PlayerId: 4,
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
})
|
||
expect(await res.json()).toEqual({ PlayerId: 4, Score: 20, Rank: 2 })
|
||
})
|
||
|
||
it('answers a player with no row in the room as unranked', async () => {
|
||
const res = await post('GetPlayerRank', {
|
||
PlayerId: 77,
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
})
|
||
expect(await res.json()).toEqual({ PlayerId: 77, Score: 0, Rank: 99999 })
|
||
})
|
||
|
||
it('answers the rows around a player, clamped to the board', async () => {
|
||
const res = await post('GetNearbyScores', {
|
||
PlayerId: 4,
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
WindowSize: 1,
|
||
})
|
||
expect(await res.json()).toEqual({
|
||
Rows: [
|
||
{ PlayerId: 3, Score: 20, Rank: 1 },
|
||
{ PlayerId: 4, Score: 20, Rank: 2 },
|
||
{ PlayerId: 1, Score: 10, Rank: 3 },
|
||
],
|
||
})
|
||
})
|
||
|
||
it('answers the top of the board around a player who isn’t on it', async () => {
|
||
const res = await post('GetNearbyScores', {
|
||
PlayerId: 77,
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
WindowSize: 1,
|
||
})
|
||
expect(await res.json()).toEqual({
|
||
Rows: [
|
||
{ PlayerId: 2, Score: 30, Rank: 0 },
|
||
{ PlayerId: 3, Score: 20, Rank: 1 },
|
||
{ PlayerId: 4, Score: 20, Rank: 2 },
|
||
],
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('the friends filter', () => {
|
||
// Room 200, channel 2: players 11..15 score 50, 40, 30, 20, 10. Player 14 is friends
|
||
// with 11 (14 requested) and 15 (15 requested); 12 and 13 are strangers, and 16 is a
|
||
// friend with no score. Globally 14 is 4th (`Rank` 3); among friends 2nd (`Rank` 1).
|
||
const ROOM = 200
|
||
const board = (extra: object) => ({
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 1,
|
||
SortAscending: false,
|
||
...extra,
|
||
})
|
||
beforeAll(async () => {
|
||
await setStat(11, ROOM, 50)
|
||
await setStat(12, ROOM, 40)
|
||
await setStat(13, ROOM, 30)
|
||
await setStat(14, ROOM, 20)
|
||
await setStat(15, ROOM, 10)
|
||
await befriend(14, 11)
|
||
await befriend(15, 14)
|
||
await befriend(14, 16)
|
||
// A pending request is not a friendship.
|
||
await env.DB.prepare(
|
||
'INSERT INTO relationship (requester_id, target_id, relationship_type) VALUES (14, 12, ?1)'
|
||
)
|
||
.bind(RelationshipType.FriendRequestSent)
|
||
.run()
|
||
})
|
||
|
||
it('ranks the viewer among their friends on GetRanks', async () => {
|
||
const res = await post('GetRanks', board({ PlayerId: 14, RankStart: 0, RankEnd: 9 }))
|
||
expect(await res.json()).toEqual({
|
||
Rows: [
|
||
{ PlayerId: 11, Score: 50, Rank: 0 },
|
||
{ PlayerId: 14, Score: 20, Rank: 1 },
|
||
{ PlayerId: 15, Score: 10, Rank: 2 },
|
||
],
|
||
})
|
||
})
|
||
|
||
it('gives the friends rank on GetPlayerRank', async () => {
|
||
const res = await post('GetPlayerRank', board({ PlayerId: 14 }))
|
||
expect(await res.json()).toEqual({ PlayerId: 14, Score: 20, Rank: 1 })
|
||
const global = await post('GetPlayerRank', board({ PlayerId: 14, FilterType: 0 }))
|
||
expect(await global.json()).toEqual({ PlayerId: 14, Score: 20, Rank: 3 })
|
||
})
|
||
|
||
it('centres GetNearbyScores on the viewer within their friends', async () => {
|
||
const res = await post('GetNearbyScores', board({ PlayerId: 14, WindowSize: 10 }))
|
||
expect(await res.json()).toEqual({
|
||
Rows: [
|
||
{ PlayerId: 11, Score: 50, Rank: 0 },
|
||
{ PlayerId: 14, Score: 20, Rank: 1 },
|
||
{ PlayerId: 15, Score: 10, Rank: 2 },
|
||
],
|
||
})
|
||
})
|
||
|
||
it('shows a player with no friends only themself', async () => {
|
||
const res = await post('GetRanks', board({ PlayerId: 13, RankStart: 0, RankEnd: 9 }))
|
||
expect(await res.json()).toEqual({ Rows: [{ PlayerId: 13, Score: 30, Rank: 0 }] })
|
||
})
|
||
})
|
||
|
||
describe('the nearby window', () => {
|
||
// Room 300: players 21..45 score 25..1, so 25 rows with player 33 in the middle (rank 12,
|
||
// the 13th row — ranks are 0-based).
|
||
const ROOM = 300
|
||
beforeAll(async () => {
|
||
for (let p = 21; p <= 45; p++) await setStat(p, ROOM, 46 - p)
|
||
})
|
||
|
||
it('caps WindowSize at 10 either side', async () => {
|
||
const res = await post('GetNearbyScores', {
|
||
PlayerId: 33,
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
WindowSize: 100,
|
||
})
|
||
const { Rows } = (await res.json()) as { Rows: { Rank: number }[] }
|
||
expect(Rows).toHaveLength(21)
|
||
expect(Rows[0]?.Rank).toBe(2)
|
||
expect(Rows[20]?.Rank).toBe(22)
|
||
})
|
||
|
||
// The slice the client actually asks for: 0..9 is TEN rows, starting at the top. Read as
|
||
// 1-based this served nine rows starting at the second one.
|
||
it('serves ten rows for the client’s RankStart 0, RankEnd 9', async () => {
|
||
const res = await post('GetRanks', {
|
||
RankStart: 0,
|
||
RankEnd: 9,
|
||
PlayerId: 33,
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
})
|
||
const { Rows } = (await res.json()) as {
|
||
Rows: Array<{ PlayerId: number; Score: number; Rank: number }>
|
||
}
|
||
expect(Rows).toHaveLength(10)
|
||
expect(Rows[0]).toEqual({ PlayerId: 21, Score: 25, Rank: 0 })
|
||
expect(Rows[9]?.Rank).toBe(9)
|
||
})
|
||
|
||
it('defaults WindowSize to 10 when absent', async () => {
|
||
const res = await post('GetNearbyScores', {
|
||
PlayerId: 33,
|
||
StatChannel: 2,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
})
|
||
const { Rows } = (await res.json()) as { Rows: unknown[] }
|
||
expect(Rows).toHaveLength(21)
|
||
})
|
||
})
|
||
|
||
describe('the 2026 body shape', () => {
|
||
// 20260323 renamed the read selectors: `PrimaryKey` is the room, `SecondaryKey` the
|
||
// channel, and `LeaderboardType` 0 says it's a room board. Same room, same board as the
|
||
// 2025 body. `CheckAndSetStat` did NOT change.
|
||
const ROOM = 400
|
||
const body2026 = (extra: object) => ({
|
||
PlayerId: 51,
|
||
PrimaryKey: ROOM,
|
||
SecondaryKey: 1,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
LeaderboardType: 0,
|
||
...extra,
|
||
})
|
||
const ROWS = {
|
||
Rows: [
|
||
{ PlayerId: 52, Score: 9, Rank: 0 },
|
||
{ PlayerId: 51, Score: 7, Rank: 1 },
|
||
],
|
||
}
|
||
beforeAll(async () => {
|
||
await setStat(51, ROOM, 7, null, 1)
|
||
await setStat(52, ROOM, 9, null, 1)
|
||
})
|
||
|
||
it('reads the board 2025’s RoomId + StatChannel name', async () => {
|
||
const legacy = await post('GetNearbyScores', {
|
||
WindowSize: 10,
|
||
PlayerId: 51,
|
||
StatChannel: 1,
|
||
RoomId: ROOM,
|
||
FilterType: 0,
|
||
SortAscending: false,
|
||
})
|
||
expect(await legacy.json()).toEqual(ROWS)
|
||
const modern = await post('GetNearbyScores', body2026({ WindowSize: 10 }))
|
||
expect(await modern.json()).toEqual(ROWS)
|
||
const ranks = await post('GetRanks', body2026({ RankStart: 0, RankEnd: 9 }))
|
||
expect(await ranks.json()).toEqual(ROWS)
|
||
const rank = await post('GetPlayerRank', body2026({}))
|
||
expect(await rank.json()).toEqual({ PlayerId: 51, Score: 7, Rank: 1 })
|
||
})
|
||
|
||
it('ignores RoomId/StatChannel once a 2026 key is present', async () => {
|
||
// The 2026 keys name no board here, so the stale 2025 ones must not be used instead.
|
||
const res = await post(
|
||
'GetRanks',
|
||
body2026({ PrimaryKey: null, RoomId: ROOM, StatChannel: 1, RankStart: 0, RankEnd: 9 })
|
||
)
|
||
expect(await res.json()).toEqual({ Rows: [] })
|
||
})
|
||
|
||
it('answers a null-keyed Event board on GetPlayerRank with the full object', async () => {
|
||
const res = await post(
|
||
'GetPlayerRank',
|
||
body2026({ PrimaryKey: null, SecondaryKey: null, LeaderboardType: 1 })
|
||
)
|
||
// 20260323 throws "Response was empty" on null/empty/204: always the three keys.
|
||
expect(res.status).toBe(200)
|
||
expect(await res.json()).toEqual({ PlayerId: 51, Score: 0, Rank: 99999 })
|
||
})
|
||
|
||
it('answers an unranked player with the full object', async () => {
|
||
const res = await post('GetPlayerRank', body2026({ PlayerId: 53 }))
|
||
expect(res.status).toBe(200)
|
||
expect(await res.json()).toEqual({ PlayerId: 53, Score: 0, Rank: 99999 })
|
||
})
|
||
|
||
it('answers null keys and Event boards with an empty Rows, never a bare array', async () => {
|
||
for (const extra of [{ PrimaryKey: null }, { SecondaryKey: null }, { LeaderboardType: 1 }]) {
|
||
const ranks = await post('GetRanks', body2026({ RankStart: 0, RankEnd: 9, ...extra }))
|
||
expect(ranks.status).toBe(200)
|
||
expect(await ranks.text()).toBe('{"Rows":[]}')
|
||
const nearby = await post('GetNearbyScores', body2026({ WindowSize: 10, ...extra }))
|
||
expect(nearby.status).toBe(200)
|
||
expect(await nearby.text()).toBe('{"Rows":[]}')
|
||
}
|
||
})
|
||
|
||
it('still writes CheckAndSetStat by RoomId + StatChannel, answering a bare 0', async () => {
|
||
const res = await post(
|
||
'CheckAndSetStat',
|
||
{ StatChannel: 1, RoomId: ROOM, StatValue: 12, CurrentStatValue: 7 },
|
||
await bearer(51)
|
||
)
|
||
// The whole body is the number: not `{"result":0}`, not an envelope.
|
||
expect(res.headers.get('content-type')).toContain('application/json')
|
||
expect(await res.text()).toBe('0')
|
||
expect(await stored(51, ROOM, 1)).toEqual({ stat_value: 12 })
|
||
})
|
||
|
||
it('does not write CheckAndSetStat by the read-side PrimaryKey/SecondaryKey', async () => {
|
||
const res = await post(
|
||
'CheckAndSetStat',
|
||
{ PrimaryKey: ROOM + 1, SecondaryKey: 1, StatValue: 5, CurrentStatValue: null },
|
||
await bearer(51)
|
||
)
|
||
expect(await res.text()).toBe('0')
|
||
expect(await stored(51, ROOM + 1, 1)).toBeNull()
|
||
})
|
||
})
|
||
|
||
it('serves an openapi spec with no dangling refs', async () => {
|
||
const res = await SELF.fetch('https://example.com/openapi.json')
|
||
expect(res.status).toBe(200)
|
||
const spec = (await res.json()) as Record<string, unknown>
|
||
expect(Object.keys(spec.paths as object).sort()).toEqual([
|
||
'/',
|
||
'/leaderboard/CheckAndSetStat',
|
||
'/leaderboard/GetNearbyScores',
|
||
'/leaderboard/GetPlayerRank',
|
||
'/leaderboard/GetRanks',
|
||
])
|
||
expect(JSON.stringify(spec).match(/\$ref/g)).toBeNull()
|
||
})
|