From a446bc1c119159fbf95244169003d1ff7e720734 Mon Sep 17 00:00:00 2001 From: jableader Date: Sat, 1 Nov 2025 13:44:58 +1100 Subject: [PATCH] housekeeping --- frontend-spec.md | 53 +++++++++++------ src/App.vue | 6 ++ src/api/auth.ts | 21 +++++++ src/api/client.ts | 10 ++++ src/components/HouseholdSwitcher.vue | 42 ++++++++++++++ src/composables/useAuth.ts | 38 +++++++++++- src/composables/useHousehold.ts | 18 ++++++ src/router/index.ts | 87 ++++++++++++++-------------- src/views/CreateAccount.vue | 10 ++++ src/views/InvitationAccept.vue | 10 ++++ src/views/Welcome.vue | 10 ++++ tests/auth.api.test.ts | 42 ++++++++++++++ tests/household.header.test.ts | 23 ++++++++ tests/router.multitenant.test.ts | 36 ++++++++++++ tests/useAuth.multitenant.test.ts | 62 ++++++++++++++++++++ 15 files changed, 402 insertions(+), 66 deletions(-) create mode 100644 src/components/HouseholdSwitcher.vue create mode 100644 src/composables/useHousehold.ts create mode 100644 src/views/CreateAccount.vue create mode 100644 src/views/InvitationAccept.vue create mode 100644 src/views/Welcome.vue create mode 100644 tests/auth.api.test.ts create mode 100644 tests/household.header.test.ts create mode 100644 tests/router.multitenant.test.ts create mode 100644 tests/useAuth.multitenant.test.ts diff --git a/frontend-spec.md b/frontend-spec.md index c4b800a..b814085 100644 --- a/frontend-spec.md +++ b/frontend-spec.md @@ -57,37 +57,40 @@ This plan is adapted to the existing codebase, focusing on refactoring rather th ## 4. Actionable Implementation Steps -1. **[ ] Refactor Authentication State & API**: +1. **[~] Refactor Authentication State & API**: - **Modify `src/api/auth.ts`**: - - Replace `login(username: string)` with `login(email, password)`. - - Add `createAccount(email, displayName, password)`, `handleGoogleLogin(token)`, and `logout()`. - - The API client should handle storing and clearing the auth token (e.g., from `localStorage`). + - Added minimal multitenant-ready surface while preserving legacy login: + - `loginWithPassword(email, password)` implemented by delegating to legacy `login(username)`; tests added. + - `logout()` clears cached user; covered by tests. + - Stubs for `createAccount(email, displayName, password)` and `handleGoogleLogin(token)` throw pending-implementation errors; tests assert presence. + - Token/JWT storage is still cookie-based (per current backend); no localStorage changes yet. - **Modify `src/composables/useAuth.ts`**: - - Update the `login` function to accept email/password. - - Add a `register` function. - - The `user` ref should now hold the global User profile, and you should add a new state for `households` and `activeHousehold`. - - The `logout` function should clear the JWT and all user state. + - TODO: Update to accept email/password and expose `register`, `logout`, `households`, and `activeHousehold`. Not started. -2. **[ ] Update Router for Multi-Tenancy**: +2. **[~] Update Router for Multi-Tenancy**: - **Modify `src/router/index.ts`**: - - Add new public routes: `/create-account`, `/welcome`, and `/invitations/accept`. + - Added new public routes: `/create-account`, `/welcome`, and `/invitations/accept`. + - Feature flag `VUE_APP_MULTITENANT_ENABLED` controls nesting: + - When enabled: feature routes are nested under `/:householdSlug/...`. + - When disabled: legacy flat routes remain for backward compatibility. - **Refactor the `beforeEach` guard**: - It should allow access to public routes. - After login, it must fetch the user's households. - If the user has no households, redirect to `/welcome`. - If the user has households but is at the root (`/`), redirect to the first household's dashboard (e.g., `/${householdSlug}/dashboard`). - - **Nest existing routes**: All current data-related routes (`/meals`, `/shopping`, etc.) must be moved as children of a new dynamic `/:householdSlug` route. + - **Nest existing routes**: Implemented behind feature flag. 3. **[ ] Implement Onboarding and Invitation Flows**: - Build the `Welcome.vue` view for creating the first household. - Build the `CreateAccount.vue` view. - Build the "Accept Invitation" page (`/invitations/accept?token=...`). It should take the token from the URL, call the API, and redirect on success. + - Status: Placeholder views created for all three; logic pending. -4. **[ ] Integrate Household Context into the App**: - - **Create `src/composables/useHousehold.ts`**: This composable should extract the `householdSlug` from the current route's params. It will provide the `activeHouseholdSlug` to any component or service that needs it. - - **Implement `HouseholdSwitcher.vue`**: This component will use `useAuth` to get the list of the user's households and render navigation links. - - **Update API Services**: All data-fetching calls (e.g., for recipes, meals) must be updated to use the `activeHouseholdSlug` from `useHousehold`. The API client wrapper should be modified to prepend this slug to the request URL. - - Example: `api.get('/recipes')` becomes `api.get(\`/${activeHouseholdSlug.value}/recipes\`)`. +4. **[~] Integrate Household Context into the App**: + - **Create `src/composables/useHousehold.ts`**: Implemented. Extracts `householdSlug` from route and binds provider to API client. + - **Implement `HouseholdSwitcher.vue`**: Implemented minimal version and mounted in `App.vue`. + - **Update API Services**: Implemented header-injection in `src/api/client.ts` via `X-Household-Slug` using a configurable provider; no path changes. + - Verified by `tests/household.header.test.ts`. 5. **[ ] Implement Invitation UI**: - Build the `HouseholdSettings.vue` view for inviting members and listing current members. @@ -119,11 +122,11 @@ What exists today Gaps vs requirements - Authentication & onboarding - - Missing email/password login, Google OAuth, account creation, logout, JWT storage/refresh. + - Partial: Added `loginWithPassword` shim and `logout`. Still missing Google OAuth, account creation, JWT storage/refresh beyond cookie. - Missing `CreateAccount.vue`, `Welcome.vue`, invitation acceptance flow. - Routing & URL-based tenancy - - Missing `/:householdSlug/...` route nesting and post-login redirect logic. - - No `useHousehold.ts`; no `HouseholdSwitcher.vue`. + - Partial: Feature-flagged nesting implemented; still need guard logic for fetching households and redirects. + - `useHousehold.ts` and `HouseholdSwitcher.vue` added; further wiring to fetch households pending. - API boundary - No household scoping passed to backend. Need a typed strategy (prefer header parameter) without breaking OpenAPI typing. - Cleanup @@ -144,6 +147,18 @@ To align with OpenAPI typing and README axioms, do not rewrite request paths to Action - Implement header injection in `api/client.ts` with a pluggable getter for the active slug (decoupled from Vue imports). Update this spec once backend finalizes the parameter shape. + - DONE: Implemented with `setHouseholdSlugProvider`. Will align to OpenAPI when backend finalizes. + +--- + +Progress Log (Nov 1, 2025) +- Established green baseline (typecheck + tests pass). +- Added auth API tests driving a minimal multitenant-ready surface. +- Implemented `loginWithPassword`, `logout`, and stubs in `src/api/auth.ts` to satisfy tests. +- Refactored `useAuth` to add households and activeHousehold state, plus `loginWithPassword` and `logout`. Tests added and passing. +- Added router tests and implemented feature-flagged nested routes and new public routes. Placeholders for onboarding/invitations added. +- Implemented `useHousehold.ts`, header injection provider in API client, minimal `HouseholdSwitcher.vue`, and mounted it. Added a header injection test. +- Next: Update `LoginPage.vue` to email/password UI using `useAuth.loginWithPassword`; implement router guard post-login household redirects; scaffold `HouseholdSettings.vue` and households fetching in `useAuth`. --- diff --git a/src/App.vue b/src/App.vue index e96fb13..9368ecd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -29,6 +29,7 @@ +
@@ -39,6 +40,11 @@ diff --git a/src/api/auth.ts b/src/api/auth.ts index d211e77..f1e9db6 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -28,3 +28,24 @@ export async function login(username: string): Promise { if (!cachedUser) throw new Error('Login failed: empty response') return cachedUser } + +// New multitenant-ready API surface (backward compatible) +export async function loginWithPassword(email: string, _password: string): Promise { + // Backend currently expects { username }; map email to username until OpenAPI updates + return login(email) +} + +export async function createAccount(_email: string, _displayName: string, _password: string): Promise { + // Placeholder until backend endpoints and OpenAPI are finalized + throw new Error('createAccount not implemented yet') +} + +export async function handleGoogleLogin(_token: string): Promise { + // Placeholder until Google OAuth flow is wired + throw new Error('handleGoogleLogin not implemented yet') +} + +export async function logout(): Promise { + // Clear local cache; server session is cookie-based and will be refreshed on next call + cachedUser = null +} diff --git a/src/api/client.ts b/src/api/client.ts index c3d81de..b214122 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -9,12 +9,22 @@ const isTest = typeof process !== 'undefined' && (process.env?.VITEST === 'true' const defaultBase = '' const baseUrl: string = isTest ? 'http://localhost' : (vueCliBase || defaultBase) +let householdSlugProvider: (() => string | null) | null = null + +export function setHouseholdSlugProvider(provider: (() => string | null) | null) { + householdSlugProvider = provider +} + export const api = createClient({ baseUrl, fetch: (input: RequestInfo | URL, init?: RequestInit) => { + const headers = new Headers(init?.headers || {}) + const slug = householdSlugProvider ? householdSlugProvider() : null + if (slug) headers.set('X-Household-Slug', slug) return globalThis.fetch(input, { credentials: 'include', ...init, + headers, }) }, }) diff --git a/src/components/HouseholdSwitcher.vue b/src/components/HouseholdSwitcher.vue new file mode 100644 index 0000000..f2b0213 --- /dev/null +++ b/src/components/HouseholdSwitcher.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/src/composables/useAuth.ts b/src/composables/useAuth.ts index c6cdeca..139839c 100644 --- a/src/composables/useAuth.ts +++ b/src/composables/useAuth.ts @@ -1,8 +1,12 @@ import { ref } from 'vue' -import { currentUser as apiCurrentUser, login as apiLogin } from '@/api/auth' +import { currentUser as apiCurrentUser, login as apiLogin, loginWithPassword as apiLoginWithPassword, logout as apiLogout } from '@/api/auth' import type { Person } from '@/domain/types' +type Household = { id: number; name: string; slug: string } + const user = ref(null) +const households = ref([]) +const activeHousehold = ref(null) let initialized = false export async function loadUser() { @@ -18,6 +22,34 @@ export async function login(username: string) { return user.value } -export function useAuth() { - return { user, loadUser, login } +export async function loginWithPassword(email: string, password: string) { + user.value = await apiLoginWithPassword(email, password) + return user.value +} + +export async function logout() { + await apiLogout() + user.value = null + households.value = [] + activeHousehold.value = null +} + +export function setHouseholds(hs: Household[]) { + households.value = Array.isArray(hs) ? hs.slice() : [] +} + +export function setActiveHousehold(h: Household | string | null) { + if (h == null) { + activeHousehold.value = null + return + } + if (typeof h === 'string') { + activeHousehold.value = households.value.find((x) => x.slug === h) ?? null + } else { + activeHousehold.value = h + } +} + +export function useAuth() { + return { user, households, activeHousehold, loadUser, login, loginWithPassword, logout, setHouseholds, setActiveHousehold } } diff --git a/src/composables/useHousehold.ts b/src/composables/useHousehold.ts new file mode 100644 index 0000000..088e99a --- /dev/null +++ b/src/composables/useHousehold.ts @@ -0,0 +1,18 @@ +import { computed, watchEffect } from 'vue' +import { useRoute } from 'vue-router' +import { setHouseholdSlugProvider } from '@/api/client' + +export function useHousehold() { + const route = useRoute() + const activeHouseholdSlug = computed(() => { + const p = route.params?.householdSlug + return typeof p === 'string' ? p : null + }) + + // Keep API client in sync with current route's household slug + watchEffect(() => { + setHouseholdSlugProvider(() => activeHouseholdSlug.value) + }) + + return { activeHouseholdSlug } +} diff --git a/src/router/index.ts b/src/router/index.ts index ffa0276..5c660d2 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -10,55 +10,54 @@ const PurchasedShoppingListPage = () => import('@/components/shopping/PurchasedS const CurrentShoppingListPage = () => import('@/components/shopping/CurrentShoppingListPage.vue') const EditMealPage = () => import('@/components/meals/EditMealPage.vue') const EditRecipePage = () => import('@/components/recipes/EditRecipePage.vue') +const CreateAccount = () => import('@/views/CreateAccount.vue') +const Welcome = () => import('@/views/Welcome.vue') +const InvitationAccept = () => import('@/views/InvitationAccept.vue') export function createAppRouter(getCurrentUser: () => Promise | unknown): Router { - const routes: RouteRecordRaw[] = [ - { path: '/', redirect: { name: 'mealplan' } }, + const multitenantEnabled = typeof process !== 'undefined' && process.env?.VUE_APP_MULTITENANT_ENABLED === 'true' + const publicRoutes: RouteRecordRaw[] = [ { path: '/login', name: 'login', component: LoginPage }, - { path: '/mealplan', name: 'mealplan', component: MealPlanPage, meta: { requiresAuth: true } }, - { - path: '/shopping', - name: 'shopping', - component: MyShoppingPage, - meta: { requiresAuth: true }, - }, - { - path: '/shopping/current', - name: 'shopping-current', - component: CurrentShoppingListPage, - meta: { requiresAuth: true }, - }, - { - path: '/shopping/:id', - name: 'shopping-list', - component: PurchasedShoppingListPage, - props: true, - meta: { requiresAuth: true }, - }, - { path: '/recipes', name: 'recipes', component: RecipesPage, meta: { requiresAuth: true } }, - { - path: '/recipes/add', - name: 'recipe-add', - component: EditRecipePage, - meta: { requiresAuth: true }, - }, - { - path: '/recipes/:id', - name: 'recipe-edit', - component: EditRecipePage, - props: true, - meta: { requiresAuth: true }, - }, - { path: '/meals/add', name: 'meal-add', component: EditMealPage, meta: { requiresAuth: true } }, - { - path: '/meals/:id', - name: 'meal-edit', - component: EditMealPage, - props: true, - meta: { requiresAuth: true }, - }, + { path: '/create-account', name: 'create-account', component: CreateAccount }, + { path: '/welcome', name: 'welcome', component: Welcome }, + { path: '/invitations/accept', name: 'invitation-accept', component: InvitationAccept }, ] + const featureChildren: RouteRecordRaw[] = [ + { path: 'recipes', name: 'recipes', component: RecipesPage, meta: { requiresAuth: true } }, + { path: 'recipes/add', name: 'recipe-add', component: EditRecipePage, meta: { requiresAuth: true } }, + { path: 'recipes/:id', name: 'recipe-edit', component: EditRecipePage, props: true, meta: { requiresAuth: true } }, + { path: 'mealplan', name: 'mealplan', component: MealPlanPage, meta: { requiresAuth: true } }, + { path: 'meals/add', name: 'meal-add', component: EditMealPage, meta: { requiresAuth: true } }, + { path: 'meals/:id', name: 'meal-edit', component: EditMealPage, props: true, meta: { requiresAuth: true } }, + { path: 'shopping', name: 'shopping', component: MyShoppingPage, meta: { requiresAuth: true } }, + { path: 'shopping/current', name: 'shopping-current', component: CurrentShoppingListPage, meta: { requiresAuth: true } }, + { path: 'shopping/:id', name: 'shopping-list', component: PurchasedShoppingListPage, props: true, meta: { requiresAuth: true } }, + ] + + const routes: RouteRecordRaw[] = [] + + if (multitenantEnabled) { + routes.push({ path: '/', redirect: { name: 'login' } }) + routes.push(...publicRoutes) + routes.push({ path: '/:householdSlug', component: { template: '' }, children: featureChildren }) + } else { + routes.push({ path: '/', redirect: { name: 'mealplan' } }) + routes.push(...publicRoutes) + // legacy flat routes + routes.push( + { path: '/recipes', name: 'recipes', component: RecipesPage, meta: { requiresAuth: true } }, + { path: '/recipes/add', name: 'recipe-add', component: EditRecipePage, meta: { requiresAuth: true } }, + { path: '/recipes/:id', name: 'recipe-edit', component: EditRecipePage, props: true, meta: { requiresAuth: true } }, + { path: '/mealplan', name: 'mealplan', component: MealPlanPage, meta: { requiresAuth: true } }, + { path: '/meals/add', name: 'meal-add', component: EditMealPage, meta: { requiresAuth: true } }, + { path: '/meals/:id', name: 'meal-edit', component: EditMealPage, props: true, meta: { requiresAuth: true } }, + { path: '/shopping', name: 'shopping', component: MyShoppingPage, meta: { requiresAuth: true } }, + { path: '/shopping/current', name: 'shopping-current', component: CurrentShoppingListPage, meta: { requiresAuth: true } }, + { path: '/shopping/:id', name: 'shopping-list', component: PurchasedShoppingListPage, props: true, meta: { requiresAuth: true } }, + ) + } + const router = createRouter({ history: createWebHashHistory(), routes, diff --git a/src/views/CreateAccount.vue b/src/views/CreateAccount.vue new file mode 100644 index 0000000..aeb7001 --- /dev/null +++ b/src/views/CreateAccount.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/views/InvitationAccept.vue b/src/views/InvitationAccept.vue new file mode 100644 index 0000000..67f0d6e --- /dev/null +++ b/src/views/InvitationAccept.vue @@ -0,0 +1,10 @@ + + + diff --git a/src/views/Welcome.vue b/src/views/Welcome.vue new file mode 100644 index 0000000..9facc91 --- /dev/null +++ b/src/views/Welcome.vue @@ -0,0 +1,10 @@ + + + diff --git a/tests/auth.api.test.ts b/tests/auth.api.test.ts new file mode 100644 index 0000000..451ec1d --- /dev/null +++ b/tests/auth.api.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest' +import { server, http, HttpResponse } from './test-setup' +import { currentUser, logout, loginWithPassword, createAccount, handleGoogleLogin } from '@/api/auth' + +describe('auth api (multitenant prep)', () => { + it('loginWithPassword proxies to /auth/login with username=email', async () => { + server.use( + http.post('*/api/v1/auth/login', async ({ request }) => { + const body = await request.json() + expect(body).toEqual({ username: 'test@example.com' }) + return HttpResponse.json({ id: 123, name: 'Test User' }) + }) + ) + const person = await loginWithPassword('test@example.com', 'secret') + expect(person.id).toBe(123) + expect(person.name).toBe('Test User') + }) + + it('logout clears cached user; subsequent currentUser returns null', async () => { + // First refresh returns a user + server.use( + http.post('*/api/v1/auth/refresh', () => { + return HttpResponse.json({ id: 1, name: 'Ada' }) + }) + ) + const first = await currentUser() + expect(first?.name).toBe('Ada') + + // After logout, next refresh returns 401 and currentUser should resolve to null + await logout() + server.use( + http.post('*/api/v1/auth/refresh', () => new HttpResponse(null, { status: 401 })) + ) + const second = await currentUser() + expect(second).toBeNull() + }) + + it('createAccount and handleGoogleLogin exist but are not implemented yet', async () => { + await expect(createAccount('new@example.com', 'New User', 'pw')).rejects.toBeInstanceOf(Error) + await expect(handleGoogleLogin('token-123')).rejects.toBeInstanceOf(Error) + }) +}) diff --git a/tests/household.header.test.ts b/tests/household.header.test.ts new file mode 100644 index 0000000..8b7a3d9 --- /dev/null +++ b/tests/household.header.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest' +import { server, http, HttpResponse } from './test-setup' +import { api } from '@/api/client' + +describe('API household header injection', () => { + it('sends X-Household-Slug when provider returns slug', async () => { + server.use( + http.get('*/api/v1/recipes', ({ request }) => { + // 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([]) + }) + ) + + // 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) + }) +}) diff --git a/tests/router.multitenant.test.ts b/tests/router.multitenant.test.ts new file mode 100644 index 0000000..14750fc --- /dev/null +++ b/tests/router.multitenant.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +describe('router multitenant routing', () => { + beforeEach(() => { + vi.resetModules() + delete (process.env as any).VUE_APP_MULTITENANT_ENABLED + }) + + it('includes public routes and legacy flat routes when flag disabled', async () => { + const { createAppRouter } = await import('@/router/index') + const router = createAppRouter(() => null) + const paths = router.getRoutes().map((r) => r.path) + expect(paths).toContain('/login') + expect(paths).toContain('/create-account') + expect(paths).toContain('/welcome') + expect(paths).toContain('/invitations/accept') + // legacy flat routes + expect(paths).toContain('/recipes') + expect(paths).toContain('/shopping') + }) + + it('nests routes under /:householdSlug when flag enabled', async () => { + ;(process.env as any).VUE_APP_MULTITENANT_ENABLED = 'true' + const { createAppRouter } = await import('@/router/index') + const router = createAppRouter(() => null) + const paths = router.getRoutes().map((r) => r.path) + // Public routes still exist + expect(paths).toContain('/login') + expect(paths).toContain('/create-account') + // Nested route example + const hasNestedRecipes = paths.some((p) => p === '/:householdSlug/recipes') + expect(hasNestedRecipes).toBe(true) + // Legacy route should not be present when nested is enabled + expect(paths).not.toContain('/recipes') + }) +}) diff --git a/tests/useAuth.multitenant.test.ts b/tests/useAuth.multitenant.test.ts new file mode 100644 index 0000000..7f0c806 --- /dev/null +++ b/tests/useAuth.multitenant.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +vi.mock('@/api/auth', () => { + return { + currentUser: vi.fn(async () => ({ id: 42, name: 'Grace Hopper' })), + login: vi.fn(async (username: string) => ({ id: 7, name: username })), + loginWithPassword: vi.fn(async (email: string) => ({ id: 8, name: email })), + logout: vi.fn(async () => {}), + createAccount: vi.fn(async () => { + throw new Error('createAccount not implemented yet') + }), + handleGoogleLogin: vi.fn(async () => { + throw new Error('handleGoogleLogin not implemented yet') + }), + } +}) + +import { useAuth, loadUser } from '@/composables/useAuth' + +describe('useAuth (multitenant state)', () => { + beforeEach(() => { + // Reset modules and state between tests + vi.resetModules() + }) + + it('exposes households and activeHousehold state with setters', async () => { + const { user, households, activeHousehold, setHouseholds, setActiveHousehold } = useAuth() + expect(user.value).toBeNull() + expect(households.value).toEqual([]) + expect(activeHousehold.value).toBeNull() + + const hs = [ + { id: 1, name: 'Smiths', slug: 'the-smiths' }, + { id: 2, name: 'Johnsons', slug: 'the-johnsons' }, + ] + setHouseholds(hs) + expect(households.value.length).toBe(2) + setActiveHousehold(hs[1]) + expect(activeHousehold.value?.slug).toBe('the-johnsons') + setActiveHousehold('the-smiths') + expect(activeHousehold.value?.slug).toBe('the-smiths') + }) + + it('supports loading current user, loginWithPassword, and logout clearing user', async () => { + const { user, loginWithPassword, logout } = useAuth() + // load current user + await loadUser() + expect(user.value?.name).toBe('Grace Hopper') + // login with email/password + await loginWithPassword('user@example.com', 'pw') + expect(user.value?.name).toBe('user@example.com') + // logout clears user + await logout() + expect(user.value).toBeNull() + }) + + it('keeps legacy login(username) available for now', async () => { + const { user, login } = useAuth() + await login('legacy-user') + expect(user.value?.name).toBe('legacy-user') + }) +})