Add admin/image domain separation with host isolation middleware, optional hide-folder public URLs, GitHub release update check, and Lucky/Nginx deployment docs. Switch license to GPL-3.0-only.
258 lines
7.6 KiB
TypeScript
258 lines
7.6 KiB
TypeScript
import { describe, expect, it, vi, afterEach } from 'vitest'
|
|
import type { H3Event } from 'h3'
|
|
import {
|
|
getApiUploadToken,
|
|
getApiUploadTokenFromEnv,
|
|
getApiUploadTokenSource,
|
|
getAllowedRefererHostsRaw,
|
|
getAutoDeleteDays,
|
|
getAutoDeleteDaysSource,
|
|
getRefererSource,
|
|
getSiteBaseUrl,
|
|
getSiteBaseUrlConfigured,
|
|
getSiteBaseUrlSource,
|
|
getWebpQuality,
|
|
getWebpQualitySource,
|
|
isApiUploadTokenEnvConfigured,
|
|
isAutoDeleteDaysEnvConfigured,
|
|
isRefererEnvConfigured,
|
|
isWebpQualityEnvConfigured,
|
|
validateDomainSeparationPair
|
|
} from './env'
|
|
import {
|
|
getSetting,
|
|
SETTINGS_ALLOWED_REFERER_HOSTS,
|
|
SETTINGS_API_UPLOAD_TOKEN,
|
|
SETTINGS_AUTO_DELETE_DAYS,
|
|
SETTINGS_SITE_BASE_URL,
|
|
SETTINGS_WEBP_QUALITY,
|
|
generateApiUploadToken
|
|
} from './db'
|
|
|
|
vi.mock('./db', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('./db')>()
|
|
return {
|
|
...actual,
|
|
getSetting: vi.fn(),
|
|
setSetting: vi.fn()
|
|
}
|
|
})
|
|
|
|
vi.stubGlobal('useRuntimeConfig', () => ({
|
|
apiUploadToken: '',
|
|
siteBaseUrl: '',
|
|
imageBaseUrl: ''
|
|
}))
|
|
|
|
vi.stubGlobal('getRequestURL', () => new URL('https://fallback.example.com'))
|
|
|
|
function mockEvent(): H3Event {
|
|
return {
|
|
node: { req: {}, res: {} },
|
|
context: {}
|
|
} as H3Event
|
|
}
|
|
|
|
describe('getApiUploadToken priority', () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
delete process.env.API_UPLOAD_TOKEN
|
|
delete process.env.NUXT_API_UPLOAD_TOKEN
|
|
})
|
|
|
|
it('prefers environment variable over database', () => {
|
|
process.env.API_UPLOAD_TOKEN = 'env-token'
|
|
vi.mocked(getSetting).mockReturnValue('db-token')
|
|
|
|
const event = mockEvent()
|
|
expect(getApiUploadToken(event)).toBe('env-token')
|
|
expect(getApiUploadTokenSource(event)).toBe('env')
|
|
expect(isApiUploadTokenEnvConfigured(event)).toBe(true)
|
|
})
|
|
|
|
it('falls back to database when env is unset', () => {
|
|
vi.mocked(getSetting).mockImplementation(key =>
|
|
key === SETTINGS_API_UPLOAD_TOKEN ? 'db-token' : null
|
|
)
|
|
|
|
const event = mockEvent()
|
|
expect(getApiUploadToken(event)).toBe('db-token')
|
|
expect(getApiUploadTokenSource(event)).toBe('db')
|
|
expect(isApiUploadTokenEnvConfigured(event)).toBe(false)
|
|
})
|
|
|
|
it('returns empty when neither env nor db is configured', () => {
|
|
vi.mocked(getSetting).mockReturnValue(null)
|
|
|
|
const event = mockEvent()
|
|
expect(getApiUploadToken(event)).toBe('')
|
|
expect(getApiUploadTokenSource(event)).toBe('none')
|
|
})
|
|
})
|
|
|
|
describe('generateApiUploadToken', () => {
|
|
it('returns a 64-char hex string', () => {
|
|
const token = generateApiUploadToken()
|
|
expect(token).toMatch(/^[0-9a-f]{64}$/)
|
|
})
|
|
|
|
it('generates unique tokens', () => {
|
|
const a = generateApiUploadToken()
|
|
const b = generateApiUploadToken()
|
|
expect(a).not.toBe(b)
|
|
})
|
|
})
|
|
|
|
describe('settings storage helpers', () => {
|
|
it('getApiUploadTokenFromEnv reads API_UPLOAD_TOKEN', () => {
|
|
process.env.API_UPLOAD_TOKEN = 'from-env'
|
|
const event = mockEvent()
|
|
expect(getApiUploadTokenFromEnv(event)).toBe('from-env')
|
|
delete process.env.API_UPLOAD_TOKEN
|
|
})
|
|
})
|
|
|
|
describe('getWebpQuality priority', () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
delete process.env.WEBP_QUALITY
|
|
})
|
|
|
|
it('prefers database over environment variable', () => {
|
|
process.env.WEBP_QUALITY = '95'
|
|
vi.mocked(getSetting).mockImplementation(key =>
|
|
key === SETTINGS_WEBP_QUALITY ? '72' : null
|
|
)
|
|
expect(getWebpQuality()).toBe(72)
|
|
expect(getWebpQualitySource()).toBe('db')
|
|
})
|
|
|
|
it('falls back to environment variable when db is unset', () => {
|
|
process.env.WEBP_QUALITY = '95'
|
|
vi.mocked(getSetting).mockReturnValue(null)
|
|
expect(getWebpQuality()).toBe(95)
|
|
expect(getWebpQualitySource()).toBe('env')
|
|
expect(isWebpQualityEnvConfigured()).toBe(true)
|
|
})
|
|
|
|
it('uses default 80 when neither env nor db is configured', () => {
|
|
vi.mocked(getSetting).mockReturnValue(null)
|
|
expect(getWebpQuality()).toBe(80)
|
|
expect(getWebpQualitySource()).toBe('default')
|
|
})
|
|
})
|
|
|
|
describe('getAllowedRefererHostsRaw priority', () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
delete process.env.ALLOWED_REFERER_HOSTS
|
|
})
|
|
|
|
it('prefers database over environment variable', () => {
|
|
process.env.ALLOWED_REFERER_HOSTS = 'blog.example.com'
|
|
vi.mocked(getSetting).mockImplementation(key =>
|
|
key === SETTINGS_ALLOWED_REFERER_HOSTS ? 'wiki.example.com' : null
|
|
)
|
|
expect(getAllowedRefererHostsRaw()).toBe('wiki.example.com')
|
|
expect(getRefererSource()).toBe('db')
|
|
})
|
|
|
|
it('falls back to environment variable when db is unset', () => {
|
|
process.env.ALLOWED_REFERER_HOSTS = 'blog.example.com'
|
|
vi.mocked(getSetting).mockReturnValue(null)
|
|
expect(getAllowedRefererHostsRaw()).toBe('blog.example.com')
|
|
expect(getRefererSource()).toBe('env')
|
|
expect(isRefererEnvConfigured()).toBe(true)
|
|
})
|
|
|
|
it('returns empty when neither env nor db is configured', () => {
|
|
vi.mocked(getSetting).mockReturnValue(null)
|
|
expect(getAllowedRefererHostsRaw()).toBe('')
|
|
expect(getRefererSource()).toBe('none')
|
|
})
|
|
|
|
it('allows database empty string to override env', () => {
|
|
process.env.ALLOWED_REFERER_HOSTS = 'blog.example.com'
|
|
vi.mocked(getSetting).mockImplementation(key =>
|
|
key === SETTINGS_ALLOWED_REFERER_HOSTS ? '' : null
|
|
)
|
|
expect(getAllowedRefererHostsRaw()).toBe('')
|
|
expect(getRefererSource()).toBe('db')
|
|
})
|
|
})
|
|
|
|
describe('getAutoDeleteDays priority', () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
delete process.env.AUTO_DELETE_DAYS
|
|
})
|
|
|
|
it('prefers database over environment variable', () => {
|
|
process.env.AUTO_DELETE_DAYS = '90'
|
|
vi.mocked(getSetting).mockImplementation(key =>
|
|
key === SETTINGS_AUTO_DELETE_DAYS ? '30' : null
|
|
)
|
|
expect(getAutoDeleteDays()).toBe(30)
|
|
expect(getAutoDeleteDaysSource()).toBe('db')
|
|
})
|
|
|
|
it('falls back to environment variable when db is unset', () => {
|
|
process.env.AUTO_DELETE_DAYS = '45'
|
|
vi.mocked(getSetting).mockReturnValue(null)
|
|
expect(getAutoDeleteDays()).toBe(45)
|
|
expect(getAutoDeleteDaysSource()).toBe('env')
|
|
expect(isAutoDeleteDaysEnvConfigured()).toBe(true)
|
|
})
|
|
|
|
it('returns 0 when neither env nor db is configured', () => {
|
|
vi.mocked(getSetting).mockReturnValue(null)
|
|
expect(getAutoDeleteDays()).toBe(0)
|
|
expect(getAutoDeleteDaysSource()).toBe('default')
|
|
})
|
|
})
|
|
|
|
describe('getSiteBaseUrl priority', () => {
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
delete process.env.SITE_BASE_URL
|
|
})
|
|
|
|
it('prefers database over environment variable', () => {
|
|
process.env.SITE_BASE_URL = 'https://env.example.com'
|
|
vi.mocked(getSetting).mockImplementation(key =>
|
|
key === SETTINGS_SITE_BASE_URL ? 'https://db.example.com' : null
|
|
)
|
|
const event = mockEvent()
|
|
expect(getSiteBaseUrlConfigured(event)).toBe('https://db.example.com')
|
|
expect(getSiteBaseUrlSource(event)).toBe('db')
|
|
expect(getSiteBaseUrl(event)).toBe('https://db.example.com')
|
|
})
|
|
|
|
it('falls back to request origin when unset', () => {
|
|
vi.mocked(getSetting).mockReturnValue(null)
|
|
const event = mockEvent()
|
|
expect(getSiteBaseUrl(event)).toBe('https://fallback.example.com')
|
|
expect(getSiteBaseUrlSource(event)).toBe('none')
|
|
})
|
|
})
|
|
|
|
describe('validateDomainSeparationPair', () => {
|
|
it('rejects missing values', () => {
|
|
expect(validateDomainSeparationPair('', 'https://pic.example.com')).toContain('同时填写')
|
|
})
|
|
|
|
it('rejects identical hostnames', () => {
|
|
expect(validateDomainSeparationPair(
|
|
'https://same.example.com',
|
|
'https://same.example.com'
|
|
)).toContain('相同主机名')
|
|
})
|
|
|
|
it('accepts different hostnames', () => {
|
|
expect(validateDomainSeparationPair(
|
|
'https://admin.example.com',
|
|
'https://pic.example.com'
|
|
)).toBeNull()
|
|
})
|
|
})
|