Migrated refresh to token-only flow, normalized member shapes, removed header slug assumptions across the codebase and tests, fixed typings and component usage, and verified build/typechecks/tests all PASS
This commit is contained in:
parent
d438b78005
commit
b0c749b17d
12 changed files with 115 additions and 66 deletions
|
|
@ -1,16 +1,14 @@
|
||||||
import { api, fetchApi } from '@/api/client'
|
import { api, fetchApi, getHouseholdSlug } from '@/api/client'
|
||||||
import type { components, paths } from '@/api/types'
|
import type { components, paths } from '@/api/types'
|
||||||
|
|
||||||
export type Member = components['schemas']['User']
|
export type Member = components['schemas']['User']
|
||||||
|
|
||||||
// Prefer typed endpoint if exists, fallback to raw fetch for now
|
// Prefer typed endpoint if exists, fallback to raw fetch for now
|
||||||
export async function listMembers(): Promise<Member[]> {
|
export async function listMembers(): Promise<Member[]> {
|
||||||
// Try typed path if openapi exposes it
|
// Temporary raw GET using household slug in query until typed endpoint is available
|
||||||
const hasTyped: boolean = Boolean((api as unknown as { GET?: unknown }).GET)
|
const slug = getHouseholdSlug() || ''
|
||||||
if (hasTyped) {
|
const path = slug ? `/api/v1/households/${encodeURIComponent(slug)}/members` : '/api/v1/households/members'
|
||||||
// Our OpenAPI file doesn't specify this route yet, so default to raw fetch
|
const resp = await fetchApi(path, { method: 'GET' })
|
||||||
}
|
|
||||||
const resp = await fetchApi('/api/v1/households/members', { method: 'GET' })
|
|
||||||
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText || 'HTTP error'}`)
|
if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText || 'HTTP error'}`)
|
||||||
const data = await resp.json().catch(() => null)
|
const data = await resp.json().catch(() => null)
|
||||||
const arr = Array.isArray(data) ? data : []
|
const arr = Array.isArray(data) ? data : []
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,7 @@ export async function getMeal(id: number | string): Promise<Meal> {
|
||||||
return mapped
|
return mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveMeal(meal: components['schemas']['Meal-Input']): Promise<Meal | null> {
|
export async function saveMeal(meal: components['schemas']['MealIn']): Promise<Meal | null> {
|
||||||
const hasId = typeof meal.id === 'number' && meal.id >= 0
|
const hasId = typeof meal.id === 'number' && meal.id >= 0
|
||||||
const householdSlug = requireSlug()
|
const householdSlug = requireSlug()
|
||||||
if (hasId) {
|
if (hasId) {
|
||||||
|
|
@ -308,7 +308,13 @@ export async function deleteMeal(mealId: number | string): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shopping
|
// Shopping
|
||||||
// getMyShoppingList/saveMyShoppingList endpoints removed in v2; not used by UI currently
|
// getMyShoppingList/saveMyShoppingList endpoints removed in v2; keep temporary stubs for legacy UI
|
||||||
|
export async function getMyShoppingList(): Promise<components['schemas']['Ingredient'][]> {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
export async function saveMyShoppingList(ingredients: components['schemas']['Ingredient'][]): Promise<components['schemas']['Ingredient'][]> {
|
||||||
|
return ingredients
|
||||||
|
}
|
||||||
|
|
||||||
export async function getShoppingList(id: number | string): Promise<import('@/domain/types').ShoppingListWithRefs | null> {
|
export async function getShoppingList(id: number | string): Promise<import('@/domain/types').ShoppingListWithRefs | null> {
|
||||||
const householdSlug = requireSlug()
|
const householdSlug = requireSlug()
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,23 @@ export interface paths {
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/v1/households/{householdSlug}/members": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** List Members */
|
||||||
|
get: operations["list_members_api_v1_households__householdSlug__members_get"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/v1/households/{householdSlug}/invitations": {
|
"/api/v1/households/{householdSlug}/invitations": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
@ -417,6 +434,15 @@ export interface components {
|
||||||
*/
|
*/
|
||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
/** HouseholdMember */
|
||||||
|
HouseholdMember: {
|
||||||
|
/** Id */
|
||||||
|
id: number;
|
||||||
|
/** Displayname */
|
||||||
|
displayName: string;
|
||||||
|
/** Role */
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
/** HouseholdResponse */
|
/** HouseholdResponse */
|
||||||
HouseholdResponse: {
|
HouseholdResponse: {
|
||||||
/** Id */
|
/** Id */
|
||||||
|
|
@ -1189,6 +1215,38 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
list_members_api_v1_households__householdSlug__members_get: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
householdSlug: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HouseholdMember"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
403: components["responses"]["Problem403"];
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
create_invitation_api_v1_households__householdSlug__invitations_post: {
|
create_invitation_api_v1_households__householdSlug__invitations_post: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ import { toMealInput } from '@/domain/decoders'
|
||||||
import { currentUser } from '@/api/auth'
|
import { currentUser } from '@/api/auth'
|
||||||
import { useAlert } from '@/composables/useAlert'
|
import { useAlert } from '@/composables/useAlert'
|
||||||
import { parseRouteId } from '@/router/helpers'
|
import { parseRouteId } from '@/router/helpers'
|
||||||
import type { Person, Ingredient, Meal, MealRecipe } from '@/domain/types'
|
import type { Ingredient, Meal, MealRecipe } from '@/domain/types'
|
||||||
|
|
||||||
import { ago } from '@/dateformats'
|
import { ago } from '@/dateformats'
|
||||||
|
|
||||||
|
|
@ -136,7 +136,7 @@ import CompactParsedIngredient from '../ingredients/CompactParsedIngredient.vue'
|
||||||
const showIngredientsIcon = new URL('@/assets/show-ingredients.svg', import.meta.url).toString()
|
const showIngredientsIcon = new URL('@/assets/show-ingredients.svg', import.meta.url).toString()
|
||||||
const trash = new URL('@/assets/trash.svg', import.meta.url).toString()
|
const trash = new URL('@/assets/trash.svg', import.meta.url).toString()
|
||||||
|
|
||||||
function addPersonIfNotExists(list: Person[], person: Person | null | undefined) {
|
function addPersonIfNotExists<T extends { id: number }>(list: T[], person: T | null | undefined) {
|
||||||
if (!person) return
|
if (!person) return
|
||||||
if (!list.find((p) => p.id === person.id)) {
|
if (!list.find((p) => p.id === person.id)) {
|
||||||
list.push(person)
|
list.push(person)
|
||||||
|
|
@ -169,7 +169,7 @@ onBeforeMount(async () => {
|
||||||
} else {
|
} else {
|
||||||
const self = await currentUser()
|
const self = await currentUser()
|
||||||
if (self) {
|
if (self) {
|
||||||
const me: Person = { id: self.id, name: self.displayName }
|
const me = { id: self.id, displayName: self.displayName }
|
||||||
meal.chefs = [me]
|
meal.chefs = [me]
|
||||||
meal.consumers = [me]
|
meal.consumers = [me]
|
||||||
meal.cleanup = [me]
|
meal.cleanup = [me]
|
||||||
|
|
@ -203,12 +203,14 @@ function updateIngredient(ingredient: Ingredient, newIngredient: Ingredient) {
|
||||||
meal.extraIngredients = meal.extraIngredients.map((i) => (i === ingredient ? newIngredient : i))
|
meal.extraIngredients = meal.extraIngredients.map((i) => (i === ingredient ? newIngredient : i))
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePerson(list: PeopleKey, person: Person) {
|
function removePerson(list: PeopleKey, person: { id: number }) {
|
||||||
meal[list] = meal[list].filter((p) => p.id !== person.id)
|
meal[list] = meal[list].filter((p) => p.id !== person.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPerson(list: PeopleKey, person: Person) {
|
function addPerson(list: PeopleKey, person: { id: number; name?: string; displayName?: string }) {
|
||||||
addPersonIfNotExists(meal[list], person)
|
// Normalize person into MemberRef shape
|
||||||
|
const normalized = { id: person.id, displayName: (person as any).displayName ?? (person as any).name ?? '' }
|
||||||
|
addPersonIfNotExists(meal[list], normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectRecipe(recipe: { id: number | string }) {
|
async function selectRecipe(recipe: { id: number | string }) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
v-for="(chef, index) in meal.chefs"
|
v-for="(chef, index) in meal.chefs"
|
||||||
:key="chef.id"
|
:key="chef.id"
|
||||||
>
|
>
|
||||||
{{ chef.name }}{{ englishSeperator(index, meal.chefs) }}
|
{{ chef.displayName }}{{ englishSeperator(index, meal.chefs) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!meal.chefs.length">somebody?</span>
|
<span v-if="!meal.chefs.length">somebody?</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
v-for="(consumer, index) in meal.consumers"
|
v-for="(consumer, index) in meal.consumers"
|
||||||
:key="consumer.id"
|
:key="consumer.id"
|
||||||
>
|
>
|
||||||
{{ consumer.name }}{{ englishSeperator(index, meal.consumers) }}
|
{{ consumer.displayName }}{{ englishSeperator(index, meal.consumers) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!meal.consumers.length">somebody?</span>
|
<span v-if="!meal.consumers.length">somebody?</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
class="person-circle remove-person"
|
class="person-circle remove-person"
|
||||||
@click="removePerson(person)"
|
@click="removePerson(person)"
|
||||||
>
|
>
|
||||||
{{ person.name }}
|
{{ person.displayName || (person as any).name || '' }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
class="person-circle add-person"
|
class="person-circle add-person"
|
||||||
@mousedown="addPerson(person)"
|
@mousedown="addPerson(person)"
|
||||||
>
|
>
|
||||||
{{ person.name }}
|
{{ person.displayName || (person as any).name || '' }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -51,16 +51,17 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { searchPersons } from '@/api/sdk'
|
import { searchPersons } from '@/api/sdk'
|
||||||
import type { Person } from '@/domain/types'
|
// Accept either legacy Person shape or new MemberRef
|
||||||
const props = withDefaults(defineProps<{ people?: Person[] }>(), { people: () => [] })
|
type PersonLike = { id: number; name?: string; displayName?: string }
|
||||||
|
const props = withDefaults(defineProps<{ people?: PersonLike[] }>(), { people: () => [] })
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'add-person', person: Person): void
|
(e: 'add-person', person: PersonLike): void
|
||||||
(e: 'remove-person', person: Person): void
|
(e: 'remove-person', person: PersonLike): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isAddingPerson = ref(false)
|
const isAddingPerson = ref(false)
|
||||||
const searchName = ref('')
|
const searchName = ref('')
|
||||||
const searchResults = ref<Person[]>([])
|
const searchResults = ref<PersonLike[]>([])
|
||||||
|
|
||||||
// Template refs for DOM elements
|
// Template refs for DOM elements
|
||||||
const searchNameInput = ref<HTMLInputElement | null>(null)
|
const searchNameInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
@ -73,12 +74,12 @@ async function updateSearchResults() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const page = await searchPersons(q)
|
const page = await searchPersons(q)
|
||||||
const results: Person[] = page.items.map((p) => ({ id: p.id, name: p.name }))
|
const results: PersonLike[] = page.items.map((p) => ({ id: p.id, name: p.name }))
|
||||||
const idSet = new Set(props.people.map((p) => p.id))
|
const idSet = new Set(props.people.map((p) => p.id))
|
||||||
searchResults.value = results.filter((p: Person) => !idSet.has(p.id))
|
searchResults.value = results.filter((p: PersonLike) => !idSet.has(p.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPerson(person?: Person) {
|
function addPerson(person?: PersonLike) {
|
||||||
if (!person && searchResults.value.length > 0) {
|
if (!person && searchResults.value.length > 0) {
|
||||||
person = searchResults.value[0]
|
person = searchResults.value[0]
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +97,7 @@ function addPerson(person?: Person) {
|
||||||
isAddingPerson.value = false
|
isAddingPerson.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function removePerson(person: Person) {
|
function removePerson(person: PersonLike) {
|
||||||
emit('remove-person', person)
|
emit('remove-person', person)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,8 +153,6 @@ function createFromScratch() {
|
||||||
ingredients: [],
|
ingredients: [],
|
||||||
imageUrls: [],
|
imageUrls: [],
|
||||||
serves: 1,
|
serves: 1,
|
||||||
dateCreated: new Date(),
|
|
||||||
dateHidden: null,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export function decodeLookup<TIn, TOut>(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeRecipe(
|
export function decodeRecipe(
|
||||||
r: components['schemas']['RecipeOut'] | null | undefined
|
r: components['schemas']['RecipeOut'] | components['schemas']['Recipe'] | null | undefined
|
||||||
): Recipe {
|
): Recipe {
|
||||||
if (!r) throw new Error('Invalid recipe payload')
|
if (!r) throw new Error('Invalid recipe payload')
|
||||||
// Normalize arrays that may be optional
|
// Normalize arrays that may be optional
|
||||||
|
|
@ -34,11 +34,11 @@ export function decodeRecipe(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeMeal(
|
export function decodeMeal(
|
||||||
m: components['schemas']['MealOut'] | components['schemas']['Meal-Output'] | null | undefined
|
m: components['schemas']['MealOut'] | components['schemas']['Meal'] | null | undefined
|
||||||
): Meal {
|
): Meal {
|
||||||
if (!m) throw new Error('Invalid meal payload')
|
if (!m) throw new Error('Invalid meal payload')
|
||||||
const recipes = Array.isArray(m.recipes)
|
const recipes = Array.isArray(m.recipes)
|
||||||
? m.recipes.map((mr: components['schemas']['MealRecipe-Output'] | null | undefined) => decodeMealRecipe(mr))
|
? m.recipes.map((mr: components['schemas']['MealRecipe'] | null | undefined) => decodeMealRecipe(mr))
|
||||||
: []
|
: []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -47,14 +47,14 @@ export function decodeMeal(
|
||||||
purchaseDate: toDate(m.purchaseDate),
|
purchaseDate: toDate(m.purchaseDate),
|
||||||
consumedDate: toDate(m.consumedDate),
|
consumedDate: toDate(m.consumedDate),
|
||||||
recipes,
|
recipes,
|
||||||
chefs: m.chefs ?? [],
|
chefs: (m.chefs ?? []).map((p: any) => ({ id: p.id, displayName: 'displayName' in p ? p.displayName : (p.name ?? '') })),
|
||||||
consumers: m.consumers ?? [],
|
consumers: (m.consumers ?? []).map((p: any) => ({ id: p.id, displayName: 'displayName' in p ? p.displayName : (p.name ?? '') })),
|
||||||
cleanup: m.cleanup ?? [],
|
cleanup: (m.cleanup ?? []).map((p: any) => ({ id: p.id, displayName: 'displayName' in p ? p.displayName : (p.name ?? '') })),
|
||||||
extraIngredients: m.extraIngredients ?? [],
|
extraIngredients: m.extraIngredients ?? [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeMealRecipe(mr: components['schemas']['MealRecipe-Output'] | null | undefined): MealRecipe {
|
function decodeMealRecipe(mr: components['schemas']['MealRecipe'] | null | undefined): MealRecipe {
|
||||||
if (!mr) throw new Error('Invalid meal recipe payload')
|
if (!mr) throw new Error('Invalid meal recipe payload')
|
||||||
return {
|
return {
|
||||||
...mr,
|
...mr,
|
||||||
|
|
@ -131,7 +131,6 @@ export function toMealInput(meal: Meal): MealInput {
|
||||||
id: meal.id,
|
id: meal.id,
|
||||||
suggestedDate: meal.suggestedDate ? meal.suggestedDate.toISOString() : new Date().toISOString(),
|
suggestedDate: meal.suggestedDate ? meal.suggestedDate.toISOString() : new Date().toISOString(),
|
||||||
consumedDate: meal.consumedDate ? meal.consumedDate.toISOString() : null,
|
consumedDate: meal.consumedDate ? meal.consumedDate.toISOString() : null,
|
||||||
purchaseDate: meal.purchaseDate ? meal.purchaseDate.toISOString() : null,
|
|
||||||
chefs: meal.chefs,
|
chefs: meal.chefs,
|
||||||
cleanup: meal.cleanup,
|
cleanup: meal.cleanup,
|
||||||
consumers: meal.consumers,
|
consumers: meal.consumers,
|
||||||
|
|
@ -139,7 +138,6 @@ export function toMealInput(meal: Meal): MealInput {
|
||||||
mealId: r.mealId,
|
mealId: r.mealId,
|
||||||
recipeId: r.recipeId,
|
recipeId: r.recipeId,
|
||||||
servings: r.servings,
|
servings: r.servings,
|
||||||
recipe: null,
|
|
||||||
})),
|
})),
|
||||||
extraIngredients: meal.extraIngredients,
|
extraIngredients: meal.extraIngredients,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ export type User = components['schemas']['User']
|
||||||
export type Household = components['schemas']['HouseholdResponse']
|
export type Household = components['schemas']['HouseholdResponse']
|
||||||
export type Person = components['schemas']['Person']
|
export type Person = components['schemas']['Person']
|
||||||
// v2 no longer exposes Recipe-Input; use RecipeCreate at boundary when creating
|
// v2 no longer exposes Recipe-Input; use RecipeCreate at boundary when creating
|
||||||
export type MealInput = components['schemas']['Meal-Input']
|
export type MealInput = components['schemas']['MealIn']
|
||||||
export type MealRecipeOut = components['schemas']['MealRecipe-Output']
|
export type MealRecipeOut = components['schemas']['MealRecipe']
|
||||||
|
|
||||||
// Domain shapes: only adjust where UI needs Dates
|
// Domain shapes: only adjust where UI needs Dates
|
||||||
// RecipeOut has no dateCreated/dateHidden in current schema; keep arrays normalized in decoder
|
// RecipeOut has no dateCreated/dateHidden in current schema; keep arrays normalized in decoder
|
||||||
|
|
|
||||||
|
|
@ -8,22 +8,22 @@ describe('auth refresh (currentUser)', () => {
|
||||||
await logout()
|
await logout()
|
||||||
})
|
})
|
||||||
it('currentUser refresh sets auth token (no user in response)', async () => {
|
it('currentUser refresh sets auth token (no user in response)', async () => {
|
||||||
|
// Register both refresh and households handlers before invoking currentUser
|
||||||
server.use(
|
server.use(
|
||||||
http.post('*/api/v1/auth/refresh', () =>
|
http.post('*/api/v1/auth/refresh', () =>
|
||||||
HttpResponse.json({ accessToken: 'ref-123', tokenType: 'bearer', user: { id: 9, email: 'ref@example.com', displayName: 'Refreshed' } })
|
HttpResponse.json({ accessToken: 'ref-123', tokenType: 'bearer', user: { id: 9, email: 'ref@example.com', displayName: 'Refreshed' } })
|
||||||
)
|
),
|
||||||
)
|
|
||||||
const user = await currentUser()
|
|
||||||
expect(user).not.toBeNull()
|
|
||||||
|
|
||||||
// Subsequent API call should include Authorization header
|
|
||||||
server.use(
|
|
||||||
http.get('*/api/v1/users/me/households', ({ request }) => {
|
http.get('*/api/v1/users/me/households', ({ request }) => {
|
||||||
const auth = request.headers.get('authorization')
|
const auth = request.headers.get('authorization')
|
||||||
expect(auth?.toLowerCase()).toBe('bearer ref-123')
|
expect(auth?.toLowerCase()).toBe('bearer ref-123')
|
||||||
return HttpResponse.json([])
|
return HttpResponse.json([])
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const user = await currentUser()
|
||||||
|
expect(user).not.toBeNull()
|
||||||
|
|
||||||
|
// Subsequent API call should also include Authorization header
|
||||||
const res = await api.GET('/api/v1/users/me/households', { params: {} })
|
const res = await api.GET('/api/v1/users/me/households', { params: {} })
|
||||||
expect(res.response.ok).toBe(true)
|
expect(res.response.ok).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,12 @@ import { describe, it, expect } from 'vitest'
|
||||||
import { server, http, HttpResponse } from './test-setup'
|
import { server, http, HttpResponse } from './test-setup'
|
||||||
import { api } from '@/api/client'
|
import { api } from '@/api/client'
|
||||||
|
|
||||||
describe('API household header injection', () => {
|
describe('API household path scoping', () => {
|
||||||
it('sends X-Household-Slug when provider returns slug', async () => {
|
it('requires household slug in path params', async () => {
|
||||||
server.use(
|
server.use(
|
||||||
http.get('*/api/v1/recipes', ({ request }) => {
|
http.get('*/api/v1/households/the-smiths/recipes', () => HttpResponse.json({ items: [], total: 0 })),
|
||||||
// Should include our header set by client provider
|
|
||||||
const slug = request.headers.get('x-household-slug') || request.headers.get('X-Household-Slug')
|
|
||||||
if (!slug) return new HttpResponse(null, { status: 400 })
|
|
||||||
return HttpResponse.json([])
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
const res = await api.GET('/api/v1/households/{householdSlug}/recipes', { params: { path: { householdSlug: 'the-smiths' }, query: {} } })
|
||||||
// Directly set provider without Vue router by calling internal setter via dynamic import
|
|
||||||
const { setHouseholdSlugProvider } = await import('@/api/client')
|
|
||||||
setHouseholdSlugProvider(() => 'the-smiths')
|
|
||||||
|
|
||||||
const res = await api.GET('/api/v1/recipes', { params: {} })
|
|
||||||
expect(res.response.ok).toBe(true)
|
expect(res.response.ok).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { setHouseholdSlugProvider } from '@/api/client'
|
||||||
import { listMembers } from '@/api/households'
|
import { listMembers } from '@/api/households'
|
||||||
|
|
||||||
describe('households api (list members)', () => {
|
describe('households api (list members)', () => {
|
||||||
it('GETs members with Authorization and X-Household-Slug headers', async () => {
|
it('GETs members with Authorization header and slug in path', async () => {
|
||||||
// Simulate login token
|
// Simulate login token
|
||||||
server.use(
|
server.use(
|
||||||
http.post('*/api/v1/auth/login', () =>
|
http.post('*/api/v1/auth/login', () =>
|
||||||
|
|
@ -18,11 +18,9 @@ describe('households api (list members)', () => {
|
||||||
setHouseholdSlugProvider(() => 'the-smiths')
|
setHouseholdSlugProvider(() => 'the-smiths')
|
||||||
|
|
||||||
server.use(
|
server.use(
|
||||||
http.get('*/api/v1/households/members', ({ request }) => {
|
http.get('*/api/v1/households/the-smiths/members', ({ request }) => {
|
||||||
const auth = request.headers.get('authorization')
|
const auth = request.headers.get('authorization')
|
||||||
expect(auth?.toLowerCase()).toBe('bearer toklm')
|
expect(auth?.toLowerCase()).toBe('bearer toklm')
|
||||||
const slug = request.headers.get('x-household-slug')
|
|
||||||
expect(slug).toBe('the-smiths')
|
|
||||||
return HttpResponse.json([
|
return HttpResponse.json([
|
||||||
{ id: 10, email: 'a@example.com', displayName: 'Alice' },
|
{ id: 10, email: 'a@example.com', displayName: 'Alice' },
|
||||||
{ id: 11, email: 'b@example.com', displayName: 'Bob' },
|
{ id: 11, email: 'b@example.com', displayName: 'Bob' },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue