Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions open-api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5181,7 +5181,8 @@
"schema": {
"type": "integer",
"nullable": true,
"minimum": 0
"minimum": 0,
"maximum": 2147483647
},
"required": false,
"name": "minDurationSeconds",
Expand All @@ -5191,7 +5192,8 @@
"schema": {
"type": "integer",
"nullable": true,
"minimum": 0
"minimum": 0,
"maximum": 2147483647
},
"required": false,
"name": "maxDurationSeconds",
Expand Down
6 changes: 3 additions & 3 deletions src/routes/player/membershipId/instances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
zBigIntString,
zBoolString,
zCoercedNaturalNumber,
zCoercedWholeNumber,
zDateString,
zPgInt32,
zSplitCommaSeparatedString
} from "@/schema/input"
import { zInt64 } from "@/schema/output"
Expand Down Expand Up @@ -37,8 +37,8 @@ export const playerInstancesRoute = new RaidHubRoute({
playerCount: zCoercedNaturalNumber().optional(),
minPlayerCount: zCoercedNaturalNumber().optional(),
maxPlayerCount: zCoercedNaturalNumber().optional(),
minDurationSeconds: zCoercedWholeNumber().optional(),
maxDurationSeconds: zCoercedWholeNumber().optional(),
minDurationSeconds: zPgInt32().optional(),
maxDurationSeconds: zPgInt32().optional(),
season: zCoercedNaturalNumber().optional(),
minSeason: zCoercedNaturalNumber().optional(),
maxSeason: zCoercedNaturalNumber().optional(),
Expand Down
82 changes: 49 additions & 33 deletions src/schema/input.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import "@/schema/registry" // Initialize OpenAPI extensions
import { describe, expect, test } from "bun:test"
import { z } from "zod"
import { zBigIntString, zBoolString, zSplitCommaSeparatedString } from "./input"
import {
PG_BIGINT_MAX,
zBigIntString,
zBoolString,
zPgInt32,
zSplitCommaSeparatedString
} from "./input"

describe("zBoolString", () => {
test("should parse truthy values correctly", () => {
Expand Down Expand Up @@ -41,6 +47,20 @@ describe("zBoolString", () => {
})
})

describe("zPgInt32", () => {
test("accepts values within PostgreSQL INTEGER range", () => {
const schema = zPgInt32()
expect(schema.safeParse(0).success).toBe(true)
expect(schema.safeParse(2_147_483_647).success).toBe(true)
})

test("rejects values above PostgreSQL INTEGER max", () => {
const schema = zPgInt32()
expect(schema.safeParse(10_000_000_000).success).toBe(false)
expect(schema.safeParse(2_147_483_648).success).toBe(false)
})
})

describe("zSplitCommaSeparatedString", () => {
test("should fail on non-string non-array input", () => {
const schema = zSplitCommaSeparatedString(z.any())
Expand Down Expand Up @@ -97,8 +117,8 @@ describe("zSplitCommaSeparatedString", () => {
const singleValueInput = [
{ input: "123", expected: [123n] },
{
input: "1234567890123456789012345678901234567890123456789012345678901234567890",
expected: [1234567890123456789012345678901234567890123456789012345678901234567890n]
input: String(PG_BIGINT_MAX),
expected: [PG_BIGINT_MAX]
}
]

Expand All @@ -111,24 +131,21 @@ describe("zSplitCommaSeparatedString", () => {
})
})

test("should reject bigint values above PostgreSQL BIGINT max", () => {
const schema = zBigIntString()
const tooLarge = String(PG_BIGINT_MAX + 1n)

expect(schema.safeParse(tooLarge).success).toBe(false)
expect(schema.safeParse("46116860184306851060").success).toBe(false)
})

test("should pass on a valid multi-value input", () => {
const schema = zSplitCommaSeparatedString(zBigIntString())
const singleValueInput = [
{ input: "123,123,123", expected: [123n, 123n, 123n] },
{
input: "123,1234567890123456789012345678901234567890123456789012345678901234567890",
expected: [
123n,
1234567890123456789012345678901234567890123456789012345678901234567890n
]
},
{
input: "11234567890123456789012345678901234567890123456789012345678901234567890,21234567890123456789012345678901234567890123456789012345678901234567890,31234567890123456789012345678901234567890123456789012345678901234567890",
expected: [
1_1234567890123456789012345678901234567890123456789012345678901234567890n,
2_1234567890123456789012345678901234567890123456789012345678901234567890n,
3_1234567890123456789012345678901234567890123456789012345678901234567890n
]
input: `123,${PG_BIGINT_MAX}`,
expected: [123n, PG_BIGINT_MAX]
}
]

Expand All @@ -148,23 +165,8 @@ describe("zSplitCommaSeparatedString", () => {
{ input: "123 ", expected: [123n] },
{ input: " 123 ", expected: [123n] },
{
input: " 1234567890123456789012345678901234567890123456789012345678901234567890",
expected: [1234567890123456789012345678901234567890123456789012345678901234567890n]
},
{
input: "1234567890123456789012345678901234567890123456789012345678901234567890 ",
expected: [1234567890123456789012345678901234567890123456789012345678901234567890n]
},
{
input: " 1234567890123456789012345678901234567890123456789012345678901234567890 ",
expected: [1234567890123456789012345678901234567890123456789012345678901234567890n]
},
{
input: " 123 , 1234567890123456789012345678901234567890123456789012345678901234567890 ",
expected: [
123n,
1234567890123456789012345678901234567890123456789012345678901234567890n
]
input: String(PG_BIGINT_MAX),
expected: [PG_BIGINT_MAX]
}
]

Expand Down Expand Up @@ -192,3 +194,17 @@ describe("zSplitCommaSeparatedString", () => {
}
})
})

describe("zPgInt32", () => {
test("accepts values within PostgreSQL INTEGER range", () => {
const schema = zPgInt32()
expect(schema.safeParse(0).success).toBe(true)
expect(schema.safeParse(2_147_483_647).success).toBe(true)
})

test("rejects values above PostgreSQL INTEGER max", () => {
const schema = zPgInt32()
expect(schema.safeParse(10_000_000_000).success).toBe(false)
expect(schema.safeParse(2_147_483_648).success).toBe(false)
})
})
21 changes: 19 additions & 2 deletions src/schema/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@ import { ZodBooleanDef, ZodDateDef, ZodNullable, ZodStringDef, ZodType, ZodTypeA

// ===== INPUT SCHEMAS (for parsing/coercing user input) =====

/** PostgreSQL signed BIGINT upper bound (membership_id, instance_id, etc.). */
export const PG_BIGINT_MAX = 9223372036854775807n

/** PostgreSQL INTEGER upper bound (duration, season_id, etc.). */
export const PG_INT32_MAX = 2_147_483_647

export const zCoercedNaturalNumber = () => z.coerce.number().int().positive()

export const zCoercedWholeNumber = () => z.coerce.number().int().nonnegative()

/** Whole number that fits in a PostgreSQL INTEGER column. */
export const zPgInt32 = () =>
zCoercedWholeNumber().max(PG_INT32_MAX, {
message: `Must be at most ${PG_INT32_MAX}`
})

export const zPage = () => z.coerce.number().int().positive().default(1)

export const zBoolString = () =>
Expand Down Expand Up @@ -41,8 +53,13 @@ export const zDateString = <N extends boolean = false>({
export const zDigitString = () =>
z.coerce.string().regex(/^\d+n?$/) as ZodType<string, ZodStringDef, number | string | bigint>

// Input param that will be coerced to a BigInt
export const zBigIntString = () => zDigitString().transform(val => BigInt(val))
// Input param that will be coerced to a BigInt within PostgreSQL BIGINT range
export const zBigIntString = () =>
zDigitString()
.transform(val => BigInt(val))
.refine(val => val <= PG_BIGINT_MAX, {
message: `Must be at most ${PG_BIGINT_MAX}`
})

export const zSplitCommaSeparatedString = <T extends ZodTypeAny, ResultingArray extends ZodTypeAny>(
itemSchema: T,
Expand Down
2 changes: 1 addition & 1 deletion src/services/reporting/update-blacklist.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,59 @@
import { pgAdmin } from "@/integrations/postgres"

export const blacklistInstance = async (data: {
instanceId: bigint | string
reportId: number | null
reason: string
players: {
membershipId: string | bigint
reason: string
}[]
}) => {
return await pgAdmin.transaction(async conn => {
// Insert into blacklist_instance
const reportSource = data.reportId ? "WebReport" : "Manual"
await conn.queryRow(
`INSERT INTO flagging.blacklist_instance (instance_id, report_source, report_id, reason)
VALUES ($1::bigint, $2, $3, $4)
VALUES ($1::bigint, $2::flagging."BlacklistReportSource", $3, $4)
ON CONFLICT (instance_id) DO NOTHING`,
{ params: [data.instanceId, reportSource, data.reportId, data.reason] }
)

// Insert into blacklist_instance_flag
const playerStmnt = await conn.prepare(
`INSERT INTO flagging.blacklist_instance_player (instance_id, membership_id, reason)
VALUES ($1::bigint, $2::bigint, $3)
ON CONFLICT (instance_id, membership_id) DO NOTHING`
)

try {
await Promise.all(
data.players.map(player =>
playerStmnt.execute({
params: [data.instanceId, player.membershipId, player.reason]
})
)
)
} finally {
await playerStmnt.close()
}

Check warning on line 39 in src/services/reporting/update-blacklist.ts

View workflow job for this annotation

GitHub Actions / test

3-39 lines are not covered with tests
})
}

export const removeInstanceBlacklist = async (instanceId: bigint | string) => {
return await pgAdmin.transaction(async conn => {
// Delete from blacklist_instance
await conn.queryRow(
`DELETE FROM flagging.blacklist_instance WHERE instance_id = $1::bigint`,
{
params: [instanceId]
}
)

// Set instance.is_whitelisted to true
await conn.queryRow(
`UPDATE instance SET is_whitelisted = true WHERE instance_id = $1::bigint`,
{ params: [instanceId] }
)

Check warning on line 57 in src/services/reporting/update-blacklist.ts

View workflow job for this annotation

GitHub Actions / test

43-57 lines are not covered with tests
})
}
Loading