mirror of https://github.com/wg-easy/wg-easy
5 changed files with 993 additions and 0 deletions
@ -15,6 +15,9 @@ |
|||||
"format:check": "prettier . --check", |
"format:check": "prettier . --check", |
||||
"typecheck": "nuxt typecheck", |
"typecheck": "nuxt typecheck", |
||||
"check:all": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm build", |
"check:all": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm build", |
||||
|
"test": "vitest", |
||||
|
"test:ui": "vitest --ui", |
||||
|
"test:run": "vitest run", |
||||
"db:generate": "drizzle-kit generate", |
"db:generate": "drizzle-kit generate", |
||||
"cli:build": "node cli/build.js", |
"cli:build": "node cli/build.js", |
||||
"cli:dev": "tsx cli/index.ts" |
"cli:dev": "tsx cli/index.ts" |
||||
@ -59,6 +62,7 @@ |
|||||
"@types/debug": "^4.1.12", |
"@types/debug": "^4.1.12", |
||||
"@types/phc__format": "^1.0.1", |
"@types/phc__format": "^1.0.1", |
||||
"@types/semver": "^7.7.1", |
"@types/semver": "^7.7.1", |
||||
|
"@vitest/ui": "^2.1.0", |
||||
"drizzle-kit": "^0.31.6", |
"drizzle-kit": "^0.31.6", |
||||
"esbuild": "^0.25.12", |
"esbuild": "^0.25.12", |
||||
"eslint": "^9.39.0", |
"eslint": "^9.39.0", |
||||
@ -67,6 +71,7 @@ |
|||||
"prettier-plugin-tailwindcss": "^0.7.1", |
"prettier-plugin-tailwindcss": "^0.7.1", |
||||
"tsx": "^4.20.6", |
"tsx": "^4.20.6", |
||||
"typescript": "^5.9.3", |
"typescript": "^5.9.3", |
||||
|
"vitest": "^2.1.0", |
||||
"vue-tsc": "^3.1.3" |
"vue-tsc": "^3.1.3" |
||||
}, |
}, |
||||
"packageManager": "[email protected]", |
"packageManager": "[email protected]", |
||||
|
|||||
File diff suppressed because it is too large
@ -0,0 +1,384 @@ |
|||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'; |
||||
|
import type { ClientType } from '#db/repositories/client/types'; |
||||
|
import type { InterfaceType } from '#db/repositories/interface/types'; |
||||
|
import type { UserConfigType } from '#db/repositories/userConfig/types'; |
||||
|
import type { HooksType } from '#db/repositories/hooks/types'; |
||||
|
|
||||
|
// Mock exec function
|
||||
|
const mockExec = vi.fn(); |
||||
|
|
||||
|
// Mock iptablesTemplate function
|
||||
|
const mockIptablesTemplate = vi.fn((template: string) => template); |
||||
|
|
||||
|
// Make WG_ENV and auto-imports available globally for test environment
|
||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
(globalThis as any).WG_ENV = { |
||||
|
EXPERIMENTAL_AWG: false, |
||||
|
OVERRIDE_AUTO_AWG: undefined, |
||||
|
PORT: '51821', |
||||
|
}; |
||||
|
|
||||
|
// Make exec and iptablesTemplate available globally (Nuxt auto-imports simulation)
|
||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
(globalThis as any).exec = mockExec; |
||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
(globalThis as any).iptablesTemplate = mockIptablesTemplate; |
||||
|
|
||||
|
// Mock the module resolution for exec to prevent import errors
|
||||
|
vi.mock('~/server/utils/cmd', () => ({ |
||||
|
exec: mockExec, |
||||
|
})); |
||||
|
|
||||
|
vi.mock('~/server/utils/template', () => ({ |
||||
|
iptablesTemplate: mockIptablesTemplate, |
||||
|
})); |
||||
|
|
||||
|
// Import wgHelper after setting up mocks
|
||||
|
const { wg } = await import('./wgHelper'); |
||||
|
|
||||
|
describe('wgHelper', () => { |
||||
|
const mockClient: Omit<ClientType, 'createdAt' | 'updatedAt'> = { |
||||
|
id: 1, |
||||
|
userId: 1, |
||||
|
interfaceId: 'wg0', |
||||
|
name: 'Test Client', |
||||
|
enabled: true, |
||||
|
ipv4Address: '10.8.0.2', |
||||
|
ipv6Address: 'fd42:42:42::2', |
||||
|
publicKey: 'clientPublicKey123', |
||||
|
preSharedKey: 'preSharedKey123', |
||||
|
privateKey: 'privateKey123', |
||||
|
serverAllowedIps: ['192.168.1.0/24'], |
||||
|
serverEndpoint: '192.168.1.100:51820', |
||||
|
allowedIps: ['0.0.0.0/0', '::/0'], |
||||
|
persistentKeepalive: 25, |
||||
|
mtu: 1420, |
||||
|
dns: ['1.1.1.1', '8.8.8.8'], |
||||
|
expiresAt: null, |
||||
|
preUp: '', |
||||
|
postUp: '', |
||||
|
preDown: '', |
||||
|
postDown: '', |
||||
|
}; |
||||
|
|
||||
|
const mockInterface: InterfaceType = { |
||||
|
name: 'wg0', |
||||
|
device: 'wg0', |
||||
|
enabled: true, |
||||
|
ipv4Cidr: '10.8.0.0/24', |
||||
|
ipv6Cidr: 'fd42:42:42::/64', |
||||
|
port: 51820, |
||||
|
mtu: 1420, |
||||
|
publicKey: 'serverPublicKey123', |
||||
|
privateKey: 'serverPrivateKey123', |
||||
|
createdAt: '2023-01-01T00:00:00.000Z', |
||||
|
updatedAt: '2023-01-01T00:00:00.000Z', |
||||
|
}; |
||||
|
|
||||
|
const mockHooks: HooksType = { |
||||
|
id: 'wg0', |
||||
|
preUp: 'echo "PreUp"', |
||||
|
postUp: 'iptables -A FORWARD -i wg0 -j ACCEPT', |
||||
|
preDown: 'echo "PreDown"', |
||||
|
postDown: 'iptables -D FORWARD -i wg0 -j ACCEPT', |
||||
|
createdAt: '2023-01-01T00:00:00.000Z', |
||||
|
updatedAt: '2023-01-01T00:00:00.000Z', |
||||
|
}; |
||||
|
|
||||
|
const mockUserConfig: UserConfigType = { |
||||
|
id: 'wg0', |
||||
|
host: 'vpn.example.com', |
||||
|
port: 51820, |
||||
|
defaultDns: ['1.1.1.1', '1.0.0.1'], |
||||
|
defaultAllowedIps: ['0.0.0.0/0', '::/0'], |
||||
|
defaultMtu: 1420, |
||||
|
defaultPersistentKeepalive: 25, |
||||
|
createdAt: '2023-01-01T00:00:00.000Z', |
||||
|
updatedAt: '2023-01-01T00:00:00.000Z', |
||||
|
}; |
||||
|
|
||||
|
describe('generateServerPeer', () => { |
||||
|
it('should generate server peer configuration with IPv6 enabled', () => { |
||||
|
const result = wg.generateServerPeer(mockClient, { enableIpv6: true }); |
||||
|
|
||||
|
expect(result).toContain('# Client: Test Client (1)'); |
||||
|
expect(result).toContain('[Peer]'); |
||||
|
expect(result).toContain('PublicKey = clientPublicKey123'); |
||||
|
expect(result).toContain('PresharedKey = preSharedKey123'); |
||||
|
expect(result).toContain( |
||||
|
'AllowedIPs = 10.8.0.2/32, fd42:42:42::2/128, 192.168.1.0/24' |
||||
|
); |
||||
|
expect(result).toContain('Endpoint = 192.168.1.100:51820'); |
||||
|
}); |
||||
|
|
||||
|
it('should generate server peer configuration with IPv6 disabled', () => { |
||||
|
const result = wg.generateServerPeer(mockClient, { enableIpv6: false }); |
||||
|
|
||||
|
expect(result).toContain('# Client: Test Client (1)'); |
||||
|
expect(result).toContain('[Peer]'); |
||||
|
expect(result).toContain('PublicKey = clientPublicKey123'); |
||||
|
expect(result).toContain('PresharedKey = preSharedKey123'); |
||||
|
expect(result).toContain('AllowedIPs = 10.8.0.2/32, 192.168.1.0/24'); |
||||
|
expect(result).not.toContain('fd42:42:42::2/128'); |
||||
|
}); |
||||
|
|
||||
|
it('should handle client without server endpoint', () => { |
||||
|
const clientWithoutEndpoint = { ...mockClient, serverEndpoint: null }; |
||||
|
const result = wg.generateServerPeer(clientWithoutEndpoint); |
||||
|
|
||||
|
expect(result).not.toContain('Endpoint ='); |
||||
|
}); |
||||
|
|
||||
|
it('should handle client without serverAllowedIps', () => { |
||||
|
const clientWithoutAllowedIps = { ...mockClient, serverAllowedIps: [] }; |
||||
|
const result = wg.generateServerPeer(clientWithoutAllowedIps); |
||||
|
|
||||
|
expect(result).toContain('AllowedIPs = 10.8.0.2/32, fd42:42:42::2/128'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('generateServerInterface', () => { |
||||
|
it('should generate server interface configuration', () => { |
||||
|
const result = wg.generateServerInterface(mockInterface, mockHooks, { |
||||
|
enableIpv6: true, |
||||
|
}); |
||||
|
|
||||
|
expect(result).toContain('# Note: Do not edit this file directly.'); |
||||
|
expect(result).toContain('[Interface]'); |
||||
|
expect(result).toContain('PrivateKey = serverPrivateKey123'); |
||||
|
expect(result).toContain('Address = 10.8.0.1/24, fd42:42:42::1/64'); |
||||
|
expect(result).toContain('ListenPort = 51820'); |
||||
|
expect(result).toContain('MTU = 1420'); |
||||
|
}); |
||||
|
|
||||
|
it('should generate server interface configuration without IPv6', () => { |
||||
|
const result = wg.generateServerInterface(mockInterface, mockHooks, { |
||||
|
enableIpv6: false, |
||||
|
}); |
||||
|
|
||||
|
expect(result).toContain('Address = 10.8.0.1/24'); |
||||
|
expect(result).not.toContain('fd42:42:42::1/64'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('generateClientConfig', () => { |
||||
|
it('should generate client configuration with IPv6 enabled', () => { |
||||
|
const fullClient: ClientType = { |
||||
|
...mockClient, |
||||
|
createdAt: '2023-01-01T00:00:00.000Z', |
||||
|
updatedAt: '2023-01-01T00:00:00.000Z', |
||||
|
}; |
||||
|
const result = wg.generateClientConfig( |
||||
|
mockInterface, |
||||
|
mockUserConfig, |
||||
|
fullClient, |
||||
|
{ enableIpv6: true } |
||||
|
); |
||||
|
|
||||
|
expect(result).toContain('[Interface]'); |
||||
|
expect(result).toContain('PrivateKey = privateKey123'); |
||||
|
expect(result).toContain('Address = 10.8.0.2/32, fd42:42:42::2/128'); |
||||
|
expect(result).toContain('MTU = 1420'); |
||||
|
expect(result).toContain('DNS = 1.1.1.1, 8.8.8.8'); |
||||
|
expect(result).toContain('[Peer]'); |
||||
|
expect(result).toContain('PublicKey = serverPublicKey123'); |
||||
|
expect(result).toContain('PresharedKey = preSharedKey123'); |
||||
|
expect(result).toContain('AllowedIPs = 0.0.0.0/0, ::/0'); |
||||
|
expect(result).toContain('PersistentKeepalive = 25'); |
||||
|
expect(result).toContain('Endpoint = vpn.example.com:51820'); |
||||
|
}); |
||||
|
|
||||
|
it('should generate client configuration with IPv6 disabled', () => { |
||||
|
const fullClient: ClientType = { |
||||
|
...mockClient, |
||||
|
createdAt: '2023-01-01T00:00:00.000Z', |
||||
|
updatedAt: '2023-01-01T00:00:00.000Z', |
||||
|
}; |
||||
|
const result = wg.generateClientConfig( |
||||
|
mockInterface, |
||||
|
mockUserConfig, |
||||
|
fullClient, |
||||
|
{ enableIpv6: false } |
||||
|
); |
||||
|
|
||||
|
expect(result).toContain('Address = 10.8.0.2/32'); |
||||
|
expect(result).not.toContain('fd42:42:42::2/128'); |
||||
|
}); |
||||
|
|
||||
|
it('should use default DNS when client DNS is not set', () => { |
||||
|
const clientWithoutDns: ClientType = { |
||||
|
...mockClient, |
||||
|
dns: null, |
||||
|
createdAt: '2023-01-01T00:00:00.000Z', |
||||
|
updatedAt: '2023-01-01T00:00:00.000Z', |
||||
|
}; |
||||
|
const result = wg.generateClientConfig( |
||||
|
mockInterface, |
||||
|
mockUserConfig, |
||||
|
clientWithoutDns |
||||
|
); |
||||
|
|
||||
|
expect(result).toContain('DNS = 1.1.1.1, 1.0.0.1'); |
||||
|
}); |
||||
|
|
||||
|
it('should not include DNS line when DNS is empty', () => { |
||||
|
const clientWithoutDns: ClientType = { |
||||
|
...mockClient, |
||||
|
dns: null, |
||||
|
createdAt: '2023-01-01T00:00:00.000Z', |
||||
|
updatedAt: '2023-01-01T00:00:00.000Z', |
||||
|
}; |
||||
|
const configWithoutDns = { ...mockUserConfig, defaultDns: [] }; |
||||
|
const result = wg.generateClientConfig( |
||||
|
mockInterface, |
||||
|
configWithoutDns, |
||||
|
clientWithoutDns |
||||
|
); |
||||
|
|
||||
|
expect(result).not.toContain('DNS ='); |
||||
|
}); |
||||
|
|
||||
|
it('should include client hooks when present', () => { |
||||
|
const clientWithHooks: ClientType = { |
||||
|
...mockClient, |
||||
|
preUp: 'echo "Client PreUp"', |
||||
|
postUp: 'echo "Client PostUp"', |
||||
|
preDown: 'echo "Client PreDown"', |
||||
|
postDown: 'echo "Client PostDown"', |
||||
|
createdAt: '2023-01-01T00:00:00.000Z', |
||||
|
updatedAt: '2023-01-01T00:00:00.000Z', |
||||
|
}; |
||||
|
const result = wg.generateClientConfig( |
||||
|
mockInterface, |
||||
|
mockUserConfig, |
||||
|
clientWithHooks |
||||
|
); |
||||
|
|
||||
|
expect(result).toContain('PreUp = echo "Client PreUp"'); |
||||
|
expect(result).toContain('PostUp = echo "Client PostUp"'); |
||||
|
expect(result).toContain('PreDown = echo "Client PreDown"'); |
||||
|
expect(result).toContain('PostDown = echo "Client PostDown"'); |
||||
|
}); |
||||
|
|
||||
|
it('should use default allowedIps when client allowedIps is not set', () => { |
||||
|
const clientWithoutAllowedIps: ClientType = { |
||||
|
...mockClient, |
||||
|
allowedIps: null, |
||||
|
createdAt: '2023-01-01T00:00:00.000Z', |
||||
|
updatedAt: '2023-01-01T00:00:00.000Z', |
||||
|
}; |
||||
|
const result = wg.generateClientConfig( |
||||
|
mockInterface, |
||||
|
mockUserConfig, |
||||
|
clientWithoutAllowedIps |
||||
|
); |
||||
|
|
||||
|
expect(result).toContain('AllowedIPs = 0.0.0.0/0, ::/0'); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('dump', () => { |
||||
|
it('should parse wireguard dump output correctly', async () => { |
||||
|
const mockDumpOutput = `private-key public-key listen-port fwmark
|
||||
|
publicKey1 psk1 192.168.1.100:12345 10.8.0.2/32 1699000000 1000000 2000000 25 |
||||
|
publicKey2 (none) (none) 10.8.0.3/32 0 500000 1500000 0`;
|
||||
|
|
||||
|
mockExec.mockResolvedValue(mockDumpOutput); |
||||
|
|
||||
|
const result = await wg.dump('wg0'); |
||||
|
|
||||
|
expect(result).toHaveLength(2); |
||||
|
expect(result[0]).toEqual({ |
||||
|
publicKey: 'publicKey1', |
||||
|
preSharedKey: 'psk1', |
||||
|
endpoint: '192.168.1.100:12345', |
||||
|
allowedIps: '10.8.0.2/32', |
||||
|
latestHandshakeAt: new Date(1699000000000), |
||||
|
transferRx: 1000000, |
||||
|
transferTx: 2000000, |
||||
|
persistentKeepalive: '25', |
||||
|
}); |
||||
|
expect(result[1]).toEqual({ |
||||
|
publicKey: 'publicKey2', |
||||
|
preSharedKey: '(none)', |
||||
|
endpoint: null, |
||||
|
allowedIps: '10.8.0.3/32', |
||||
|
latestHandshakeAt: null, |
||||
|
transferRx: 500000, |
||||
|
transferTx: 1500000, |
||||
|
persistentKeepalive: '0', |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
describe('command execution functions', () => { |
||||
|
beforeEach(() => { |
||||
|
vi.clearAllMocks(); |
||||
|
}); |
||||
|
|
||||
|
it('should execute generatePrivateKey command', async () => { |
||||
|
mockExec.mockResolvedValue('generatedPrivateKey'); |
||||
|
|
||||
|
const result = await wg.generatePrivateKey(); |
||||
|
|
||||
|
expect(result).toBe('generatedPrivateKey'); |
||||
|
expect(mockExec).toHaveBeenCalledWith('wg genkey'); |
||||
|
}); |
||||
|
|
||||
|
it('should execute getPublicKey command', async () => { |
||||
|
mockExec.mockResolvedValue('generatedPublicKey'); |
||||
|
|
||||
|
const result = await wg.getPublicKey('privateKey123'); |
||||
|
|
||||
|
expect(result).toBe('generatedPublicKey'); |
||||
|
expect(mockExec).toHaveBeenCalledWith('echo privateKey123 | wg pubkey', { |
||||
|
log: 'echo ***hidden*** | wg pubkey', |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
it('should execute generatePreSharedKey command', async () => { |
||||
|
mockExec.mockResolvedValue('generatedPSK'); |
||||
|
|
||||
|
const result = await wg.generatePreSharedKey(); |
||||
|
|
||||
|
expect(result).toBe('generatedPSK'); |
||||
|
expect(mockExec).toHaveBeenCalledWith('wg genpsk'); |
||||
|
}); |
||||
|
|
||||
|
it('should execute up command', async () => { |
||||
|
mockExec.mockResolvedValue(''); |
||||
|
|
||||
|
await wg.up('wg0'); |
||||
|
|
||||
|
expect(mockExec).toHaveBeenCalledWith('wg-quick up wg0'); |
||||
|
}); |
||||
|
|
||||
|
it('should execute down command', async () => { |
||||
|
mockExec.mockResolvedValue(''); |
||||
|
|
||||
|
await wg.down('wg0'); |
||||
|
|
||||
|
expect(mockExec).toHaveBeenCalledWith('wg-quick down wg0'); |
||||
|
}); |
||||
|
|
||||
|
it('should execute restart command', async () => { |
||||
|
mockExec.mockResolvedValue(''); |
||||
|
|
||||
|
await wg.restart('wg0'); |
||||
|
|
||||
|
expect(mockExec).toHaveBeenCalledWith( |
||||
|
'wg-quick down wg0; wg-quick up wg0' |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('should execute sync command', async () => { |
||||
|
mockExec.mockResolvedValue(''); |
||||
|
|
||||
|
await wg.sync('wg0'); |
||||
|
|
||||
|
expect(mockExec).toHaveBeenCalledWith( |
||||
|
'wg syncconf wg0 <(wg-quick strip wg0)' |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,22 @@ |
|||||
|
import { resolve } from 'node:path'; |
||||
|
import { defineConfig } from 'vitest/config'; |
||||
|
|
||||
|
export default defineConfig({ |
||||
|
test: { |
||||
|
globals: true, |
||||
|
environment: 'node', |
||||
|
pool: 'threads', |
||||
|
poolOptions: { |
||||
|
threads: { |
||||
|
singleThread: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
resolve: { |
||||
|
alias: { |
||||
|
'#db': resolve(__dirname, './server/database'), |
||||
|
'~': resolve(__dirname, './'), |
||||
|
'@': resolve(__dirname, './'), |
||||
|
}, |
||||
|
}, |
||||
|
}); |
||||
Loading…
Reference in new issue