initial commit
This commit is contained in:
247
src/__tests__/e2e-ftp.test.ts
Normal file
247
src/__tests__/e2e-ftp.test.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
|
||||
import * as ftp from 'basic-ftp'
|
||||
import { FTPServer } from '../adapters/server/ftp.js'
|
||||
import type { BaseClient } from '../adapters/client/base.js'
|
||||
import {
|
||||
createMockClient, populateCache,
|
||||
HOME_TREE, HOME_FILES, STORAGE_TREE, STORAGE_FILES,
|
||||
} from './helpers.js'
|
||||
|
||||
vi.mock('../logger.js', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const TEST_PORT = 2391
|
||||
const TEST_USER = 'testuser'
|
||||
const TEST_PASS = 'testpass'
|
||||
|
||||
/** Creates a connected + authenticated FTP client. */
|
||||
async function connectFTP(): Promise<ftp.Client> {
|
||||
const client = new ftp.Client()
|
||||
await client.access({
|
||||
host: '127.0.0.1',
|
||||
port: TEST_PORT,
|
||||
user: TEST_USER,
|
||||
password: TEST_PASS,
|
||||
secure: false,
|
||||
})
|
||||
return client
|
||||
}
|
||||
|
||||
describe('E2E: FTP Server', () => {
|
||||
let server: FTPServer
|
||||
let homeClient: BaseClient
|
||||
let storageClient: BaseClient
|
||||
|
||||
beforeAll(async () => {
|
||||
homeClient = createMockClient('/home', HOME_TREE, HOME_FILES)
|
||||
storageClient = createMockClient('/storage', STORAGE_TREE, STORAGE_FILES)
|
||||
|
||||
await populateCache(homeClient, HOME_TREE)
|
||||
await populateCache(storageClient, STORAGE_TREE)
|
||||
|
||||
server = new FTPServer(
|
||||
[homeClient, storageClient],
|
||||
{ port: TEST_PORT, pasv_url: '127.0.0.1', pasv_min: 3000, pasv_max: 3100 },
|
||||
{ username: TEST_USER, password: TEST_PASS },
|
||||
)
|
||||
await server.start()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await server.stop()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Authentication
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('should reject invalid credentials', async () => {
|
||||
const client = new ftp.Client()
|
||||
await expect(
|
||||
client.access({
|
||||
host: '127.0.0.1',
|
||||
port: TEST_PORT,
|
||||
user: 'wrong',
|
||||
password: 'wrong',
|
||||
secure: false,
|
||||
})
|
||||
).rejects.toThrow()
|
||||
client.close()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Directory listing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('should list the virtual root', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
const list = await client.list('/')
|
||||
const names = list.map(e => e.name)
|
||||
expect(names).toContain('home')
|
||||
expect(names).toContain('storage')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('should list mount point contents', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
const list = await client.list('/home')
|
||||
const names = list.map(e => e.name)
|
||||
expect(names).toContain('docs')
|
||||
expect(names).toContain('readme.txt')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('should list nested directory contents', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
const list = await client.list('/home/docs')
|
||||
const names = list.map(e => e.name)
|
||||
expect(names).toContain('hello.txt')
|
||||
expect(names).toContain('world.txt')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('should list storage mount contents', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
const list = await client.list('/storage/backups')
|
||||
const names = list.map(e => e.name)
|
||||
expect(names).toContain('db.sql')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Directory navigation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('should change directory to mount point', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
await client.cd('/home')
|
||||
const pwd = await client.pwd()
|
||||
expect(pwd).toBe('/home')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('should change directory to nested path', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
await client.cd('/home/docs')
|
||||
const pwd = await client.pwd()
|
||||
expect(pwd).toBe('/home/docs')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File download
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('should download a file', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
const chunks: Buffer[] = []
|
||||
const writable = new (await import('node:stream')).PassThrough()
|
||||
writable.on('data', (chunk: Buffer) => chunks.push(chunk))
|
||||
|
||||
await client.downloadTo(writable, '/home/readme.txt')
|
||||
const content = Buffer.concat(chunks).toString()
|
||||
expect(content).toBe('Hello from home!')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
it('should download a nested file', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
const chunks: Buffer[] = []
|
||||
const writable = new (await import('node:stream')).PassThrough()
|
||||
writable.on('data', (chunk: Buffer) => chunks.push(chunk))
|
||||
|
||||
await client.downloadTo(writable, '/home/docs/hello.txt')
|
||||
const content = Buffer.concat(chunks).toString()
|
||||
expect(content).toBe('Hello, World!')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File upload
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('should upload a file', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
const { Readable } = await import('node:stream')
|
||||
const readable = Readable.from(Buffer.from('Uploaded content'))
|
||||
|
||||
await client.uploadFrom(readable, '/home/uploaded.txt')
|
||||
expect(homeClient.write).toHaveBeenCalled()
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Directory creation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('should create a directory', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
// Use raw MKD command — ensureDir does CWD+MKD which fails on virtual paths
|
||||
await client.send('MKD /home/new-dir')
|
||||
expect(homeClient.mkdir).toHaveBeenCalledWith('/new-dir')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Delete
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('should delete a file', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
await client.remove('/home/readme.txt')
|
||||
expect(homeClient.delete).toHaveBeenCalledWith('/readme.txt')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rename
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('should rename a file', async () => {
|
||||
const client = await connectFTP()
|
||||
try {
|
||||
await client.rename('/home/readme.txt', '/home/renamed.txt')
|
||||
expect(homeClient.rename).toHaveBeenCalledWith('/readme.txt', '/renamed.txt')
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user