some work on clubs schema

This commit is contained in:
Devin Zuczek
2026-07-09 22:28:09 -04:00
parent 7686bf7bf6
commit 879b5d5905
6 changed files with 584 additions and 12 deletions
+113 -2
View File
@@ -4,6 +4,8 @@ import { beforeAll, describe, expect, test } from 'vitest'
import '../../clubs.app'
import { SCHEMA_DDL } from '../../clubs-db'
import type { Env } from '../../context'
declare module 'cloudflare:test' {
@@ -15,6 +17,8 @@ const ORIGIN = 'https://example.com'
beforeAll(async () => {
// Seed the shared JWT signing key into the local Secrets Store so .get() resolves.
await adminSecretsStore(env.JWT_SECRET).create('test-signing-key')
// Build the club / club_member tables (mirrors the migration).
for (const stmt of SCHEMA_DDL) await env.DB.prepare(stmt).run()
})
// Mint a token the way the `auth` worker does, signing with the shared test key seeded into the JWT_SECRET store.
@@ -86,8 +90,11 @@ describe('clubs endpoints', () => {
expect(await res.json()).toEqual([])
})
test('GET /club/mine/member returns []', async () => {
const res = await exports.default.fetch(`${ORIGIN}/club/mine/member`)
test('GET /club/mine/member is auth-gated and lists the callers clubs', async () => {
expect((await exports.default.fetch(`${ORIGIN}/club/mine/member`)).status).toBe(401)
const res = await exports.default.fetch(`${ORIGIN}/club/mine/member`, {
headers: await bearer('4242'),
})
expect(res.status).toBe(200)
expect(await res.json()).toEqual([])
})
@@ -96,4 +103,108 @@ describe('clubs endpoints', () => {
const res = await exports.default.fetch(`${ORIGIN}/nope`)
expect(res.status).toBe(404)
})
// Full club lifecycle: create → get → other player joins/leaves, with the
// creator auto-membership and MemberCount upkeep, plus the mine/* lists.
test('create → get → join → leave a club, with MemberCount and my-clubs lists', async () => {
type Club = {
ClubId: number
Name: string
Category: string
Visibility: number
AllowJuniors: boolean
MainImageName: string
CreatorAccountId: number
MemberCount: number
}
const post = async (path: string, sub: string | null, fields: Record<string, string> = {}) =>
exports.default.fetch(`${ORIGIN}${path}`, {
method: 'POST',
headers: {
...(sub ? await bearer(sub) : {}),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(fields).toString(),
})
// Create requires auth.
expect((await post('/club', null, { Name: 'Nope' })).status).toBe(401)
// Create requires a name.
expect((await post('/club', '800', { Name: ' ' })).status).toBe(400)
// 800 creates a club → defaults applied, creator auto-joined (MemberCount 1).
const created = (await (
await post('/club', '800', { Name: 'Speedrunners', Category: 'Competitive' })
).json()) as Club
expect(created).toMatchObject({
Name: 'Speedrunners',
Category: 'Competitive',
Visibility: 1, // default
AllowJuniors: true, // default
MainImageName: 'DefaultImgPurple', // default
CreatorAccountId: 800,
MemberCount: 1,
})
const clubId = created.ClubId
// Public get by id returns it; unknown id 404s.
const fetched = (await (await exports.default.fetch(`${ORIGIN}/club/${clubId}`)).json()) as Club
expect(fetched.ClubId).toBe(clubId)
expect((await exports.default.fetch(`${ORIGIN}/club/99999`)).status).toBe(404)
// It shows in the creator's created + member lists.
const created800 = (await (
await exports.default.fetch(`${ORIGIN}/club/mine/created`, { headers: await bearer('800') })
).json()) as Club[]
expect(created800.map((c) => c.ClubId)).toContain(clubId)
// 801 joins → MemberCount 2, and the club appears in 801's member list (not created).
const joined = (await (await post(`/club/${clubId}/join`, '801')).json()) as Club
expect(joined.MemberCount).toBe(2)
const member801 = (await (
await exports.default.fetch(`${ORIGIN}/club/mine/member`, { headers: await bearer('801') })
).json()) as Club[]
expect(member801.map((c) => c.ClubId)).toContain(clubId)
const createdBy801 = (await (
await exports.default.fetch(`${ORIGIN}/club/mine/created`, { headers: await bearer('801') })
).json()) as Club[]
expect(createdBy801).toEqual([])
// Joining again is idempotent (still 2).
expect(((await (await post(`/club/${clubId}/join`, '801')).json()) as Club).MemberCount).toBe(2)
// 801 leaves → back to 1, and it drops out of their member list.
expect(((await (await post(`/club/${clubId}/leave`, '801')).json()) as Club).MemberCount).toBe(1)
const afterLeave = (await (
await exports.default.fetch(`${ORIGIN}/club/mine/member`, { headers: await bearer('801') })
).json()) as Club[]
expect(afterLeave.map((c) => c.ClubId)).not.toContain(clubId)
// Join/leave on a missing club 404s.
expect((await post('/club/99999/join', '801')).status).toBe(404)
})
test('joining a non-open club records a pending request, not a membership', async () => {
const post = async (path: string, sub: string, fields: Record<string, string> = {}) =>
exports.default.fetch(`${ORIGIN}${path}`, {
method: 'POST',
headers: { ...(await bearer(sub)), 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(fields).toString(),
})
type Club = { ClubId: number; Joinability: number; MemberCount: number }
// 810 creates an ask-to-join club (Joinability 2) → only the creator counts.
const club = (await (
await post('/club', '810', { Name: 'Inner Circle', Joinability: '2' })
).json()) as Club
expect(club).toMatchObject({ Joinability: 2, MemberCount: 1 })
// 811 asks to join → pending, so MemberCount is unchanged and it isn't a membership.
const joined = (await (await post(`/club/${club.ClubId}/join`, '811')).json()) as Club
expect(joined.MemberCount).toBe(1)
const member811 = (await (
await exports.default.fetch(`${ORIGIN}/club/mine/member`, { headers: await bearer('811') })
).json()) as Club[]
expect(member811.map((c) => c.ClubId)).not.toContain(club.ClubId)
})
})