feat(13-01): add 18 backends to BACKEND_REGISTRY with category field
- Removed explicit BackendType union literal; derived via keyof typeof BACKEND_REGISTRY - Added BackendCategory type and category field to all 18 backend entries - Added BackendMeta interface for individual entry typing - Removed circular Record<BackendType,...> cast; replaced with as const - Added 11 new backends: azure-files, swift, gdrive, dropbox, box, pcloud, ftp, webdav, smb, http, seafile - Updated registry.test.ts: 48 tests covering all 18 backends, category assertions, field validation
This commit is contained in:
+238
-14
@@ -1,23 +1,81 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BACKEND_REGISTRY, BackendType } from './registry';
|
||||
import { BACKEND_REGISTRY, BackendType, BackendCategory } from './registry';
|
||||
|
||||
const EXPECTED_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible', 'onedrive', 'sftp', 'gcs', 'b2'];
|
||||
// 7 cloud-object-storage + 5 cloud-drives + 6 protocol-based = 18 total
|
||||
const ALL_BACKENDS: BackendType[] = [
|
||||
'azureblob', 's3', 's3-compatible', 'gcs', 'b2', 'azure-files', 'swift',
|
||||
'onedrive', 'gdrive', 'dropbox', 'box', 'pcloud',
|
||||
'sftp', 'ftp', 'webdav', 'smb', 'http', 'seafile',
|
||||
];
|
||||
|
||||
// Keep the original 7 for backwards compatibility checks
|
||||
const ORIGINAL_7_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible', 'onedrive', 'sftp', 'gcs', 'b2'];
|
||||
|
||||
const VALID_CATEGORIES: BackendCategory[] = ['cloud-object-storage', 'cloud-drives', 'protocol-based'];
|
||||
|
||||
describe('Backend Schema Registry', () => {
|
||||
it('exports all three required backend types', () => {
|
||||
for (const backend of EXPECTED_BACKENDS) {
|
||||
it('has exactly 18 backend entries', () => {
|
||||
const keys = Object.keys(BACKEND_REGISTRY) as BackendType[];
|
||||
expect(keys).toHaveLength(18);
|
||||
});
|
||||
|
||||
it('exports all 17 required backend types', () => {
|
||||
for (const backend of ALL_BACKENDS) {
|
||||
expect(BACKEND_REGISTRY[backend]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('exports all original 7 backend types (regression)', () => {
|
||||
for (const backend of ORIGINAL_7_BACKENDS) {
|
||||
expect(BACKEND_REGISTRY[backend]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('each backend has displayName, description, category, and at least one field', () => {
|
||||
for (const backend of ALL_BACKENDS) {
|
||||
const entry = BACKEND_REGISTRY[backend];
|
||||
expect(entry.displayName, `${backend}.displayName`).toBeTruthy();
|
||||
expect(entry.description, `${backend}.description`).toBeTruthy();
|
||||
expect(entry.category, `${backend}.category`).toBeTruthy();
|
||||
expect(entry.fields.length, `${backend}.fields.length`).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('each backend has a valid category value', () => {
|
||||
for (const backend of ALL_BACKENDS) {
|
||||
expect(VALID_CATEGORIES).toContain(BACKEND_REGISTRY[backend].category);
|
||||
}
|
||||
});
|
||||
|
||||
it('cloud-object-storage backends are correctly categorized', () => {
|
||||
const expected: BackendType[] = ['azureblob', 's3', 's3-compatible', 'gcs', 'b2', 'azure-files', 'swift'];
|
||||
for (const b of expected) {
|
||||
expect(BACKEND_REGISTRY[b].category).toBe('cloud-object-storage');
|
||||
}
|
||||
});
|
||||
|
||||
it('cloud-drives backends are correctly categorized', () => {
|
||||
const expected: BackendType[] = ['onedrive', 'gdrive', 'dropbox', 'box', 'pcloud'];
|
||||
for (const b of expected) {
|
||||
expect(BACKEND_REGISTRY[b].category).toBe('cloud-drives');
|
||||
}
|
||||
});
|
||||
|
||||
it('protocol-based backends are correctly categorized', () => {
|
||||
const expected: BackendType[] = ['sftp', 'ftp', 'webdav', 'smb', 'http', 'seafile'];
|
||||
for (const b of expected) {
|
||||
expect(BACKEND_REGISTRY[b].category).toBe('protocol-based');
|
||||
}
|
||||
});
|
||||
|
||||
it('each backend has at least one field definition', () => {
|
||||
for (const backend of EXPECTED_BACKENDS) {
|
||||
for (const backend of ALL_BACKENDS) {
|
||||
expect(BACKEND_REGISTRY[backend].fields.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('each FieldDef has a non-empty key (snake_case, no camelCase)', () => {
|
||||
for (const backend of EXPECTED_BACKENDS) {
|
||||
for (const backend of ALL_BACKENDS) {
|
||||
for (const field of BACKEND_REGISTRY[backend].fields) {
|
||||
expect(field.key).toBeTruthy();
|
||||
// Reject camelCase — rclone keys are snake_case or lowercase
|
||||
@@ -27,13 +85,20 @@ describe('Backend Schema Registry', () => {
|
||||
});
|
||||
|
||||
it('each FieldDef has a non-empty label', () => {
|
||||
for (const backend of EXPECTED_BACKENDS) {
|
||||
for (const backend of ALL_BACKENDS) {
|
||||
for (const field of BACKEND_REGISTRY[backend].fields) {
|
||||
expect(field.label).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('each backend entry has displayName and description metadata', () => {
|
||||
for (const backend of ALL_BACKENDS) {
|
||||
expect(BACKEND_REGISTRY[backend].displayName).toBeTruthy();
|
||||
expect(BACKEND_REGISTRY[backend].description).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it('Azure Blob has account field (required)', () => {
|
||||
const accountField = BACKEND_REGISTRY.azureblob.fields.find(f => f.key === 'account');
|
||||
expect(accountField).toBeDefined();
|
||||
@@ -53,13 +118,6 @@ describe('Backend Schema Registry', () => {
|
||||
expect(endpointField!.required).toBe(true);
|
||||
});
|
||||
|
||||
it('each backend entry has displayName and description metadata', () => {
|
||||
for (const backend of EXPECTED_BACKENDS) {
|
||||
expect(BACKEND_REGISTRY[backend].displayName).toBeTruthy();
|
||||
expect(BACKEND_REGISTRY[backend].description).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
describe('OneDrive backend', () => {
|
||||
it('has token field (required)', () => {
|
||||
const f = BACKEND_REGISTRY.onedrive.fields.find(f => f.key === 'token');
|
||||
@@ -130,4 +188,170 @@ describe('Backend Schema Registry', () => {
|
||||
expect(f!.inputType).toBe('password');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Azure Files backend (new)', () => {
|
||||
it('has account field (required)', () => {
|
||||
const f = BACKEND_REGISTRY['azure-files'].fields.find(f => f.key === 'account');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(true);
|
||||
});
|
||||
it('has key field (required)', () => {
|
||||
const f = BACKEND_REGISTRY['azure-files'].fields.find(f => f.key === 'key');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(true);
|
||||
expect(f!.inputType).toBe('password');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Swift backend (new)', () => {
|
||||
it('has user, key, auth, and tenant fields (all required)', () => {
|
||||
const fields = BACKEND_REGISTRY.swift.fields;
|
||||
const keys = fields.map(f => f.key);
|
||||
expect(keys).toContain('user');
|
||||
expect(keys).toContain('key');
|
||||
expect(keys).toContain('auth');
|
||||
expect(keys).toContain('tenant');
|
||||
});
|
||||
it('has optional region field', () => {
|
||||
const f = BACKEND_REGISTRY.swift.fields.find(f => f.key === 'region');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Google Drive backend (new)', () => {
|
||||
it('has token field (optional — service account path also supported)', () => {
|
||||
const f = BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'token');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.inputType).toBe('password');
|
||||
});
|
||||
it('has service_account_credentials field (optional)', () => {
|
||||
const f = BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'service_account_credentials');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(false);
|
||||
});
|
||||
it('has root_folder_id field (optional)', () => {
|
||||
const f = BACKEND_REGISTRY.gdrive.fields.find(f => f.key === 'root_folder_id');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dropbox backend (new)', () => {
|
||||
it('has token field (required)', () => {
|
||||
const f = BACKEND_REGISTRY.dropbox.fields.find(f => f.key === 'token');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(true);
|
||||
expect(f!.inputType).toBe('password');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Box backend (new)', () => {
|
||||
it('has token field (required)', () => {
|
||||
const f = BACKEND_REGISTRY.box.fields.find(f => f.key === 'token');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(true);
|
||||
});
|
||||
it('has box_sub_type select field (optional)', () => {
|
||||
const f = BACKEND_REGISTRY.box.fields.find(f => f.key === 'box_sub_type');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.inputType).toBe('select');
|
||||
expect(f!.required).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pCloud backend (new)', () => {
|
||||
it('has token field (required)', () => {
|
||||
const f = BACKEND_REGISTRY.pcloud.fields.find(f => f.key === 'token');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(true);
|
||||
});
|
||||
it('has hostname select field (optional, US/EU options)', () => {
|
||||
const f = BACKEND_REGISTRY.pcloud.fields.find(f => f.key === 'hostname');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.inputType).toBe('select');
|
||||
const values = f!.options!.map(o => o.value);
|
||||
expect(values).toContain('api.pcloud.com');
|
||||
expect(values).toContain('eapi.pcloud.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FTP backend (new)', () => {
|
||||
it('has host field (required)', () => {
|
||||
const f = BACKEND_REGISTRY.ftp.fields.find(f => f.key === 'host');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(true);
|
||||
});
|
||||
it('has user, pass, port fields (all optional)', () => {
|
||||
const fields = BACKEND_REGISTRY.ftp.fields;
|
||||
const optional = ['user', 'pass', 'port'];
|
||||
for (const key of optional) {
|
||||
const f = fields.find(f => f.key === key);
|
||||
expect(f, `ftp.${key}`).toBeDefined();
|
||||
expect(f!.required, `ftp.${key}.required`).toBe(false);
|
||||
}
|
||||
});
|
||||
it('has explicit_tls select field (optional)', () => {
|
||||
const f = BACKEND_REGISTRY.ftp.fields.find(f => f.key === 'explicit_tls');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.inputType).toBe('select');
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebDAV backend (new)', () => {
|
||||
it('has url, user, pass fields (required)', () => {
|
||||
const fields = BACKEND_REGISTRY.webdav.fields;
|
||||
for (const key of ['url', 'user', 'pass']) {
|
||||
const f = fields.find(f => f.key === key);
|
||||
expect(f, `webdav.${key}`).toBeDefined();
|
||||
expect(f!.required, `webdav.${key}.required`).toBe(true);
|
||||
}
|
||||
});
|
||||
it('has vendor select field (optional) with nextcloud/sharepoint options', () => {
|
||||
const f = BACKEND_REGISTRY.webdav.fields.find(f => f.key === 'vendor');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.inputType).toBe('select');
|
||||
const values = f!.options!.map(o => o.value);
|
||||
expect(values).toContain('nextcloud');
|
||||
expect(values).toContain('sharepoint');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SMB backend (new)', () => {
|
||||
it('has host field (required)', () => {
|
||||
const f = BACKEND_REGISTRY.smb.fields.find(f => f.key === 'host');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(true);
|
||||
});
|
||||
it('has user field (required)', () => {
|
||||
const f = BACKEND_REGISTRY.smb.fields.find(f => f.key === 'user');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(true);
|
||||
});
|
||||
it('has pass, domain, port fields (optional)', () => {
|
||||
for (const key of ['pass', 'domain', 'port']) {
|
||||
const f = BACKEND_REGISTRY.smb.fields.find(f => f.key === key);
|
||||
expect(f, `smb.${key}`).toBeDefined();
|
||||
expect(f!.required, `smb.${key}.required`).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP backend (new)', () => {
|
||||
it('has url field (required)', () => {
|
||||
const f = BACKEND_REGISTRY.http.fields.find(f => f.key === 'url');
|
||||
expect(f).toBeDefined();
|
||||
expect(f!.required).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Seafile backend (new)', () => {
|
||||
it('has url, user, pass fields (all required)', () => {
|
||||
for (const key of ['url', 'user', 'pass']) {
|
||||
const f = BACKEND_REGISTRY.seafile.fields.find(f => f.key === key);
|
||||
expect(f, `seafile.${key}`).toBeDefined();
|
||||
expect(f!.required, `seafile.${key}.required`).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user