feat(13-01): auto-generate BACKEND_SCHEMAS, update RCLONE_TYPE_MAP for all 18 backends

- BACKEND_SCHEMAS now auto-generated via Object.fromEntries over registry keys
- RCLONE_TYPE_MAP expanded with 11 new entries for all 18 backend types
- RCLONE_TYPE_MAP exported for direct testing
- index.test.ts: added auto-generation coverage (3 tests) + 4 new backend schema smoke tests
- rclone-conf.test.ts: added RCLONE_TYPE_MAP exhaustiveness test + gdrive/ftp/smb conf output tests
- Rule 1 fix: BackendSelectionStep test used /next/i regex matching WebDAV "NextCloud" card buttons
This commit is contained in:
2026-04-01 16:20:18 +02:00
parent b8c64937ea
commit 3010cdb0c6
5 changed files with 251 additions and 16 deletions
+136 -2
View File
@@ -1,8 +1,8 @@
// src/generators/rclone-conf.test.ts
// Tests for buildRcloneConf — rclone INI config generator.
// RED: imports will fail until Plan 02-02 creates rclone-conf.ts.
import { buildRcloneConf } from './rclone-conf';
import { buildRcloneConf, RCLONE_TYPE_MAP } from './rclone-conf';
import { BACKEND_REGISTRY, BackendType } from '../schemas/registry';
import { INITIAL_STATE } from '../store/types';
import type { WizardState } from '../store/types';
@@ -273,3 +273,137 @@ describe('buildRcloneConf — b2', () => {
expect(buildRcloneConf(b2State)).toContain('key = APP_KEY_SECRET');
});
});
// --- RCLONE_TYPE_MAP coverage ---
describe('RCLONE_TYPE_MAP', () => {
it('has an entry for every BackendType in BACKEND_REGISTRY', () => {
const registryKeys = Object.keys(BACKEND_REGISTRY) as BackendType[];
for (const key of registryKeys) {
expect(RCLONE_TYPE_MAP[key], `RCLONE_TYPE_MAP["${key}"]`).toBeDefined();
}
});
it('maps gdrive to drive (not gdrive)', () => {
expect(RCLONE_TYPE_MAP['gdrive']).toBe('drive');
});
it('maps azure-files to azurefiles (no hyphen)', () => {
expect(RCLONE_TYPE_MAP['azure-files']).toBe('azurefiles');
});
it('maps s3-compatible to s3 (provider=Other differentiates)', () => {
expect(RCLONE_TYPE_MAP['s3-compatible']).toBe('s3');
});
it('maps gcs to google cloud storage (with spaces)', () => {
expect(RCLONE_TYPE_MAP['gcs']).toBe('google cloud storage');
});
});
// --- New backend rclone.conf output tests ---
const gdriveState: WizardState = {
...INITIAL_STATE,
remote: {
name: 'my-gdrive',
backendType: 'gdrive',
params: {
token: '{"access_token":"GTOKEN","token_type":"Bearer"}',
root_folder_id: '',
service_account_credentials: '',
},
},
};
const ftpState: WizardState = {
...INITIAL_STATE,
remote: {
name: 'my-ftp',
backendType: 'ftp',
params: {
host: 'ftp.example.com',
user: 'ftpuser',
pass: 'ftppass',
port: '',
explicit_tls: '',
},
},
};
const smbState: WizardState = {
...INITIAL_STATE,
remote: {
name: 'my-smb',
backendType: 'smb',
params: {
host: 'fileserver.local',
user: 'domain\\admin',
pass: 'secret',
domain: 'CORP',
port: '',
},
},
};
describe('buildRcloneConf — gdrive (new)', () => {
it('contains type = drive (not type = gdrive)', () => {
const output = buildRcloneConf(gdriveState);
expect(output).toContain('type = drive');
expect(output).not.toContain('type = gdrive');
});
it('contains [my-gdrive] section header', () => {
expect(buildRcloneConf(gdriveState)).toContain('[my-gdrive]');
});
it('contains token value', () => {
expect(buildRcloneConf(gdriveState)).toContain('token = ');
});
it('omits empty root_folder_id', () => {
expect(buildRcloneConf(gdriveState)).not.toContain('root_folder_id');
});
it('omits empty service_account_credentials', () => {
expect(buildRcloneConf(gdriveState)).not.toContain('service_account_credentials');
});
});
describe('buildRcloneConf — ftp (new)', () => {
it('contains type = ftp', () => {
expect(buildRcloneConf(ftpState)).toContain('type = ftp');
});
it('contains host', () => {
expect(buildRcloneConf(ftpState)).toContain('host = ftp.example.com');
});
it('contains user', () => {
expect(buildRcloneConf(ftpState)).toContain('user = ftpuser');
});
it('contains pass', () => {
expect(buildRcloneConf(ftpState)).toContain('pass = ftppass');
});
it('omits empty port', () => {
expect(buildRcloneConf(ftpState)).not.toContain('port =');
});
it('omits empty explicit_tls', () => {
expect(buildRcloneConf(ftpState)).not.toContain('explicit_tls');
});
});
describe('buildRcloneConf — smb (new)', () => {
it('contains type = smb', () => {
expect(buildRcloneConf(smbState)).toContain('type = smb');
});
it('contains host', () => {
expect(buildRcloneConf(smbState)).toContain('host = fileserver.local');
});
it('contains user', () => {
expect(buildRcloneConf(smbState)).toContain('user = domain\\admin');
});
it('contains pass', () => {
expect(buildRcloneConf(smbState)).toContain('pass = secret');
});
it('contains domain', () => {
expect(buildRcloneConf(smbState)).toContain('domain = CORP');
});
it('omits empty port', () => {
expect(buildRcloneConf(smbState)).not.toContain('port =');
});
});
+16 -3
View File
@@ -7,14 +7,27 @@ import type { WizardState } from '../store/types';
// Maps our BackendType to rclone's internal type string.
// IMPORTANT: s3-compatible uses 'type = s3' — rclone does not recognize 's3-compatible' as a type.
// S3-compatible backends are differentiated by provider = Other in params.
const RCLONE_TYPE_MAP: Record<string, string> = {
// IMPORTANT: gdrive uses 'type = drive' — rclone's internal name for Google Drive.
// IMPORTANT: gcs uses 'type = google cloud storage' (with spaces) — rclone's internal name.
export const RCLONE_TYPE_MAP: Record<string, string> = {
azureblob: 'azureblob',
s3: 's3',
's3-compatible': 's3',
onedrive: 'onedrive',
sftp: 'sftp',
gcs: 'google cloud storage',
b2: 'b2',
'azure-files': 'azurefiles',
swift: 'swift',
onedrive: 'onedrive',
gdrive: 'drive',
dropbox: 'dropbox',
box: 'box',
pcloud: 'pcloud',
sftp: 'sftp',
ftp: 'ftp',
webdav: 'webdav',
smb: 'smb',
http: 'http',
seafile: 'seafile',
};
export function buildRcloneConf(state: WizardState): string {