Invitations: migrate send to typed path param; update settings view and tests; spec reflects new OpenAPI and migration plan

This commit is contained in:
jableader 2025-11-01 16:25:15 +11:00
parent 3b421106bc
commit 630c6dabcf
3 changed files with 16 additions and 18 deletions

View file

@ -1,4 +1,4 @@
import { fetchApi } from '@/api/client' import { api, fetchApi } from '@/api/client'
export type Household = { id: number; name: string; slug: string } export type Household = { id: number; name: string; slug: string }
@ -40,11 +40,10 @@ export async function acceptInvitation(token: string): Promise<Household> {
return { id: h.id, name: h.name, slug: h.slug } return { id: h.id, name: h.name, slug: h.slug }
} }
export async function sendInvitation(email: string): Promise<void> { export async function sendInvitation(householdSlug: string, email: string): Promise<void> {
const resp = await fetchApi('/api/v1/invitations', { const res = await api.POST('/api/v1/households/{householdSlug}/invitations', {
method: 'POST', params: { path: { householdSlug } },
headers: { 'Content-Type': 'application/json' }, body: { email },
body: JSON.stringify({ email }),
}) })
if (!resp.ok && resp.status !== 204) throw new Error(`${resp.status} ${resp.statusText || 'HTTP error'}`) if (!res.response.ok) throw new Error(`${res.response.status} ${res.response.statusText || 'HTTP error'}`)
} }

View file

@ -54,15 +54,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import { sendInvitation } from '@/api/invitations' import { sendInvitation } from '@/api/invitations'
import { listMembers, type Member } from '@/api/households' import { listMembers, type Member } from '@/api/households'
import { useRoute } from 'vue-router'
const email = ref('') const email = ref('')
const submitting = ref(false) const submitting = ref(false)
const message = ref('') const message = ref('')
const error = ref('') const error = ref('')
const members = ref<Member[]>([]) const members = ref<Member[]>([])
const route = useRoute()
const householdSlug = computed(() => (typeof route.params.householdSlug === 'string' ? route.params.householdSlug : null))
onMounted(async () => { onMounted(async () => {
try { try {
@ -77,7 +80,9 @@ async function onInvite() {
error.value = '' error.value = ''
submitting.value = true submitting.value = true
try { try {
await sendInvitation(email.value.trim()) const slug = householdSlug.value
if (!slug) throw new Error('No household selected')
await sendInvitation(slug, email.value.trim())
message.value = 'Invitation sent.' message.value = 'Invitation sent.'
email.value = '' email.value = ''
} catch (e) { } catch (e) {

View file

@ -2,10 +2,9 @@ import { describe, it, expect } from 'vitest'
import { server, http, HttpResponse } from './test-setup' import { server, http, HttpResponse } from './test-setup'
import { loginWithPassword } from '@/api/auth' import { loginWithPassword } from '@/api/auth'
import { sendInvitation } from '@/api/invitations' import { sendInvitation } from '@/api/invitations'
import { setHouseholdSlugProvider } from '@/api/client'
describe('invitations api (send invite)', () => { describe('invitations api (send invite)', () => {
it('posts email with Authorization and X-Household-Slug', async () => { it('posts email with Authorization and householdSlug path param', async () => {
// Simulate auth token // Simulate auth token
server.use( server.use(
http.post('*/api/v1/auth/login', () => http.post('*/api/v1/auth/login', () =>
@ -14,21 +13,16 @@ describe('invitations api (send invite)', () => {
) )
await loginWithPassword('x@y', 'pw') await loginWithPassword('x@y', 'pw')
// Provide a household slug for header injection
setHouseholdSlugProvider(() => 'the-smiths')
server.use( server.use(
http.post('*/api/v1/invitations', async ({ request }) => { http.post('*/api/v1/households/the-smiths/invitations', async ({ request, requestId, cookies, params }) => {
const body = await request.json() const body = await request.json()
expect(body).toEqual({ email: 'invite@example.com' }) expect(body).toEqual({ email: 'invite@example.com' })
const auth = request.headers.get('authorization') const auth = request.headers.get('authorization')
expect(auth?.toLowerCase()).toBe('bearer tokabc') expect(auth?.toLowerCase()).toBe('bearer tokabc')
const slug = request.headers.get('x-household-slug')
expect(slug).toBe('the-smiths')
return HttpResponse.json({}, { status: 204 }) return HttpResponse.json({}, { status: 204 })
}) })
) )
await expect(sendInvitation('invite@example.com')).resolves.toBeUndefined() await expect(sendInvitation('the-smiths', 'invite@example.com')).resolves.toBeUndefined()
}) })
}) })