Ctrl K
ComunidadGuías…

lib/middleware/access-control.test.ts

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { Hono } from 'hono';
import { afterEach, describe, expect, it, vi } from 'vitest';

import md5 from '@/utils/md5';
import type { WeiboOAuthState } from '@/utils/weibo-oauth';

process.env.NODE_NAME = 'mock';

async function checkBlock(response) {
    expect(response.status).toBe(403);
    expect(await response.text()).toMatch(/Access denied\./);
}

afterEach(() => {
    delete process.env.ACCESS_KEY;
    vi.resetModules();
});

describe('access-control', () => {
    it('access key', async () => {
        const key = '1L0veRSSHub';
        const code = md5('/test/2' + key);
        process.env.ACCESS_KEY = key;
        const app = (await import('@/app')).default;

        const response01 = await app.request('/');
        expect(response01.status).toBe(200);

        const response02 = await app.request('/robots.txt');
        expect(response02.status).toBe(404);

        // no key/code
        const response21 = await app.request('/test/2');
        await checkBlock(response21);

        // wrong key/code
        const response321 = await app.request(`/test/2?key=wrong+${key}`);
        await checkBlock(response321);

        const response322 = await app.request(`/test/2?code=wrong+${code}`);
        await checkBlock(response322);

        // right key/code
        const response331 = await app.request(`/test/2?key=${key}`);
        expect(response331.status).toBe(200);

        const response332 = await app.request(`/test/2?code=${code}`);
        expect(response332.status).toBe(200);
    });
});

const setupProtectedWeiboOAuth = async () => {
    process.env.ACCESS_KEY = 'fake-test-key';
    const { config } = await import('@/config');
    const cache = (await import('@/utils/cache')).default;
    const { default: accessControl } = await import('@/middleware/access-control');
    const { createWeiboOAuthState } = await import('@/utils/weibo-oauth');
    const app = new Hono<{ Variables: { weiboOAuthState: WeiboOAuthState } }>();
    app.onError((error, ctx) => ctx.text(error.message, error.name === 'RejectError' ? 403 : 500));
    app.use('*', accessControl);
    app.get('/authorize', async (ctx) => ctx.text(await createWeiboOAuthState(ctx, '2', 'displayVideo=1')));
    app.get('/weibo/timeline/0', (ctx) => {
        const state = ctx.get('weiboOAuthState');
        return state ? ctx.json(state) : ctx.text('initial authorization');
    });
    return { app, cache, config };
};

describe('protected Weibo OAuth', () => {
    it('rejects anonymous authorization and unknown callback states', async () => {
        const { app } = await setupProtectedWeiboOAuth();
        expect((await app.request('/authorize')).status).toBe(403);
        expect((await app.request('/weibo/timeline/0?code=fake-oauth-code&state=not-a-state')).status).toBe(403);
    });

    it('allows a valid access signature to initiate authorization on the callback path', async () => {
        const { app, config } = await setupProtectedWeiboOAuth();
        const response = await app.request(`/weibo/timeline/0?code=${md5('/weibo/timeline/0' + config.accessKey)}`);
        expect(response.status).toBe(200);
        expect(await response.text()).toBe('initial authorization');
    });

    it('consumes callback state once, including concurrent callbacks', async () => {
        const { app } = await setupProtectedWeiboOAuth();
        const authorization = await app.request('/authorize?key=fake-test-key');
        expect(authorization.status).toBe(200);
        const state = await authorization.text();
        expect(state).not.toContain('fake-test-key');
        const responses = await Promise.all([app.request(`/weibo/timeline/0?code=fake-oauth-code&state=${state}`), app.request(`/weibo/timeline/0?code=fake-oauth-code&state=${state}`)]);
        expect(responses.map((response) => response.status).toSorted((left, right) => left - right)).toEqual([200, 403]);
        const response = responses.find((response) => response.status === 200)!;
        expect(await response.json()).toMatchObject({ feature: '2', routeParams: 'displayVideo=1' });
        expect((await app.request(`/weibo/timeline/0?code=fake-oauth-code&state=${state}`)).status).toBe(403);
    });

    it('rejects expired states and states issued before access-key rotation', async () => {
        const { app, cache, config } = await setupProtectedWeiboOAuth();
        const expiredState = await (await app.request('/authorize?key=fake-test-key')).text();
        const key = `weibo:oauth:state:${expiredState}`;
        const stored = JSON.parse((await cache.globalCache.get(key))!);
        await cache.globalCache.set(key, { ...stored, expiresAt: Date.now() - 1 }, 600);
        expect((await app.request(`/weibo/timeline/0?code=fake-oauth-code&state=${expiredState}`)).status).toBe(403);
        const previousState = await (await app.request('/authorize?key=fake-test-key')).text();
        config.accessKey = 'rotated-fake-test-key';
        expect((await app.request(`/weibo/timeline/0?code=fake-oauth-code&state=${previousState}`)).status).toBe(403);
    });

    it('fails closed when the cache cannot guarantee atomic claims', async () => {
        const { app, cache } = await setupProtectedWeiboOAuth();
        cache.globalCache.supportsAtomicClaims = false;
        const response = await app.request('/authorize?key=fake-test-key');
        expect(response.status).toBe(500);
        expect(await response.text()).toContain('memory or Redis cache');
        expect((await app.request('/weibo/timeline/0?code=fake-oauth-code&state=00000000-0000-0000-0000-000000000000')).status).toBe(403);
    });
});