Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/api/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,24 @@ export interface User {
role: string
}

export interface MeUser {
id: number
email: string
name: string
avatar: string | null
role: string
}
Comment on lines +10 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: MeUser duplicates all of User's fields, with avatar being the only addition. Consider extending instead:

export interface MeUser extends User {
	avatar: string | null
}

This avoids the maintenance burden of keeping both interfaces in sync if shared fields change.


export const createUserApi = (fetcher: typeof fetch) => {
fetcher = withJson(fetcher)
return {
list: () =>
fetcher(`/api/public/v0/users`)
.then((r) => jsonResponse<{ users: User[] }>(r))
.then((r) => r.users),
me: () =>
fetcher(`/api/public/v0/users/me`)
.then((r) => jsonResponse<{ user: MeUser }>(r))
.then((r) => r.user),
}
}
3 changes: 2 additions & 1 deletion src/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,9 @@ async function handleStatus(): Promise<void> {
let valid = false
try {
const api = createApi(tenantUrl, token, result.authType)
await api.projects.list()
const me = await api.users.me()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid admin-only user endpoint for auth validation

auth status now validates credentials via api.users.me(), but QA Sphere’s Users API is admin-restricted in the public docs; in environments where /users routes enforce that policy, valid non-admin credentials will be reported as invalid or expired. This is a regression from the previous projects.list() check, which worked for regular project roles, so the status command can now fail for legitimate users.

Useful? React with 👍 / 👎.

console.log(` Status: ${chalk.green('valid')}`)
console.log(` User: ${me.name} <${me.email}> (${me.role})`)
valid = true
} catch {
console.log(` Status: ${chalk.red('invalid or expired')}`)
Expand Down
27 changes: 20 additions & 7 deletions src/tests/auth-e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,23 @@ const tokenSuccessHandler = (expiresIn = 3600, refreshExpiresIn = 90 * 24 * 3600
})
})

const projectsHandler = http.get(`${tenantUrl}/api/public/v0/project`, ({ request }) => {
const testMeUser = {
id: 42,
email: 'tester@example.com',
name: 'Test User',
avatar: null,
role: 'admin',
}

const meHandler = http.get(`${tenantUrl}/api/public/v0/users/me`, ({ request }) => {
const auth = request.headers.get('Authorization')
if (auth === `ApiKey ${testApiKey}` || auth === `Bearer ${testAccessToken}`) {
return HttpResponse.json({ data: [], total: 0 })
return HttpResponse.json({ user: testMeUser })
}
return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 })
})

const server = setupServer(checkTenantHandler, projectsHandler)
const server = setupServer(checkTenantHandler, meHandler)

// --- Hoisted mock state ---
// vi.hoisted runs before imports, so vi.mock factories can reference these.
Expand Down Expand Up @@ -395,6 +403,11 @@ describe('auth login → status → logout lifecycle', () => {
)
expect(log).toHaveBeenCalledWith(expect.stringContaining('credentials.json'))
expect(log).toHaveBeenCalledWith(expect.stringContaining('valid'))
expect(log).toHaveBeenCalledWith(
expect.stringContaining(
`User: ${testMeUser.name} <${testMeUser.email}> (${testMeUser.role})`
)
)
expect(log).toHaveBeenCalledWith(
expect.stringMatching(/Re-authentication required: in (89|90) days \(resets on each use\)/)
)
Expand Down Expand Up @@ -884,10 +897,10 @@ describe('token refresh at load time', () => {
{ status: 401 }
)
}),
http.get(`${tenantUrl}/api/public/v0/project`, ({ request }) => {
http.get(`${tenantUrl}/api/public/v0/users/me`, ({ request }) => {
const auth = request.headers.get('Authorization')
if (auth === `Bearer ${refreshedAccessToken}`) {
return HttpResponse.json({ data: [], total: 0 })
return HttpResponse.json({ user: testMeUser })
}
return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 })
})
Expand Down Expand Up @@ -981,10 +994,10 @@ describe('token refresh at load time', () => {
{ status: 401 }
)
}),
http.get(`${tenantUrl}/api/public/v0/project`, ({ request }) => {
http.get(`${tenantUrl}/api/public/v0/users/me`, ({ request }) => {
const auth = request.headers.get('Authorization')
if (auth === `Bearer ${refreshedAccessToken}`) {
return HttpResponse.json({ data: [], total: 0 })
return HttpResponse.json({ user: testMeUser })
}
return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 })
})
Expand Down
Loading