mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
Merge 57293bd243 into 44ecf41ca6
This commit is contained in:
commit
712d28b145
39
admin/inertia/hooks/useDebounce.test.ts
Normal file
39
admin/inertia/hooks/useDebounce.test.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import useDebounce from './useDebounce'
|
||||
|
||||
describe('useDebounce', () => {
|
||||
beforeEach(() => {
|
||||
// Freeze the system time so we can control the milliseconds
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore normal time after the test
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should debounce function calls', () => {
|
||||
// renderHook is the safe way to test React hooks in isolation
|
||||
const { result } = renderHook(() => useDebounce())
|
||||
const mockFn = vi.fn()
|
||||
|
||||
const debouncedFn = result.current.debounce(mockFn, 500)
|
||||
|
||||
// Call the function 3 times in a row very quickly
|
||||
debouncedFn()
|
||||
debouncedFn()
|
||||
debouncedFn()
|
||||
|
||||
// Advance time by 499ms - The function should not have been executed yet
|
||||
vi.advanceTimersByTime(499)
|
||||
expect(mockFn).not.toHaveBeenCalled()
|
||||
|
||||
// Advance the remaining 1ms (totaling 500ms)
|
||||
vi.advanceTimersByTime(1)
|
||||
|
||||
// The original function must be called EXACTLY ONCE
|
||||
expect(mockFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
59
admin/inertia/hooks/useDiskDisplayData.test.ts
Normal file
59
admin/inertia/hooks/useDiskDisplayData.test.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { getAllDiskDisplayItems, getPrimaryDiskInfo } from './useDiskDisplayData'
|
||||
import { NomadDiskInfo } from '../../types/system'
|
||||
|
||||
// Mock data to simulate disks
|
||||
const mockDisks = [
|
||||
{
|
||||
name: 'sda', totalSize: 1000, totalUsed: 500, percentUsed: 50,
|
||||
filesystems: [{ mount: '/boot', fs: '/dev/sda1', used: 10, size: 100, percentUsed: 10 }]
|
||||
},
|
||||
{
|
||||
name: 'nvme0n1', totalSize: 5000, totalUsed: 4000, percentUsed: 80,
|
||||
filesystems: [{ mount: '/', fs: '/dev/nvme0n1p1', used: 4000, size: 5000, percentUsed: 80 }]
|
||||
}
|
||||
] as NomadDiskInfo[]
|
||||
|
||||
const mockFsSize = [
|
||||
{ fs: '/dev/sda1', size: 1000, used: 500, use: 50 },
|
||||
{ fs: 'tmpfs', size: 200, used: 10, use: 5 }
|
||||
] as any
|
||||
|
||||
describe('useDiskDisplayData', () => {
|
||||
describe('getAllDiskDisplayItems', () => {
|
||||
it('should return empty array if no data is provided', () => {
|
||||
expect(getAllDiskDisplayItems(undefined, undefined)).toEqual([])
|
||||
})
|
||||
|
||||
it('should map NomadDiskInfo correctly and calculate formatBytes', () => {
|
||||
const result = getAllDiskDisplayItems(mockDisks, undefined)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].label).toBe('sda')
|
||||
expect(result[1].label).toBe('nvme0n1')
|
||||
expect(result[1].value).toBe(80)
|
||||
})
|
||||
|
||||
it('should fallback to fsSize if disks array is empty', () => {
|
||||
const result = getAllDiskDisplayItems([], mockFsSize)
|
||||
// Should filter out tmpfs and only keep physical devices (/dev/)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].label).toBe('/dev/sda1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPrimaryDiskInfo', () => {
|
||||
it('should return null if no data is provided', () => {
|
||||
expect(getPrimaryDiskInfo(undefined, undefined)).toBeNull()
|
||||
})
|
||||
|
||||
it('should return the disk mounted at root (/)', () => {
|
||||
const result = getPrimaryDiskInfo(mockDisks, undefined)
|
||||
expect(result).toEqual({ totalSize: 5000, totalUsed: 4000 })
|
||||
})
|
||||
|
||||
it('should fallback to fsSize if disks is empty', () => {
|
||||
const result = getPrimaryDiskInfo([], mockFsSize)
|
||||
expect(result).toEqual({ totalSize: 1000, totalUsed: 500 })
|
||||
})
|
||||
})
|
||||
})
|
||||
16
admin/inertia/lib/classNames.test.ts
Normal file
16
admin/inertia/lib/classNames.test.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import classNames from './classNames'
|
||||
|
||||
describe('classNames', () => {
|
||||
it('should join multiple valid class strings with a space', () => {
|
||||
expect(classNames('btn', 'btn-primary', 'active')).toBe('btn btn-primary active')
|
||||
})
|
||||
|
||||
it('should filter out undefined, null, and empty values', () => {
|
||||
expect(classNames('container', undefined, 'mx-auto', null as any, '', 'p-4')).toBe('container mx-auto p-4')
|
||||
})
|
||||
|
||||
it('should handle empty arguments without breaking', () => {
|
||||
expect(classNames()).toBe('')
|
||||
})
|
||||
})
|
||||
103
admin/inertia/lib/util.test.ts
Normal file
103
admin/inertia/lib/util.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import {
|
||||
setGlobalNotificationCallback,
|
||||
capitalizeFirstLetter,
|
||||
formatBytes,
|
||||
generateRandomString,
|
||||
generateUUID,
|
||||
extractFileName,
|
||||
catchInternal
|
||||
} from './util'
|
||||
|
||||
describe('util', () => {
|
||||
describe('capitalizeFirstLetter', () => {
|
||||
it('should capitalize the first letter of a string', () => {
|
||||
expect(capitalizeFirstLetter('nomad')).toBe('Nomad')
|
||||
expect(capitalizeFirstLetter('PROJECT')).toBe('PROJECT')
|
||||
})
|
||||
|
||||
it('should handle empty or null values safely', () => {
|
||||
expect(capitalizeFirstLetter('')).toBe('')
|
||||
expect(capitalizeFirstLetter(null)).toBe('')
|
||||
expect(capitalizeFirstLetter(undefined)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('should format bytes into human readable sizes', () => {
|
||||
expect(formatBytes(0)).toBe('0 Bytes')
|
||||
expect(formatBytes(1024)).toBe('1 KB')
|
||||
expect(formatBytes(1048576)).toBe('1 MB')
|
||||
})
|
||||
|
||||
it('should respect decimal places', () => {
|
||||
expect(formatBytes(1500, 2)).toBe('1.46 KB')
|
||||
expect(formatBytes(1500, 0)).toBe('1 KB')
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateRandomString', () => {
|
||||
it('should generate a string of the exact specified length', () => {
|
||||
expect(generateRandomString(10)).toHaveLength(10)
|
||||
expect(generateRandomString(0)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateUUID', () => {
|
||||
it('should generate a valid UUID v4 format string', () => {
|
||||
const uuid = generateUUID()
|
||||
// Regex UUID
|
||||
expect(uuid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractFileName', () => {
|
||||
it('should extract filename from unix-style paths', () => {
|
||||
expect(extractFileName('/storage/zim/wikipedia.zim')).toBe('wikipedia.zim')
|
||||
})
|
||||
|
||||
it('should extract filename from windows-style paths', () => {
|
||||
expect(extractFileName('C:\\Users\\nomad\\downloads\\map.pmtiles')).toBe('map.pmtiles')
|
||||
})
|
||||
|
||||
it('should return the original string if no path separators exist', () => {
|
||||
expect(extractFileName('just-a-file.pdf')).toBe('just-a-file.pdf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('catchInternal', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
setGlobalNotificationCallback(null as any)
|
||||
})
|
||||
|
||||
it('should return the result of the wrapped function if successful', async () => {
|
||||
const fn = vi.fn().mockResolvedValue('success data')
|
||||
const wrapped = catchInternal(fn)
|
||||
const result = await wrapped()
|
||||
|
||||
expect(result).toBe('success data')
|
||||
expect(fn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should catch errors, log to console, and trigger global notification', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const notificationMock = vi.fn()
|
||||
setGlobalNotificationCallback(notificationMock)
|
||||
|
||||
const fn = vi.fn().mockRejectedValue(new Error('API Timeout'))
|
||||
const wrapped = catchInternal(fn)
|
||||
const result = await wrapped()
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
expect(consoleSpy).toHaveBeenCalledWith('Internal error caught:', expect.any(Error))
|
||||
expect(notificationMock).toHaveBeenCalledWith({
|
||||
message: expect.stringContaining('API Timeout'),
|
||||
type: 'error',
|
||||
duration: 5000
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
44
admin/tests/unit/models/kv_store.spec.ts
Normal file
44
admin/tests/unit/models/kv_store.spec.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { test } from '@japa/runner'
|
||||
import KVStore from '../../../app/models/kv_store.js'
|
||||
|
||||
test.group('Models | KVStore (Mocked Database)', (group) => {
|
||||
let originalFindBy: any
|
||||
let originalFirstOrCreate: any
|
||||
|
||||
group.each.setup(() => {
|
||||
// Save original database functions to restore later
|
||||
originalFindBy = KVStore.findBy
|
||||
originalFirstOrCreate = KVStore.firstOrCreate
|
||||
})
|
||||
|
||||
group.each.teardown(() => {
|
||||
// Restore to avoid polluting other test suites
|
||||
KVStore.findBy = originalFindBy
|
||||
KVStore.firstOrCreate = originalFirstOrCreate
|
||||
})
|
||||
|
||||
test('getValue: should return null when the key does not exist in the database', async ({ assert }) => {
|
||||
// Mock the database returning nothing
|
||||
KVStore.findBy = async () => null
|
||||
|
||||
const result = await KVStore.getValue('system.updateAvailable' as any)
|
||||
assert.isNull(result)
|
||||
})
|
||||
|
||||
test('clearValue: should set the value to null and invoke save() on the record', async ({ assert }) => {
|
||||
let saveCalled = false
|
||||
|
||||
// Mock the record returned by the database
|
||||
const mockDbRecord = {
|
||||
value: 'old_configuration',
|
||||
save: async () => { saveCalled = true }
|
||||
}
|
||||
|
||||
KVStore.findBy = async () => (mockDbRecord as any)
|
||||
|
||||
await KVStore.clearValue('some_key' as any)
|
||||
|
||||
assert.isNull(mockDbRecord.value)
|
||||
assert.isTrue(saveCalled) // Ensures the null value was persisted
|
||||
})
|
||||
})
|
||||
39
admin/tests/unit/services/chat_service.spec.ts
Normal file
39
admin/tests/unit/services/chat_service.spec.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { test } from '@japa/runner'
|
||||
import { ChatService } from '../../../app/services/chat_service.js'
|
||||
import { OllamaService } from '../../../app/services/ollama_service.js'
|
||||
|
||||
test.group('Services | ChatService (Mocked)', () => {
|
||||
test('getChatSuggestions: should return an empty array if there are no models in Ollama', async ({ assert }) => {
|
||||
// Mock OllamaService returning zero models
|
||||
const mockOllamaService = {
|
||||
getModels: async () => []
|
||||
} as unknown as OllamaService
|
||||
|
||||
const chatService = new ChatService(mockOllamaService)
|
||||
const suggestions = await chatService.getChatSuggestions()
|
||||
|
||||
assert.deepEqual(suggestions, [])
|
||||
})
|
||||
|
||||
test('getChatSuggestions: should extract, clean and format suggestions generated by the LLM', async ({ assert }) => {
|
||||
// Simulate a scenario where Ollama responds to the request
|
||||
const mockOllamaService = {
|
||||
getModels: async () => [{ name: 'llama3:8b', size: 4000000000 }],
|
||||
chat: async () => ({
|
||||
message: {
|
||||
// Simulating dirty LLM output with list markers
|
||||
content: '1. How to create a game in Python\n2. What is Docker?\n3. Artisanal burger recipe'
|
||||
}
|
||||
})
|
||||
} as unknown as OllamaService
|
||||
|
||||
const chatService = new ChatService(mockOllamaService)
|
||||
const suggestions = await chatService.getChatSuggestions()
|
||||
|
||||
// Verify regex and toTitleCase utility logic
|
||||
assert.lengthOf(suggestions, 3)
|
||||
assert.equal(suggestions[0], 'How To Create A Game In Python')
|
||||
assert.equal(suggestions[1], 'What Is Docker?')
|
||||
assert.equal(suggestions[2], 'Artisanal Burger Recipe')
|
||||
})
|
||||
})
|
||||
56
admin/tests/unit/services/system_service.spec.ts
Normal file
56
admin/tests/unit/services/system_service.spec.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { test } from '@japa/runner'
|
||||
import { SystemService } from '../../../app/services/system_service.js'
|
||||
import { DockerService } from '../../../app/services/docker_service.js'
|
||||
import axios from 'axios'
|
||||
|
||||
test.group('Services | SystemService (Mocked)', (group) => {
|
||||
let originalAxiosGet: any
|
||||
let originalAxiosPost: any
|
||||
|
||||
group.each.setup(() => {
|
||||
// Save original Axios functions
|
||||
originalAxiosGet = axios.get
|
||||
originalAxiosPost = axios.post
|
||||
})
|
||||
|
||||
group.each.teardown(() => {
|
||||
// Restore original functions
|
||||
axios.get = originalAxiosGet
|
||||
axios.post = originalAxiosPost
|
||||
})
|
||||
|
||||
test('getInternetStatus: should return true if the request returns status 200', async ({ assert }) => {
|
||||
// Simulate Cloudflare URL (1.1.1.1) responding perfectly
|
||||
axios.get = async () => ({ status: 200 }) as any
|
||||
|
||||
// Inject empty DockerService mock
|
||||
const mockDockerService = {} as DockerService
|
||||
const systemService = new SystemService(mockDockerService)
|
||||
|
||||
const isOnline = await systemService.getInternetStatus()
|
||||
assert.isTrue(isOnline)
|
||||
})
|
||||
|
||||
test('subscribeToReleaseNotes: should return success when the external API responds with 200', async ({ assert }) => {
|
||||
// Simulate projectnomad.us API returning success
|
||||
axios.post = async () => ({ status: 200 }) as any
|
||||
|
||||
const systemService = new SystemService({} as DockerService)
|
||||
const response = await systemService.subscribeToReleaseNotes('jv@example.com')
|
||||
|
||||
assert.isTrue(response.success)
|
||||
assert.equal(response.message, 'Successfully subscribed to release notes')
|
||||
})
|
||||
|
||||
test('subscribeToReleaseNotes: should gracefully handle network failures', async ({ assert }) => {
|
||||
// Simulate network error
|
||||
axios.post = async () => { throw new Error('Network Error') }
|
||||
|
||||
const systemService = new SystemService({} as DockerService)
|
||||
const response = await systemService.subscribeToReleaseNotes('jv@example.com')
|
||||
|
||||
// The service should not crash, it should catch the error and return false
|
||||
assert.isFalse(response.success)
|
||||
assert.include(response.message, 'Failed to subscribe:')
|
||||
})
|
||||
})
|
||||
24
admin/tests/unit/utils/fs.spec.ts
Normal file
24
admin/tests/unit/utils/fs.spec.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { test } from '@japa/runner'
|
||||
import { determineFileType, sanitizeFilename, matchesDevice } from '../../../app/utils/fs.js'
|
||||
|
||||
test.group('Utils | fs (pure functions)', () => {
|
||||
test('determineFileType: should classify the file by extension case-insensitively', ({ assert }) => {
|
||||
assert.equal(determineFileType('document.pdf'), 'pdf')
|
||||
assert.equal(determineFileType('image.JPG'), 'image')
|
||||
assert.equal(determineFileType('library.zim'), 'zim')
|
||||
assert.equal(determineFileType('notes.md'), 'text')
|
||||
assert.equal(determineFileType('unknown_file.xyz'), 'unknown')
|
||||
})
|
||||
|
||||
test('sanitizeFilename: should replace dangerous characters with underscore', ({ assert }) => {
|
||||
assert.equal(sanitizeFilename('my file!.txt'), 'my_file_.txt')
|
||||
assert.equal(sanitizeFilename('../hidden/file.txt'), '.._hidden_file.txt')
|
||||
assert.equal(sanitizeFilename('safe-file_name.123.pdf'), 'safe-file_name.123.pdf')
|
||||
})
|
||||
|
||||
test('matchesDevice: should match system paths with block device names', ({ assert }) => {
|
||||
assert.isTrue(matchesDevice('/dev/sda1', 'sda1'))
|
||||
assert.isTrue(matchesDevice('/dev/mapper/ubuntu--vg-ubuntu--lv', 'ubuntu--lv'))
|
||||
assert.isFalse(matchesDevice('/dev/sda1', 'sdb1'))
|
||||
})
|
||||
})
|
||||
31
admin/tests/unit/utils/misc.spec.ts
Normal file
31
admin/tests/unit/utils/misc.spec.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { test } from '@japa/runner'
|
||||
import { formatSpeed, toTitleCase, parseBoolean } from '../../../app/utils/misc.js'
|
||||
|
||||
test.group('Utils | misc', () => {
|
||||
test('formatSpeed: should format bytes correctly', ({ assert }) => {
|
||||
assert.equal(formatSpeed(500), '500 B/s')
|
||||
assert.equal(formatSpeed(1024), '1.0 KB/s')
|
||||
assert.equal(formatSpeed(1536), '1.5 KB/s')
|
||||
assert.equal(formatSpeed(1048576), '1.0 MB/s')
|
||||
})
|
||||
|
||||
test('toTitleCase: should capitalize the first letter of each word', ({ assert }) => {
|
||||
assert.equal(toTitleCase('hello world'), 'Hello World')
|
||||
assert.equal(toTitleCase('PROJECT NOMAD'), 'Project Nomad')
|
||||
assert.equal(toTitleCase('cOMmAnD cEnTeR'), 'Command Center')
|
||||
})
|
||||
|
||||
test('parseBoolean: should convert various types to boolean', ({ assert }) => {
|
||||
assert.isTrue(parseBoolean(true))
|
||||
assert.isTrue(parseBoolean('true'))
|
||||
assert.isTrue(parseBoolean('1'))
|
||||
assert.isTrue(parseBoolean(1))
|
||||
|
||||
assert.isFalse(parseBoolean(false))
|
||||
assert.isFalse(parseBoolean('false'))
|
||||
assert.isFalse(parseBoolean('0'))
|
||||
assert.isFalse(parseBoolean(0))
|
||||
assert.isFalse(parseBoolean(null))
|
||||
assert.isFalse(parseBoolean(undefined))
|
||||
})
|
||||
})
|
||||
28
admin/tests/unit/utils/version.spec.ts
Normal file
28
admin/tests/unit/utils/version.spec.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { test } from '@japa/runner'
|
||||
import { isNewerVersion, parseMajorVersion } from '../../../app/utils/version.js'
|
||||
|
||||
test.group('Utils | version', () => {
|
||||
test('parseMajorVersion: should extract the major version ignoring the v prefix', ({ assert }) => {
|
||||
assert.equal(parseMajorVersion('v3.8.1'), 3)
|
||||
assert.equal(parseMajorVersion('10.19.4'), 10)
|
||||
assert.equal(parseMajorVersion('invalid'), 0)
|
||||
})
|
||||
|
||||
test('isNewerVersion: should compare standard versions correctly', ({ assert }) => {
|
||||
assert.isTrue(isNewerVersion('1.25.0', '1.24.0'))
|
||||
assert.isTrue(isNewerVersion('2.0.0', '1.9.9'))
|
||||
assert.isFalse(isNewerVersion('1.24.0', '1.25.0'))
|
||||
assert.isFalse(isNewerVersion('1.0.0', '1.0.0'))
|
||||
})
|
||||
|
||||
test('isNewerVersion: should handle pre-release (RC) logic', ({ assert }) => {
|
||||
assert.isTrue(isNewerVersion('1.0.0', '1.0.0-rc.1', true))
|
||||
assert.isFalse(isNewerVersion('1.0.0-rc.1', '1.0.0', true))
|
||||
assert.isTrue(isNewerVersion('1.0.0-rc.2', '1.0.0-rc.1', true))
|
||||
assert.isFalse(isNewerVersion('1.0.0-rc.1', '1.0.0-rc.2', true))
|
||||
})
|
||||
|
||||
test('isNewerVersion: should ignore pre-releases when includePreReleases is false', ({ assert }) => {
|
||||
assert.isFalse(isNewerVersion('1.0.1-rc.1', '1.0.0'))
|
||||
})
|
||||
})
|
||||
29
admin/tests/unit/validators/chat.spec.ts
Normal file
29
admin/tests/unit/validators/chat.spec.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { test } from '@japa/runner'
|
||||
import { createSessionSchema, addMessageSchema } from '../../../app/validators/chat.js'
|
||||
|
||||
test.group('Validators | chat', () => {
|
||||
test('createSessionSchema: should validate correct payload', async ({ assert }) => {
|
||||
const payload = { title: 'New Chat', model: 'llama3' }
|
||||
const result = await createSessionSchema.validate(payload)
|
||||
|
||||
assert.equal(result.title, 'New Chat')
|
||||
assert.equal(result.model, 'llama3')
|
||||
})
|
||||
|
||||
test('createSessionSchema: should fail without title', async ({ assert }) => {
|
||||
const payload = { model: 'llama3' }
|
||||
await assert.rejects(() => createSessionSchema.validate(payload))
|
||||
})
|
||||
|
||||
test('addMessageSchema: should validate correct roles', async ({ assert }) => {
|
||||
await assert.doesNotReject(() => addMessageSchema.validate({ role: 'user', content: 'Hello' }))
|
||||
await assert.doesNotReject(() => addMessageSchema.validate({ role: 'assistant', content: 'Hi' }))
|
||||
await assert.doesNotReject(() => addMessageSchema.validate({ role: 'system', content: 'Prompt' }))
|
||||
})
|
||||
|
||||
test('addMessageSchema: should fail with invalid role or empty content', async ({ assert }) => {
|
||||
await assert.rejects(() => addMessageSchema.validate({ role: 'admin', content: 'Hello' }))
|
||||
await assert.rejects(() => addMessageSchema.validate({ role: 'user', content: ' ' }))
|
||||
await assert.rejects(() => addMessageSchema.validate({ role: 'user' }))
|
||||
})
|
||||
})
|
||||
27
admin/tests/unit/validators/common.spec.ts
Normal file
27
admin/tests/unit/validators/common.spec.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { test } from '@japa/runner'
|
||||
import { assertNotPrivateUrl } from '../../../app/validators/common.js'
|
||||
|
||||
test.group('Validators | common | SSRF Protection', () => {
|
||||
test('assertNotPrivateUrl: should block loopback addresses', ({ assert }) => {
|
||||
assert.throws(() => assertNotPrivateUrl('http://localhost:8080/file.zim'))
|
||||
assert.throws(() => assertNotPrivateUrl('http://127.0.0.1/api'))
|
||||
assert.throws(() => assertNotPrivateUrl('http://0.0.0.0/test'))
|
||||
assert.throws(() => assertNotPrivateUrl('http://[::1]/'))
|
||||
})
|
||||
|
||||
test('assertNotPrivateUrl: should block link-local and cloud metadata addresses', ({ assert }) => {
|
||||
assert.throws(() => assertNotPrivateUrl('http://169.254.169.254/latest/meta-data/'))
|
||||
assert.throws(() => assertNotPrivateUrl('http://fe80::1ff:fe23:4567:890a/'))
|
||||
})
|
||||
|
||||
test('assertNotPrivateUrl: should allow local network IP addresses (RFC1918)', ({ assert }) => {
|
||||
assert.doesNotThrow(() => assertNotPrivateUrl('http://192.168.1.100:8080/file.zim'))
|
||||
assert.doesNotThrow(() => assertNotPrivateUrl('http://10.0.0.5/data'))
|
||||
assert.doesNotThrow(() => assertNotPrivateUrl('http://172.16.0.10/'))
|
||||
})
|
||||
|
||||
test('assertNotPrivateUrl: should allow normal external domains', ({ assert }) => {
|
||||
assert.doesNotThrow(() => assertNotPrivateUrl('https://download.kiwix.org/zim/wikipedia.zim'))
|
||||
assert.doesNotThrow(() => assertNotPrivateUrl('http://meu-nas-local:8080/file'))
|
||||
})
|
||||
})
|
||||
15
admin/tests/unit/validators/settings.spec.ts
Normal file
15
admin/tests/unit/validators/settings.spec.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { test } from '@japa/runner'
|
||||
import { updateSettingSchema } from '../../../app/validators/settings.js'
|
||||
import { SETTINGS_KEYS } from '../../../constants/kv_store.js'
|
||||
|
||||
test.group('Validators | settings', () => {
|
||||
test('updateSettingSchema: should validate using a valid system key', async ({ assert }) => {
|
||||
const validKey = SETTINGS_KEYS[0]
|
||||
await assert.doesNotReject(() => updateSettingSchema.validate({ key: validKey, value: 'some value' }))
|
||||
await assert.doesNotReject(() => updateSettingSchema.validate({ key: validKey }))
|
||||
})
|
||||
|
||||
test('updateSettingSchema: should fail with an invalid key', async ({ assert }) => {
|
||||
await assert.rejects(() => updateSettingSchema.validate({ key: 'INVALID_KEY', value: '123' }))
|
||||
})
|
||||
})
|
||||
1990
package-lock.json
generated
1990
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -7,5 +7,10 @@
|
|||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Crosstalk Solutions, LLC",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"jsdom": "^29.0.1",
|
||||
"vitest": "^4.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user