feat: resolve + cache auth at startup via getAuth() — fail-fast on invalid token
This commit is contained in:
parent
994f28f103
commit
0b75a08d7e
3 changed files with 81 additions and 0 deletions
73
__tests__/auth.test.ts
Normal file
73
__tests__/auth.test.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
vi.mock('../src/prisma.js', () => ({
|
||||
prisma: {
|
||||
apiToken: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
import { prisma } from '../src/prisma.js'
|
||||
import { getAuth, resetAuthCache } from '../src/auth.js'
|
||||
|
||||
const mockPrisma = prisma as unknown as {
|
||||
apiToken: { findUnique: ReturnType<typeof vi.fn> }
|
||||
}
|
||||
|
||||
const VALID_TOKEN_ROW = {
|
||||
id: 'token-id-1',
|
||||
user_id: 'user-1',
|
||||
token_hash: 'irrelevant-checked-by-crypto',
|
||||
revoked_at: null,
|
||||
user: { username: 'alice', is_demo: false },
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resetAuthCache()
|
||||
process.env.SCRUM4ME_TOKEN = 'test-token-value'
|
||||
})
|
||||
|
||||
describe('getAuth', () => {
|
||||
it('returns AuthContext for a valid token', async () => {
|
||||
mockPrisma.apiToken.findUnique.mockResolvedValue(VALID_TOKEN_ROW)
|
||||
|
||||
const auth = await getAuth()
|
||||
|
||||
expect(auth.tokenId).toBe('token-id-1')
|
||||
expect(auth.userId).toBe('user-1')
|
||||
expect(auth.username).toBe('alice')
|
||||
expect(auth.isDemo).toBe(false)
|
||||
})
|
||||
|
||||
it('caches the result — prisma is only called once on repeated calls', async () => {
|
||||
mockPrisma.apiToken.findUnique.mockResolvedValue(VALID_TOKEN_ROW)
|
||||
|
||||
await getAuth()
|
||||
await getAuth()
|
||||
|
||||
expect(mockPrisma.apiToken.findUnique).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('throws when SCRUM4ME_TOKEN is not set', async () => {
|
||||
delete process.env.SCRUM4ME_TOKEN
|
||||
|
||||
await expect(getAuth()).rejects.toThrow('SCRUM4ME_TOKEN is not set')
|
||||
})
|
||||
|
||||
it('throws when token is not found in the database', async () => {
|
||||
mockPrisma.apiToken.findUnique.mockResolvedValue(null)
|
||||
|
||||
await expect(getAuth()).rejects.toThrow('invalid or revoked')
|
||||
})
|
||||
|
||||
it('throws when token is revoked', async () => {
|
||||
mockPrisma.apiToken.findUnique.mockResolvedValue({
|
||||
...VALID_TOKEN_ROW,
|
||||
revoked_at: new Date(),
|
||||
})
|
||||
|
||||
await expect(getAuth()).rejects.toThrow('invalid or revoked')
|
||||
})
|
||||
})
|
||||
|
|
@ -10,6 +10,10 @@ export type AuthContext = {
|
|||
|
||||
let cached: AuthContext | null = null
|
||||
|
||||
export function resetAuthCache(): void {
|
||||
cached = null
|
||||
}
|
||||
|
||||
export async function getAuth(): Promise<AuthContext> {
|
||||
if (cached) return cached
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,14 @@ import { registerUpdateJobStatusTool } from './tools/update-job-status.js'
|
|||
import { registerVerifyTaskAgainstPlanTool } from './tools/verify-task-against-plan.js'
|
||||
import { registerCleanupMyWorktreesTool } from './tools/cleanup-my-worktrees.js'
|
||||
import { registerImplementNextStoryPrompt } from './prompts/implement-next-story.js'
|
||||
import { getAuth } from './auth.js'
|
||||
|
||||
const VERSION = '0.1.0'
|
||||
|
||||
async function main() {
|
||||
const auth = await getAuth()
|
||||
console.error(`scrum4me-mcp: authenticated as ${auth.username} (token ${auth.tokenId})`)
|
||||
|
||||
const server = new McpServer(
|
||||
{ name: 'scrum4me-mcp', version: VERSION },
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue