mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-09 15:11:29 -07:00
more routes
This commit is contained in:
@@ -39,6 +39,8 @@ export interface Account {
|
|||||||
phone?: string
|
phone?: string
|
||||||
/** Set via PUT /account/me/bio; read back via GET /account/:id/bio. */
|
/** Set via PUT /account/me/bio; read back via GET /account/:id/bio. */
|
||||||
bio?: string
|
bio?: string
|
||||||
|
/** Remaining username changes; decremented by PUT /account/me/username. */
|
||||||
|
availableUsernameChanges?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccountRow {
|
interface AccountRow {
|
||||||
@@ -122,6 +124,19 @@ export async function getAccount(db: D1Database, id: number): Promise<Account |
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Look up a single account by username (case-insensitive), or null if none. */
|
||||||
|
export async function getAccountByUsername(
|
||||||
|
db: D1Database,
|
||||||
|
username: string
|
||||||
|
): Promise<Account | null> {
|
||||||
|
return parseOne(
|
||||||
|
await db
|
||||||
|
.prepare('SELECT data FROM accounts WHERE username_lower = ?1')
|
||||||
|
.bind(username.toLowerCase())
|
||||||
|
.first<AccountRow>()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Look up multiple accounts by AccountId (order not guaranteed). */
|
/** Look up multiple accounts by AccountId (order not guaranteed). */
|
||||||
export async function getAccountsByIds(db: D1Database, ids: number[]): Promise<Account[]> {
|
export async function getAccountsByIds(db: D1Database, ids: number[]): Promise<Account[]> {
|
||||||
if (ids.length === 0) return []
|
if (ids.length === 0) return []
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
createAccount,
|
createAccount,
|
||||||
defaultAccount,
|
defaultAccount,
|
||||||
getAccount,
|
getAccount,
|
||||||
|
getAccountByUsername,
|
||||||
getAccountsByIds,
|
getAccountsByIds,
|
||||||
updateAccount,
|
updateAccount,
|
||||||
} from './accounts-db'
|
} from './accounts-db'
|
||||||
@@ -47,6 +48,18 @@ function unauthorized(c: Context<App>) {
|
|||||||
return c.body(null, 401)
|
return c.body(null, 401)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Username changes a fresh account starts with (until one has been consumed). */
|
||||||
|
const DEFAULT_USERNAME_CHANGES = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Username-change result envelope: `{ success, error, value }`, always HTTP 200.
|
||||||
|
* On success `value` is the updated account; on error `error` carries the message
|
||||||
|
* and `value` is an empty string.
|
||||||
|
*/
|
||||||
|
function usernameResult(c: Context<App>, error = '', value: unknown = '') {
|
||||||
|
return c.json({ success: error === '', error, value })
|
||||||
|
}
|
||||||
|
|
||||||
/** Read a single string field from a form-urlencoded / multipart body. */
|
/** Read a single string field from a form-urlencoded / multipart body. */
|
||||||
async function formField(c: Context<App>, name: string): Promise<string> {
|
async function formField(c: Context<App>, name: string): Promise<string> {
|
||||||
const body = await c.req.parseBody().catch(() => ({}) as Record<string, unknown>)
|
const body = await c.req.parseBody().catch(() => ({}) as Record<string, unknown>)
|
||||||
@@ -103,7 +116,7 @@ const app = new Hono<App>()
|
|||||||
...toAccountDto(account),
|
...toAccountDto(account),
|
||||||
email: account.email ?? null,
|
email: account.email ?? null,
|
||||||
birthday: null,
|
birthday: null,
|
||||||
availableUsernameChanges: 1,
|
availableUsernameChanges: account.availableUsernameChanges ?? DEFAULT_USERNAME_CHANGES,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -164,18 +177,44 @@ const app = new Hono<App>()
|
|||||||
})
|
})
|
||||||
|
|
||||||
// ---- Profile mutations ---------------------------------------------------
|
// ---- Profile mutations ---------------------------------------------------
|
||||||
|
// Set the player's display name (persisted on the account row).
|
||||||
.put('/account/me/displayname', async (c) => {
|
.put('/account/me/displayname', async (c) => {
|
||||||
const id = await authedId(c)
|
const id = await authedId(c)
|
||||||
if (id === null) return unauthorized(c)
|
if (id === null) return unauthorized(c)
|
||||||
await formField(c, 'displayName') // TODO: persist on the account row.
|
const displayName = (await formField(c, 'displayName')).trim()
|
||||||
|
if (displayName === '') return c.body(null, 400)
|
||||||
|
await updateAccount(c.env.DB, id, { displayName })
|
||||||
return c.json({ success: true })
|
return c.json({ success: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Change the caller's username. Rejects a name already taken by another account,
|
||||||
|
// and requires the account to have username changes remaining. On success the
|
||||||
|
// new name is persisted and the remaining-changes counter is decremented.
|
||||||
.put('/account/me/username', async (c) => {
|
.put('/account/me/username', async (c) => {
|
||||||
const id = await authedId(c)
|
const id = await authedId(c)
|
||||||
if (id === null) return unauthorized(c)
|
if (id === null) return unauthorized(c)
|
||||||
await formField(c, 'username') // TODO: persist on the account row.
|
|
||||||
return c.json({ success: true })
|
const username = (await formField(c, 'username')).trim()
|
||||||
|
if (username === '') return usernameResult(c, 'You must enter a username.')
|
||||||
|
|
||||||
|
// Duplicate check first (case-insensitive); keeping your own name is allowed.
|
||||||
|
const existing = await getAccountByUsername(c.env.DB, username)
|
||||||
|
if (existing && existing.accountId !== id) {
|
||||||
|
return usernameResult(c, 'That username is already taken.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then require a remaining change.
|
||||||
|
const account = (await getAccount(c.env.DB, id)) ?? defaultAccount(id)
|
||||||
|
const remaining = account.availableUsernameChanges ?? DEFAULT_USERNAME_CHANGES
|
||||||
|
if (remaining <= 0) {
|
||||||
|
return usernameResult(c, 'You have no username changes remaining.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await updateAccount(c.env.DB, id, {
|
||||||
|
username,
|
||||||
|
availableUsernameChanges: remaining - 1,
|
||||||
|
})
|
||||||
|
return usernameResult(c, '', toAccountDto(updated))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set the player's email (persisted on the account row; surfaced by /account/me).
|
// Set the player's email (persisted on the account row; surfaced by /account/me).
|
||||||
@@ -208,6 +247,16 @@ const app = new Hono<App>()
|
|||||||
return c.json({ success: true })
|
return c.json({ success: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Set the player's personalPronouns (posted as `pronounFlags`; persisted).
|
||||||
|
.put('/account/me/personalpronouns', async (c) => {
|
||||||
|
const id = await authedId(c)
|
||||||
|
if (id === null) return unauthorized(c)
|
||||||
|
const personalPronouns = Number.parseInt((await formField(c, 'pronounFlags')).trim(), 10)
|
||||||
|
if (Number.isNaN(personalPronouns)) return c.body(null, 400)
|
||||||
|
await updateAccount(c.env.DB, id, { personalPronouns })
|
||||||
|
return c.json({ success: true })
|
||||||
|
})
|
||||||
|
|
||||||
.put('/account/me/bio', async (c) => {
|
.put('/account/me/bio', async (c) => {
|
||||||
const id = await authedId(c)
|
const id = await authedId(c)
|
||||||
if (id === null) return unauthorized(c)
|
if (id === null) return unauthorized(c)
|
||||||
|
|||||||
@@ -168,13 +168,79 @@ describe('auth-gated endpoints', () => {
|
|||||||
expect(res.status).toBe(401)
|
expect(res.status).toBe(401)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('PUT /account/me/displayname acks with a valid token', async () => {
|
test('PUT /account/me/displayname persists the display name', async () => {
|
||||||
|
const headers = {
|
||||||
|
...(await bearer('895')),
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
}
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/account/me/displayname`, {
|
const res = await exports.default.fetch(`${ORIGIN}/account/me/displayname`, {
|
||||||
...form({ displayName: 'Bob' }),
|
...form({ displayName: 'laskdjfasdlfkj' }),
|
||||||
headers: { ...(await bearer()), 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers,
|
||||||
})
|
})
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
expect(await res.json()).toEqual({ success: true })
|
expect(await res.json()).toEqual({ success: true })
|
||||||
|
|
||||||
|
const me = await exports.default.fetch(`${ORIGIN}/account/me`, { headers: await bearer('895') })
|
||||||
|
expect(((await me.json()) as { displayName: string }).displayName).toBe('laskdjfasdlfkj')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PUT /account/me/username 401s without a token', async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/account/me/username`, {
|
||||||
|
...form({ username: 'whoever' }),
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(401)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PUT /account/me/username returns a Success:false envelope for a taken name', async () => {
|
||||||
|
// "Coach" is the seeded account 1.
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/account/me/username`, {
|
||||||
|
...form({ username: 'Coach' }),
|
||||||
|
headers: { ...(await bearer('893')), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
// Business errors are HTTP 200 with the { success, error, value } envelope.
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const body = (await res.json()) as { success: boolean; error: string; value: string }
|
||||||
|
expect(body.success).toBe(false)
|
||||||
|
expect(body.error).toMatch(/already taken/i)
|
||||||
|
expect(body.value).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('PUT /account/me/username changes the name, decrements the counter, then blocks', async () => {
|
||||||
|
const headers = {
|
||||||
|
...(await bearer('892')),
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
}
|
||||||
|
// First change succeeds — value is the updated account.
|
||||||
|
const ok = await exports.default.fetch(`${ORIGIN}/account/me/username`, {
|
||||||
|
...form({ username: 'coachx' }),
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
expect(ok.status).toBe(200)
|
||||||
|
const okBody = (await ok.json()) as {
|
||||||
|
success: boolean
|
||||||
|
error: string
|
||||||
|
value: { accountId: number; username: string }
|
||||||
|
}
|
||||||
|
expect(okBody.success).toBe(true)
|
||||||
|
expect(okBody.error).toBe('')
|
||||||
|
expect(okBody.value).toMatchObject({ accountId: 892, username: 'coachx' })
|
||||||
|
|
||||||
|
// /account/me reflects the new name and the decremented counter.
|
||||||
|
const me = (await (
|
||||||
|
await exports.default.fetch(`${ORIGIN}/account/me`, { headers: await bearer('892') })
|
||||||
|
).json()) as { username: string; availableUsernameChanges: number }
|
||||||
|
expect(me.username).toBe('coachx')
|
||||||
|
expect(me.availableUsernameChanges).toBe(0)
|
||||||
|
|
||||||
|
// A second change is blocked — no changes remaining (still HTTP 200).
|
||||||
|
const blocked = await exports.default.fetch(`${ORIGIN}/account/me/username`, {
|
||||||
|
...form({ username: 'coachy' }),
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
expect(blocked.status).toBe(200)
|
||||||
|
const blockedBody = (await blocked.json()) as { success: boolean; error: string }
|
||||||
|
expect(blockedBody.success).toBe(false)
|
||||||
|
expect(blockedBody.error).toMatch(/no username changes/i)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('PUT /account/me/profileimage 401s without a token', async () => {
|
test('PUT /account/me/profileimage 401s without a token', async () => {
|
||||||
@@ -264,6 +330,18 @@ describe('auth-gated endpoints', () => {
|
|||||||
expect(await res.json()).toEqual({ success: true })
|
expect(await res.json()).toEqual({ success: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('PUT /account/me/personalpronouns persists the value, surfaced by /account/me', async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/account/me/personalpronouns`, {
|
||||||
|
...form({ pronounFlags: '2' }),
|
||||||
|
headers: { ...(await bearer('894')), 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual({ success: true })
|
||||||
|
|
||||||
|
const me = await exports.default.fetch(`${ORIGIN}/account/me`, { headers: await bearer('894') })
|
||||||
|
expect(((await me.json()) as { personalPronouns: number }).personalPronouns).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
test('PUT /account/me/bio 401s without a token', async () => {
|
test('PUT /account/me/bio 401s without a token', async () => {
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/account/me/bio`, { ...form({ bio: 'x' }) })
|
const res = await exports.default.fetch(`${ORIGIN}/account/me/bio`, { ...form({ bio: 'x' }) })
|
||||||
expect(res.status).toBe(401)
|
expect(res.status).toBe(401)
|
||||||
|
|||||||
@@ -284,6 +284,12 @@ const app = new Hono<App>({ strict: false })
|
|||||||
.get('/api/customAvatarItems/v1/isCreationEnabled', (c) => c.json(true))
|
.get('/api/customAvatarItems/v1/isCreationEnabled', (c) => c.json(true))
|
||||||
.get('/api/customAvatarItems/v1/isRenderingEnabled', (c) => c.json(true))
|
.get('/api/customAvatarItems/v1/isRenderingEnabled', (c) => c.json(true))
|
||||||
|
|
||||||
|
// Custom avatar items created by a given account. No storage yet → an empty
|
||||||
|
// paginated result (matches the econ `customAvatarItems/v1/owned` shape).
|
||||||
|
.get('/api/customAvatarItems/v2/fromCreator/:accountId{[0-9]+}', (c) =>
|
||||||
|
c.json({ Results: [], TotalResults: 0 })
|
||||||
|
)
|
||||||
|
|
||||||
// Voice chat config. The client fetches it to set up voice.
|
// Voice chat config. The client fetches it to set up voice.
|
||||||
// No reference shape, so return an empty object until the client needs fields.
|
// No reference shape, so return an empty object until the client needs fields.
|
||||||
.get('/voice/config', (c) => c.json({}))
|
.get('/voice/config', (c) => c.json({}))
|
||||||
|
|||||||
@@ -201,6 +201,12 @@ describe('public endpoints', () => {
|
|||||||
expect(await res.json()).toBe(true)
|
expect(await res.json()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('GET /api/customAvatarItems/v2/fromCreator/:id returns an empty paginated result', async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/customAvatarItems/v2/fromCreator/2`)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual({ Results: [], TotalResults: 0 })
|
||||||
|
})
|
||||||
|
|
||||||
test('GET /api/rooms/v1/filters returns an object with filter arrays', async () => {
|
test('GET /api/rooms/v1/filters returns an object with filter arrays', async () => {
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/api/rooms/v1/filters`)
|
const res = await exports.default.fetch(`${ORIGIN}/api/rooms/v1/filters`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
|
|||||||
@@ -57,10 +57,22 @@ const app = new Hono<App>()
|
|||||||
// array = no club subscription memberships (the client chokes on null).
|
// array = no club subscription memberships (the client chokes on null).
|
||||||
.get('/subscription/mine/member', (c) => c.json([]))
|
.get('/subscription/mine/member', (c) => c.json([]))
|
||||||
|
|
||||||
// Details for a given subscription. The client deserializes this into an
|
// Subscription details for an account (numeric id) — simulated: no club, no subs.
|
||||||
// object, so it must return `{}` (not `[]`).
|
.get('/subscription/details/:accountId{[0-9]+}', (c) =>
|
||||||
|
c.json({
|
||||||
|
accountId: Number.parseInt(c.req.param('accountId'), 10),
|
||||||
|
clubId: 0,
|
||||||
|
subscriberCount: 0,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Details for a named subscription (e.g. `rrplus`). The client deserializes this
|
||||||
|
// into an object, so it must return `{}` (not `[]`).
|
||||||
.get('/subscription/details/:subscription', (c) => c.json({}))
|
.get('/subscription/details/:subscription', (c) => c.json({}))
|
||||||
|
|
||||||
|
// Subscriber count for an account. No club subscriptions yet → 0.
|
||||||
|
.get('/subscription/subscriberCount/:accountId{[0-9]+}', (c) => c.json(0))
|
||||||
|
|
||||||
// The player's clubs that have unread announcements (MyClubsWithUnread-
|
// The player's clubs that have unread announcements (MyClubsWithUnread-
|
||||||
// Announcements). No DB → empty list.
|
// Announcements). No DB → empty list.
|
||||||
.get('/announcements/v2/mine/unread', (c) => c.json([]))
|
.get('/announcements/v2/mine/unread', (c) => c.json([]))
|
||||||
|
|||||||
@@ -56,6 +56,18 @@ describe('clubs endpoints', () => {
|
|||||||
expect(await res.json()).toEqual({})
|
expect(await res.json()).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('GET /subscription/details/:accountId returns simulated details', async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/subscription/details/2`)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual({ accountId: 2, clubId: 0, subscriberCount: 0 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('GET /subscription/subscriberCount/:id returns 0', async () => {
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/subscription/subscriberCount/2`)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
test('GET /announcements/v2/mine/unread returns []', async () => {
|
test('GET /announcements/v2/mine/unread returns []', async () => {
|
||||||
const res = await exports.default.fetch(`${ORIGIN}/announcements/v2/mine/unread`)
|
const res = await exports.default.fetch(`${ORIGIN}/announcements/v2/mine/unread`)
|
||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
|
|||||||
@@ -130,6 +130,13 @@ const app = new Hono<App>()
|
|||||||
return c.json([])
|
return c.json([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The player's item wishlist. [Authorize]; empty without a DB binding.
|
||||||
|
.get('/api/itemWishlists/v1/wishlist/me', async (c) => {
|
||||||
|
const id = await authedId(c)
|
||||||
|
if (id === null) return unauthorized(c)
|
||||||
|
return c.json([])
|
||||||
|
})
|
||||||
|
|
||||||
// The player's saved outfits. [Authorize]; empty without a DB binding.
|
// The player's saved outfits. [Authorize]; empty without a DB binding.
|
||||||
.get('/api/avatar/v3/saved', async (c) => {
|
.get('/api/avatar/v3/saved', async (c) => {
|
||||||
const id = await authedId(c)
|
const id = await authedId(c)
|
||||||
|
|||||||
@@ -174,6 +174,16 @@ describe('econ endpoints', () => {
|
|||||||
expect(await res.json()).toEqual([])
|
expect(await res.json()).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('GET /api/itemWishlists/v1/wishlist/me 401s without a token, returns [] with one', async () => {
|
||||||
|
const anon = await exports.default.fetch(`${ORIGIN}/api/itemWishlists/v1/wishlist/me`)
|
||||||
|
expect(anon.status).toBe(401)
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/api/itemWishlists/v1/wishlist/me`, {
|
||||||
|
headers: await bearer(),
|
||||||
|
})
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
test('GET /api/avatar/v3/saved 401s without a token, returns [] with one', async () => {
|
test('GET /api/avatar/v3/saved 401s without a token, returns [] with one', async () => {
|
||||||
const anon = await exports.default.fetch(`${ORIGIN}/api/avatar/v3/saved`)
|
const anon = await exports.default.fetch(`${ORIGIN}/api/avatar/v3/saved`)
|
||||||
expect(anon.status).toBe(401)
|
expect(anon.status).toBe(401)
|
||||||
|
|||||||
@@ -142,6 +142,16 @@ export async function getRoomsByCreator(db: D1Database, accountId: number): Prom
|
|||||||
return parseAll(results)
|
return parseAll(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An account's public, non-dorm rooms — the publicly viewable "rooms owned by
|
||||||
|
* <player>" list (excludes private rooms, dorms, and list-excluded rooms).
|
||||||
|
*/
|
||||||
|
export async function getPublicRoomsByCreator(db: D1Database, accountId: number): Promise<Room[]> {
|
||||||
|
return (await getRoomsByCreator(db, accountId)).filter(
|
||||||
|
(r) => r.IsDorm !== true && r.Accessibility === 1 && r.ExcludeFromLists !== true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rooms the player has favorited (interaction.favorited = 1), most recently
|
* Rooms the player has favorited (interaction.favorited = 1), most recently
|
||||||
* interacted first. Joins the `interaction` table to `rooms`, so a favorited room
|
* interacted first. Joins the `interaction` table to `rooms`, so a favorited room
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getFavoritedRooms,
|
getFavoritedRooms,
|
||||||
getHotRooms,
|
getHotRooms,
|
||||||
getInteraction,
|
getInteraction,
|
||||||
|
getPublicRoomsByCreator,
|
||||||
getRoomById,
|
getRoomById,
|
||||||
getRoomByName,
|
getRoomByName,
|
||||||
getRoomsByCreator,
|
getRoomsByCreator,
|
||||||
@@ -206,6 +207,12 @@ const app = new Hono<App>()
|
|||||||
.get('/rooms/ownedby/me', ownedRooms)
|
.get('/rooms/ownedby/me', ownedRooms)
|
||||||
.get('/rooms/createdby/me', ownedRooms)
|
.get('/rooms/createdby/me', ownedRooms)
|
||||||
|
|
||||||
|
// Public: the rooms a given account owns that are publicly viewable. No auth —
|
||||||
|
// returns a bare array (empty when the account owns no public rooms).
|
||||||
|
.get('/rooms/ownedby/:accountId{[0-9]+}', async (c) =>
|
||||||
|
c.json(await getPublicRoomsByCreator(c.env.DB, Number.parseInt(c.req.param('accountId'), 10)))
|
||||||
|
)
|
||||||
|
|
||||||
// Rooms the caller has favorited (from the interaction table). Auth-gated.
|
// Rooms the caller has favorited (from the interaction table). Auth-gated.
|
||||||
// Paginated via skip/take (take defaults to 100). Returns a bare array, like the
|
// Paginated via skip/take (take defaults to 100). Returns a bare array, like the
|
||||||
// other room-source `*by/me` lists the client loads.
|
// other room-source `*by/me` lists the client loads.
|
||||||
|
|||||||
@@ -115,6 +115,27 @@ describe('rooms endpoints', () => {
|
|||||||
expect(other).toEqual([])
|
expect(other).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('GET /rooms/ownedby/:id returns an account public rooms (no auth)', async () => {
|
||||||
|
const res = await SELF.fetch(`${ORIGIN}/rooms/ownedby/1`)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const body = (await res.json()) as Array<{
|
||||||
|
RoomId: number
|
||||||
|
Accessibility: number
|
||||||
|
IsDorm?: boolean
|
||||||
|
CreatorAccountId: number
|
||||||
|
}>
|
||||||
|
expect(body.length).toBeGreaterThan(0)
|
||||||
|
// Only public, non-dorm rooms owned by account 1 — the private dorm (RoomId 1)
|
||||||
|
// is excluded.
|
||||||
|
expect(
|
||||||
|
body.every((r) => r.Accessibility === 1 && r.IsDorm !== true && r.CreatorAccountId === 1)
|
||||||
|
).toBe(true)
|
||||||
|
expect(body.some((r) => r.RoomId === 1)).toBe(false)
|
||||||
|
|
||||||
|
// An account that owns no public rooms → empty array.
|
||||||
|
expect(await (await SELF.fetch(`${ORIGIN}/rooms/ownedby/999`)).json()).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
it('GET /rooms/search returns a paginated { Results, TotalResults }', async () => {
|
it('GET /rooms/search returns a paginated { Results, TotalResults }', async () => {
|
||||||
// Name-term search resolves a known public room.
|
// Name-term search resolves a known public room.
|
||||||
const res = await SELF.fetch(`${ORIGIN}/rooms/search?query=reccenter&skip=0&take=100`)
|
const res = await SELF.fetch(`${ORIGIN}/rooms/search?query=reccenter&skip=0&take=100`)
|
||||||
|
|||||||
Reference in New Issue
Block a user