const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { buildDuplicateApiSourceName, buildDuplicateApiSource } = require('../src/web/routes/data-sources/api-sources/duplicate'); const { buildApiSourceFormViewModel } = require('../src/web/routes/data-sources/api-sources/form-view-model'); test('api source duplicate helper copies editable fields and resets runtime state', () => { const source = { id: 9, name: 'Source A', api_url: 'https://example.com/api', auth_method: 'bearer', auth_username: 'user', auth_password: 'pass', auth_bearer_token: 'token', auth_header_name: 'X-API-Key', auth_header_value: 'secret', items_path: 'items', update_interval_value: 15, update_interval_unit: 'minutes', last_pulled_at: '2026-08-01T12:00:00.000Z', last_pull_error: 'Bad response', last_response_status: 500, last_response_content_type: 'application/json', last_response_json: '{"ok":false}' }; const duplicate = buildDuplicateApiSource(source, buildDuplicateApiSourceName(source.name)); assert.equal(duplicate.id, null); assert.equal(duplicate.name, 'Copy of Source A'); assert.equal(duplicate.api_url, 'https://example.com/api'); assert.equal(duplicate.apiUrl, 'https://example.com/api'); assert.equal(duplicate.authMethod, 'bearer'); assert.equal(duplicate.authUsername, 'user'); assert.equal(duplicate.authPassword, 'pass'); assert.equal(duplicate.authBearerToken, 'token'); assert.equal(duplicate.authHeaderName, 'X-API-Key'); assert.equal(duplicate.authHeaderValue, 'secret'); assert.equal(duplicate.itemsPath, 'items'); assert.equal(duplicate.lastPulledAt, null); assert.equal(duplicate.lastPullError, ''); assert.equal(duplicate.lastResponseStatus, null); assert.equal(duplicate.lastResponseContentType, ''); assert.equal(duplicate.lastResponseJson, ''); }); test('api source duplicate form model keeps info toast variant', () => { const model = buildApiSourceFormViewModel({ id: null, name: 'Source A' }, 'Review the copied values and save when ready.', { permissionKeys: ['api-sources.create'] }, { messageVariant: 'info', showSaveSecondaryActions: true }, false); assert.equal(model.messageVariant, 'info'); assert.equal(model.showSaveSecondaryActions, true); }); test('API source edit delete action includes a confirmation message', () => { const template = fs.readFileSync(path.join(__dirname, '../src/web/views/data-sources/api-sources/form.hbs'), 'utf8'); assert.match(template, /deleteConfirmMessage="Delete this API source\?"/); }); test('API source form normalization is shared by add, edit, and duplication', () => { const source = { id: 9, name: 'Tiltify', auth_method: 'token_login', token_url: 'https://example.com/token', token_response_path: 'data.token', token_header_prefix: '', update_interval_value: 30, update_interval_unit: 'seconds', enabled: 0 }; const add = buildApiSourceFormViewModel(source, '', null, {}, false).apiSource; const edit = buildApiSourceFormViewModel(source, '', null, {}, true).apiSource; const duplicate = buildDuplicateApiSource(source, 'Copy of Tiltify'); for (const values of [add, edit, duplicate]) { assert.equal(values.authMethod, 'token_login'); assert.equal(values.tokenUrl, source.token_url); assert.equal(values.tokenResponsePath, 'data.token'); assert.equal(values.tokenHeaderPrefix, ''); assert.equal(values.updateIntervalValue, 30); assert.equal(values.updateIntervalUnit, 'seconds'); assert.equal(values.enabled, 0); } }); test('API group duplication copies request settings without IDs or response state', () => { const request = { id: 10, name: 'Donors', api_url: 'https://example.com/donors', request_method: 'GET', items_path: 'data', enabled: 1, update_interval_value: 30, update_interval_unit: 'seconds', last_response_json: '{"private":"old snapshot"}', last_pull_error: 'Old error' }; const duplicate = buildDuplicateApiSource({ id: 9, name: 'Tiltify', requests: [request] }, 'Copy of Tiltify'); assert.equal(duplicate.id, null); assert.equal(duplicate.requests.length, 1); assert.equal(duplicate.requests[0].api_url, request.api_url); assert.equal(duplicate.requests[0].update_interval_value, 30); assert.equal(duplicate.requests[0].id, undefined); assert.equal(duplicate.requests[0].last_response_json, undefined); assert.equal(duplicate.requests[0].last_pull_error, undefined); assert.equal(request.id, 10); }); test('api source list template includes duplicate action', () => { const template = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'views', 'data-sources', 'api-sources', 'list.hbs'), 'utf8'); assert.match(template, /\/data-sources\/api-sources\/{\{id\}\}\/duplicate/); assert.match(template, />Dupe<\/a>/); assert.match(template, /

API sources<\/h2>\s*

Manage API sources, shared authentication, and request refresh schedules\.<\/p>/); });