97 lines
3.8 KiB
TypeScript
97 lines
3.8 KiB
TypeScript
/**
|
|
* hasLocalCredential on the session user (issue #799): drives whether the
|
|
* password-change UI (Settings → Preferences) is shown to a user. It's
|
|
* computed by passport.deserializeUser (see auth.ts), so it must be verified
|
|
* on a REAL subsequent request — not just the login response — through a
|
|
* protected route.
|
|
*
|
|
* setupAuth ONCE per file (beforeAll), mirroring admin-api.local.test.ts:
|
|
* passport.serializeUser/deserializeUser register onto a global stack and
|
|
* don't replace, so calling setupAuth per-test would leave earlier tests'
|
|
* deserializers (bound to already-closed repos) running first and erroring
|
|
* out the whole chain. A dedicated file also keeps this isolated from
|
|
* auth.local.test.ts's per-test setupAuth calls (same global passport
|
|
* singleton within one file/worker).
|
|
*/
|
|
import { afterAll, beforeAll, describe, it, expect } from 'vitest';
|
|
import { mkdtempSync, rmSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { tmpdir } from 'os';
|
|
import express, { type Express } from 'express';
|
|
import request from 'supertest';
|
|
import { Repository } from '../db/repository.js';
|
|
import { runMigrations } from '../db/migrate.js';
|
|
import { setupAuth } from './auth.js';
|
|
import type { AuthConfig } from '../config.js';
|
|
|
|
const AUTH: AuthConfig = {
|
|
sessionSecret: 'test', sessionMaxAge: 3600_000, secureCookie: false,
|
|
adminEmails: [], providers: {}, local: { enabled: true, allowSignup: true },
|
|
};
|
|
|
|
describe('hasLocalCredential on the session user', () => {
|
|
let tempDir = '';
|
|
let repo: Repository;
|
|
let app: Express;
|
|
|
|
beforeAll(() => {
|
|
tempDir = mkdtempSync(join(tmpdir(), 'maestro-haslocalcred-'));
|
|
repo = new Repository(join(tempDir, 'orchestrator.db'));
|
|
runMigrations(repo.getDb());
|
|
|
|
app = express();
|
|
const auth = setupAuth(repo, AUTH);
|
|
app.use(auth.sessionMiddleware);
|
|
app.use(auth.passportInit);
|
|
app.use(auth.passportSession);
|
|
app.use('/auth', auth.authRouter);
|
|
app.get('/api/auth/me', (req, res) => {
|
|
if (!req.isAuthenticated() || !req.user) { res.status(401).json({ error: 'unauthenticated' }); return; }
|
|
res.json(req.user);
|
|
});
|
|
// Test-only login shortcut: bypasses password verification so a test can
|
|
// put an arbitrary existing (e.g. OAuth-created) user id into the session
|
|
// and exercise passport.deserializeUser on the next request — the same
|
|
// code path a real OAuth callback's req.login would take.
|
|
app.post('/test/login/:id', (req, res, next) => {
|
|
req.login({ id: req.params.id } as Express.User, (err) => {
|
|
if (err) { next(err); return; }
|
|
res.sendStatus(204);
|
|
});
|
|
});
|
|
});
|
|
|
|
afterAll(() => {
|
|
repo.close();
|
|
if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = ''; }
|
|
});
|
|
|
|
it('is true after a local login', async () => {
|
|
repo.createLocalUser({ email: 'local-cred@x.com', password: 'pw12345678', role: 'user', status: 'active' });
|
|
const agent = request.agent(app);
|
|
await agent.post('/auth/local').type('form').send({ email: 'local-cred@x.com', password: 'pw12345678' });
|
|
const res = await agent.get('/api/auth/me');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.hasLocalCredential).toBe(true);
|
|
// secret material must never leak onto the session user
|
|
expect(res.body.password).toBeUndefined();
|
|
expect(res.body.passwordHash).toBeUndefined();
|
|
});
|
|
|
|
it('is false for an OAuth-only account', async () => {
|
|
const oauthUser = repo.findOrCreateUserByOAuth({
|
|
provider: 'gitea',
|
|
providerId: 'gitea-123',
|
|
email: 'oauth@x.com',
|
|
name: 'OAuth User',
|
|
avatarUrl: undefined,
|
|
});
|
|
repo.updateUser(oauthUser.id, { status: 'active' });
|
|
const agent = request.agent(app);
|
|
await agent.post(`/test/login/${oauthUser.id}`);
|
|
const res = await agent.get('/api/auth/me');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.hasLocalCredential).toBe(false);
|
|
});
|
|
});
|