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:
2026-04-01 16:16:58 +02:00
parent 7e02b01595
commit b8c64937ea
2 changed files with 701 additions and 103 deletions
+238 -14
View File
@@ -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);
}
});
});
});