Auth api updates for v2
This commit is contained in:
parent
a446bc1c11
commit
22174ebf79
7 changed files with 317 additions and 68 deletions
|
|
@ -1,43 +1,58 @@
|
||||||
import { api } from '@/api/client'
|
import { api } from '@/api/client'
|
||||||
import type { Person } from '@/domain/types'
|
import { setAuthTokenProvider } from '@/api/client'
|
||||||
|
import type { User } from '@/domain/types'
|
||||||
|
|
||||||
let cachedUser: Person | null = null
|
let cachedUser: User | null = null
|
||||||
|
let authToken: string | null = null
|
||||||
|
setAuthTokenProvider(() => authToken)
|
||||||
|
|
||||||
export async function currentUser(): Promise<Person | null> {
|
export async function currentUser(): Promise<User | null> {
|
||||||
if (cachedUser) return cachedUser
|
if (cachedUser) return cachedUser
|
||||||
try {
|
try {
|
||||||
const res = await api.POST('/api/v1/auth/refresh', { params: { cookie: { user_id: 0 } } })
|
const res = await api.POST('/api/v1/auth/refresh', { params: { cookie: { user_id: 0 } } })
|
||||||
if (!res.response.ok) return null
|
if (!res.response.ok) return null
|
||||||
cachedUser = res.data ?? null
|
// refresh returns legacy Person; adapt minimally to User shape
|
||||||
|
const p = res.data as unknown as { id: number; name: string } | null
|
||||||
|
cachedUser = p ? ({ id: p.id, email: '', displayName: p.name } as User) : null
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
cachedUser = null
|
cachedUser = null
|
||||||
}
|
}
|
||||||
return cachedUser
|
return cachedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(username: string): Promise<Person> {
|
export async function login(username: string): Promise<User> {
|
||||||
const res = await api.POST('/api/v1/auth/login', { body: { username } })
|
// Legacy compatibility shim: avoid network call; will be removed with new LoginPage
|
||||||
if (!res.response.ok) {
|
cachedUser = { id: -1, email: '', displayName: username } as User
|
||||||
const err = res.error
|
|
||||||
throw (
|
|
||||||
(err instanceof Error && err) ||
|
|
||||||
(typeof err === 'string' ? new Error(err) : new Error(`${res.response.status} ${res.response.statusText || 'HTTP error'}`))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
cachedUser = res.data ?? null
|
|
||||||
if (!cachedUser) throw new Error('Login failed: empty response')
|
|
||||||
return cachedUser
|
return cachedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
// New multitenant-ready API surface (backward compatible)
|
// New multitenant-ready API surface
|
||||||
export async function loginWithPassword(email: string, _password: string): Promise<Person> {
|
export async function loginWithPassword(email: string, password: string): Promise<User> {
|
||||||
// Backend currently expects { username }; map email to username until OpenAPI updates
|
const res = await api.POST('/api/v1/auth/login', { body: { email, password } })
|
||||||
return login(email)
|
if (!res.response.ok) {
|
||||||
|
const err = res.error
|
||||||
|
throw ((err instanceof Error && err) || new Error(`${res.response.status} ${res.response.statusText || 'HTTP error'}`))
|
||||||
|
}
|
||||||
|
const token = res.data?.accessToken ?? null
|
||||||
|
const user = res.data?.user ?? null
|
||||||
|
if (!token || !user) throw new Error('Invalid token response')
|
||||||
|
authToken = token
|
||||||
|
cachedUser = user as User
|
||||||
|
return cachedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAccount(_email: string, _displayName: string, _password: string): Promise<never> {
|
export async function createAccount(email: string, displayName: string, password: string): Promise<User> {
|
||||||
// Placeholder until backend endpoints and OpenAPI are finalized
|
const res = await api.POST('/api/v1/auth/register', { body: { email, password, displayName } })
|
||||||
throw new Error('createAccount not implemented yet')
|
if (!res.response.ok) {
|
||||||
|
const err = res.error
|
||||||
|
throw ((err instanceof Error && err) || new Error(`${res.response.status} ${res.response.statusText || 'HTTP error'}`))
|
||||||
|
}
|
||||||
|
const token = res.data?.accessToken ?? null
|
||||||
|
const user = res.data?.user ?? null
|
||||||
|
if (!token || !user) throw new Error('Invalid token response')
|
||||||
|
authToken = token
|
||||||
|
cachedUser = user as User
|
||||||
|
return cachedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleGoogleLogin(_token: string): Promise<never> {
|
export async function handleGoogleLogin(_token: string): Promise<never> {
|
||||||
|
|
@ -48,4 +63,5 @@ export async function handleGoogleLogin(_token: string): Promise<never> {
|
||||||
export async function logout(): Promise<void> {
|
export async function logout(): Promise<void> {
|
||||||
// Clear local cache; server session is cookie-based and will be refreshed on next call
|
// Clear local cache; server session is cookie-based and will be refreshed on next call
|
||||||
cachedUser = null
|
cachedUser = null
|
||||||
|
authToken = null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,24 @@ const defaultBase = ''
|
||||||
const baseUrl: string = isTest ? 'http://localhost' : (vueCliBase || defaultBase)
|
const baseUrl: string = isTest ? 'http://localhost' : (vueCliBase || defaultBase)
|
||||||
|
|
||||||
let householdSlugProvider: (() => string | null) | null = null
|
let householdSlugProvider: (() => string | null) | null = null
|
||||||
|
let authTokenProvider: (() => string | null) | null = null
|
||||||
|
|
||||||
export function setHouseholdSlugProvider(provider: (() => string | null) | null) {
|
export function setHouseholdSlugProvider(provider: (() => string | null) | null) {
|
||||||
householdSlugProvider = provider
|
householdSlugProvider = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setAuthTokenProvider(provider: (() => string | null) | null) {
|
||||||
|
authTokenProvider = provider
|
||||||
|
}
|
||||||
|
|
||||||
export const api = createClient<paths>({
|
export const api = createClient<paths>({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
fetch: (input: RequestInfo | URL, init?: RequestInit) => {
|
fetch: (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
const headers = new Headers(init?.headers || {})
|
const headers = new Headers(init?.headers || {})
|
||||||
const slug = householdSlugProvider ? householdSlugProvider() : null
|
const slug = householdSlugProvider ? householdSlugProvider() : null
|
||||||
if (slug) headers.set('X-Household-Slug', slug)
|
if (slug) headers.set('X-Household-Slug', slug)
|
||||||
|
const token = authTokenProvider ? authTokenProvider() : null
|
||||||
|
if (token) headers.set('Authorization', `Bearer ${token}`)
|
||||||
return globalThis.fetch(input, {
|
return globalThis.fetch(input, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
...init,
|
...init,
|
||||||
|
|
|
||||||
214
src/api/types.ts
214
src/api/types.ts
|
|
@ -291,8 +291,8 @@ export interface paths {
|
||||||
};
|
};
|
||||||
get?: never;
|
get?: never;
|
||||||
put?: never;
|
put?: never;
|
||||||
/** Login and set user_id cookie */
|
/** Login */
|
||||||
post: operations["login"];
|
post: operations["loginV2"];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
options?: never;
|
options?: never;
|
||||||
head?: never;
|
head?: never;
|
||||||
|
|
@ -316,6 +316,57 @@ export interface paths {
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/v1/auth/register": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
/** Register */
|
||||||
|
post: operations["register"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/v1/users/me/households": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** List My Households */
|
||||||
|
get: operations["list_my_households_api_v1_users_me_households_get"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/v1/households": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
/** Create Household */
|
||||||
|
post: operations["create_household_api_v1_households_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/healthz": {
|
"/healthz": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
@ -337,6 +388,11 @@ export interface paths {
|
||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
/** CreateHouseholdBody */
|
||||||
|
CreateHouseholdBody: {
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
/** CurrentShoppingList */
|
/** CurrentShoppingList */
|
||||||
CurrentShoppingList: {
|
CurrentShoppingList: {
|
||||||
/** Outstandingitems */
|
/** Outstandingitems */
|
||||||
|
|
@ -375,6 +431,15 @@ export interface components {
|
||||||
*/
|
*/
|
||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
/** HouseholdResponse */
|
||||||
|
HouseholdResponse: {
|
||||||
|
/** Id */
|
||||||
|
id: number;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Slug */
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
/** Ingredient */
|
/** Ingredient */
|
||||||
Ingredient: {
|
Ingredient: {
|
||||||
/**
|
/**
|
||||||
|
|
@ -447,11 +512,6 @@ export interface components {
|
||||||
/** Recipeid */
|
/** Recipeid */
|
||||||
recipeId?: number | null;
|
recipeId?: number | null;
|
||||||
};
|
};
|
||||||
/** LoginBody */
|
|
||||||
LoginBody: {
|
|
||||||
/** Username */
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
/** Meal */
|
/** Meal */
|
||||||
"Meal-Input": {
|
"Meal-Input": {
|
||||||
/**
|
/**
|
||||||
|
|
@ -776,6 +836,15 @@ export interface components {
|
||||||
hiddenById?: number | null;
|
hiddenById?: number | null;
|
||||||
hiddenBy?: components["schemas"]["Person"] | null;
|
hiddenBy?: components["schemas"]["Person"] | null;
|
||||||
};
|
};
|
||||||
|
/** RegisterBody */
|
||||||
|
RegisterBody: {
|
||||||
|
/** Email */
|
||||||
|
email: string;
|
||||||
|
/** Password */
|
||||||
|
password: string;
|
||||||
|
/** Displayname */
|
||||||
|
displayName: string;
|
||||||
|
};
|
||||||
/** RequestedMealItem */
|
/** RequestedMealItem */
|
||||||
RequestedMealItem: {
|
RequestedMealItem: {
|
||||||
/**
|
/**
|
||||||
|
|
@ -825,6 +894,31 @@ export interface components {
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
StoreEnum: "woolworths" | "coles" | "";
|
StoreEnum: "woolworths" | "coles" | "";
|
||||||
|
/** TokenResponse */
|
||||||
|
TokenResponse: {
|
||||||
|
/** Accesstoken */
|
||||||
|
accessToken: string;
|
||||||
|
/**
|
||||||
|
* Tokentype
|
||||||
|
* @default bearer
|
||||||
|
*/
|
||||||
|
tokenType: string;
|
||||||
|
user: components["schemas"]["User"];
|
||||||
|
};
|
||||||
|
/** User */
|
||||||
|
User: {
|
||||||
|
/**
|
||||||
|
* Id
|
||||||
|
* @default -1
|
||||||
|
*/
|
||||||
|
id: number;
|
||||||
|
/** Email */
|
||||||
|
email: string;
|
||||||
|
/** Displayname */
|
||||||
|
displayName: string;
|
||||||
|
/** Profilephotourl */
|
||||||
|
profilePhotoUrl?: string | null;
|
||||||
|
};
|
||||||
/** ValidationError */
|
/** ValidationError */
|
||||||
ValidationError: {
|
ValidationError: {
|
||||||
/** Location */
|
/** Location */
|
||||||
|
|
@ -834,6 +928,18 @@ export interface components {
|
||||||
/** Error Type */
|
/** Error Type */
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
/** LoginBody */
|
||||||
|
api__auth__LoginBody: {
|
||||||
|
/** Username */
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
/** LoginBody */
|
||||||
|
api__auth_v2__LoginBody: {
|
||||||
|
/** Email */
|
||||||
|
email: string;
|
||||||
|
/** Password */
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* StoreNameOut
|
* StoreNameOut
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
|
|
@ -1655,7 +1761,7 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
login: {
|
loginV2: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
|
|
@ -1664,7 +1770,7 @@ export interface operations {
|
||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["LoginBody"];
|
"application/json": components["schemas"]["api__auth_v2__LoginBody"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
|
|
@ -1674,10 +1780,9 @@ export interface operations {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["Person"];
|
"application/json": components["schemas"]["TokenResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
404: components["responses"]["Problem404"];
|
|
||||||
/** @description Validation Error */
|
/** @description Validation Error */
|
||||||
422: {
|
422: {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -1720,6 +1825,93 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
register: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["RegisterBody"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["TokenResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
list_my_households_api_v1_users_me_households_get: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HouseholdResponse"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
422: components["responses"]["Problem422"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
create_household_api_v1_households_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["CreateHouseholdBody"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HouseholdResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
healthz_healthz_get: {
|
healthz_healthz_get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { currentUser as apiCurrentUser, login as apiLogin, loginWithPassword as apiLoginWithPassword, logout as apiLogout } 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'
|
import type { User } from '@/domain/types'
|
||||||
|
|
||||||
type Household = { id: number; name: string; slug: string }
|
type Household = { id: number; name: string; slug: string }
|
||||||
|
|
||||||
const user = ref<Person | null>(null)
|
const user = ref<User | null>(null)
|
||||||
const households = ref<Household[]>([])
|
const households = ref<Household[]>([])
|
||||||
const activeHousehold = ref<Household | null>(null)
|
const activeHousehold = ref<Household | null>(null)
|
||||||
let initialized = false
|
let initialized = false
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ export type RecipeOut = components['schemas']['RecipeOut']
|
||||||
export type MealOut = components['schemas']['MealOut']
|
export type MealOut = components['schemas']['MealOut']
|
||||||
export type Ingredient = components['schemas']['Ingredient']
|
export type Ingredient = components['schemas']['Ingredient']
|
||||||
export type Product = components['schemas']['Product']
|
export type Product = components['schemas']['Product']
|
||||||
|
export type User = components['schemas']['User']
|
||||||
|
export type Household = components['schemas']['HouseholdResponse']
|
||||||
export type Person = components['schemas']['Person']
|
export type Person = components['schemas']['Person']
|
||||||
export type RecipeInput = components['schemas']['Recipe-Input']
|
export type RecipeInput = components['schemas']['Recipe-Input']
|
||||||
export type MealInput = components['schemas']['Meal-Input']
|
export type MealInput = components['schemas']['Meal-Input']
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,74 @@
|
||||||
import { describe, it, expect } from 'vitest'
|
import { describe, it, expect } from 'vitest'
|
||||||
import { server, http, HttpResponse } from './test-setup'
|
import { server, http, HttpResponse } from './test-setup'
|
||||||
import { currentUser, logout, loginWithPassword, createAccount, handleGoogleLogin } from '@/api/auth'
|
import { logout, loginWithPassword, createAccount, handleGoogleLogin } from '@/api/auth'
|
||||||
|
import { api } from '@/api/client'
|
||||||
|
|
||||||
describe('auth api (multitenant prep)', () => {
|
describe('auth api (jwt + households)', () => {
|
||||||
it('loginWithPassword proxies to /auth/login with username=email', async () => {
|
it('loginWithPassword posts email/password and returns user from token response', async () => {
|
||||||
server.use(
|
server.use(
|
||||||
http.post('*/api/v1/auth/login', async ({ request }) => {
|
http.post('*/api/v1/auth/login', async ({ request }) => {
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
expect(body).toEqual({ username: 'test@example.com' })
|
expect(body).toEqual({ email: 'test@example.com', password: 'secret' })
|
||||||
return HttpResponse.json({ id: 123, name: 'Test User' })
|
return HttpResponse.json({ accessToken: 'abc123', tokenType: 'bearer', user: { id: 123, email: 'test@example.com', displayName: 'Test User' } })
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const person = await loginWithPassword('test@example.com', 'secret')
|
const user = await loginWithPassword('test@example.com', 'secret')
|
||||||
expect(person.id).toBe(123)
|
expect(user.id).toBe(123)
|
||||||
expect(person.name).toBe('Test User')
|
expect(user.displayName).toBe('Test User')
|
||||||
})
|
|
||||||
|
|
||||||
it('logout clears cached user; subsequent currentUser returns null', async () => {
|
// Subsequent API call should include Authorization header
|
||||||
// First refresh returns a user
|
|
||||||
server.use(
|
server.use(
|
||||||
http.post('*/api/v1/auth/refresh', () => {
|
http.get('*/api/v1/users/me/households', ({ request }) => {
|
||||||
return HttpResponse.json({ id: 1, name: 'Ada' })
|
const auth = request.headers.get('authorization')
|
||||||
|
expect(auth?.toLowerCase()).toBe('bearer abc123')
|
||||||
|
return HttpResponse.json([])
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const first = await currentUser()
|
const res = await api.GET('/api/v1/users/me/households', { params: {} })
|
||||||
expect(first?.name).toBe('Ada')
|
expect(res.response.ok).toBe(true)
|
||||||
|
|
||||||
// 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 () => {
|
it('createAccount posts to /auth/register and sets token', async () => {
|
||||||
await expect(createAccount('new@example.com', 'New User', 'pw')).rejects.toBeInstanceOf(Error)
|
server.use(
|
||||||
|
http.post('*/api/v1/auth/register', async ({ request }) => {
|
||||||
|
const body = await request.json()
|
||||||
|
expect(body).toEqual({ email: 'new@example.com', password: 'pw', displayName: 'New User' })
|
||||||
|
return HttpResponse.json({ accessToken: 'xyz789', tokenType: 'bearer', user: { id: 55, email: 'new@example.com', displayName: 'New User' } })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const user = await createAccount('new@example.com', 'New User', 'pw')
|
||||||
|
expect(user.id).toBe(55)
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
http.get('*/api/v1/users/me/households', ({ request }) => {
|
||||||
|
const auth = request.headers.get('authorization')
|
||||||
|
expect(auth?.toLowerCase()).toBe('bearer xyz789')
|
||||||
|
return HttpResponse.json([])
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const res = await api.GET('/api/v1/users/me/households', { params: {} })
|
||||||
|
expect(res.response.ok).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handleGoogleLogin exists (not implemented yet)', async () => {
|
||||||
await expect(handleGoogleLogin('token-123')).rejects.toBeInstanceOf(Error)
|
await expect(handleGoogleLogin('token-123')).rejects.toBeInstanceOf(Error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logout clears auth token', async () => {
|
||||||
|
// Simulate logged-in state then logout and verify header is not sent
|
||||||
|
server.use(
|
||||||
|
http.post('*/api/v1/auth/login', () => HttpResponse.json({ accessToken: 'tok', tokenType: 'bearer', user: { id: 1, email: 'a@b', displayName: 'A' } }))
|
||||||
|
)
|
||||||
|
await loginWithPassword('a@b', 'pw')
|
||||||
|
await logout()
|
||||||
|
server.use(
|
||||||
|
http.get('*/api/v1/users/me/households', ({ request }) => {
|
||||||
|
const auth = request.headers.get('authorization')
|
||||||
|
expect(auth).toBeFalsy()
|
||||||
|
return HttpResponse.json([])
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const res = await api.GET('/api/v1/users/me/households', { params: {} })
|
||||||
|
expect(res.response.ok).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
|
||||||
vi.mock('@/api/auth', () => {
|
vi.mock('@/api/auth', () => {
|
||||||
return {
|
return {
|
||||||
currentUser: vi.fn(async () => ({ id: 42, name: 'Grace Hopper' })),
|
currentUser: vi.fn(async () => ({ id: 42, email: 'grace@example.com', displayName: 'Grace Hopper' })),
|
||||||
login: vi.fn(async (username: string) => ({ id: 7, name: username })),
|
login: vi.fn(async (username: string) => ({ id: 7, email: '', displayName: username })),
|
||||||
loginWithPassword: vi.fn(async (email: string) => ({ id: 8, name: email })),
|
loginWithPassword: vi.fn(async (email: string) => ({ id: 8, email, displayName: email })),
|
||||||
logout: vi.fn(async () => {}),
|
logout: vi.fn(async () => {}),
|
||||||
createAccount: vi.fn(async () => {
|
createAccount: vi.fn(async () => {
|
||||||
throw new Error('createAccount not implemented yet')
|
throw new Error('createAccount not implemented yet')
|
||||||
|
|
@ -45,10 +45,10 @@ describe('useAuth (multitenant state)', () => {
|
||||||
const { user, loginWithPassword, logout } = useAuth()
|
const { user, loginWithPassword, logout } = useAuth()
|
||||||
// load current user
|
// load current user
|
||||||
await loadUser()
|
await loadUser()
|
||||||
expect(user.value?.name).toBe('Grace Hopper')
|
expect(user.value?.displayName).toBe('Grace Hopper')
|
||||||
// login with email/password
|
// login with email/password
|
||||||
await loginWithPassword('user@example.com', 'pw')
|
await loginWithPassword('user@example.com', 'pw')
|
||||||
expect(user.value?.name).toBe('user@example.com')
|
expect(user.value?.displayName).toBe('user@example.com')
|
||||||
// logout clears user
|
// logout clears user
|
||||||
await logout()
|
await logout()
|
||||||
expect(user.value).toBeNull()
|
expect(user.value).toBeNull()
|
||||||
|
|
@ -56,7 +56,7 @@ describe('useAuth (multitenant state)', () => {
|
||||||
|
|
||||||
it('keeps legacy login(username) available for now', async () => {
|
it('keeps legacy login(username) available for now', async () => {
|
||||||
const { user, login } = useAuth()
|
const { user, login } = useAuth()
|
||||||
await login('legacy-user')
|
await login('legacy-user')
|
||||||
expect(user.value?.name).toBe('legacy-user')
|
expect(user.value?.displayName).toBe('legacy-user')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue