aiostreams / packages /frontend /src /lib /__tests__ /template-processor.test.ts
f4b404's picture
Upload folder using huggingface_hub
f55c62a verified
Raw
History Blame Contribute Delete
112 kB
import { describe, it, expect } from 'vitest';
import {
getNestedInputValue,
evaluateTemplateCondition,
resolveRef,
applyTemplateConditionals,
resolveCredentialRefs,
} from '../templates/processors/conditionals';
// ---------------------------------------------------------------------------
// getNestedInputValue
// ---------------------------------------------------------------------------
describe('getNestedInputValue', () => {
it('returns a top-level value', () => {
expect(getNestedInputValue({ lang: 'English' }, 'lang')).toBe('English');
});
it('resolves a dot-separated path', () => {
expect(
getNestedInputValue({ proxy: { url: 'http://proxy' } }, 'proxy.url')
).toBe('http://proxy');
});
it('resolves a three-level path', () => {
expect(getNestedInputValue({ a: { b: { c: 42 } } }, 'a.b.c')).toBe(42);
});
it('returns undefined for a missing key', () => {
expect(getNestedInputValue({ a: 1 }, 'b')).toBeUndefined();
});
it('returns undefined when traversal hits null', () => {
expect(getNestedInputValue({ a: null }, 'a.b')).toBeUndefined();
});
it('returns undefined when traversal hits undefined', () => {
expect(getNestedInputValue({ a: undefined }, 'a.b')).toBeUndefined();
});
it('returns falsy value 0 without treating it as missing', () => {
expect(getNestedInputValue({ n: 0 }, 'n')).toBe(0);
});
it('returns false without treating it as missing', () => {
expect(getNestedInputValue({ flag: false }, 'flag')).toBe(false);
});
});
// ---------------------------------------------------------------------------
// evaluateTemplateCondition — bare truthiness
// ---------------------------------------------------------------------------
describe('evaluateTemplateCondition — bare truthiness', () => {
const noSvcs: string[] = [];
it('is truthy for a non-empty string', () => {
expect(evaluateTemplateCondition('inputs.x', { x: 'hello' }, noSvcs)).toBe(
true
);
});
it('is truthy for a non-zero number', () => {
expect(evaluateTemplateCondition('inputs.n', { n: 5 }, noSvcs)).toBe(true);
});
it('is truthy for numeric 0 (explicitly set value)', () => {
// 0 is NOT falsy in template conditions — it is a valid, explicitly-entered number.
// Only undefined / null / '' / false / [] are treated as falsy.
expect(evaluateTemplateCondition('inputs.n', { n: 0 }, noSvcs)).toBe(true);
});
it('is truthy for true boolean', () => {
expect(
evaluateTemplateCondition('inputs.flag', { flag: true }, noSvcs)
).toBe(true);
});
it('is truthy for a non-empty array', () => {
expect(
evaluateTemplateCondition('inputs.arr', { arr: ['a'] }, noSvcs)
).toBe(true);
});
it('is falsy for undefined', () => {
expect(evaluateTemplateCondition('inputs.x', {}, noSvcs)).toBe(false);
});
it('is falsy for null', () => {
expect(evaluateTemplateCondition('inputs.x', { x: null }, noSvcs)).toBe(
false
);
});
it('is falsy for empty string', () => {
expect(evaluateTemplateCondition('inputs.x', { x: '' }, noSvcs)).toBe(
false
);
});
it('is falsy for false', () => {
expect(
evaluateTemplateCondition('inputs.flag', { flag: false }, noSvcs)
).toBe(false);
});
it('is falsy for empty array', () => {
expect(evaluateTemplateCondition('inputs.arr', { arr: [] }, noSvcs)).toBe(
false
);
});
it('negation flips truthy to false', () => {
expect(
evaluateTemplateCondition('!inputs.flag', { flag: true }, noSvcs)
).toBe(false);
});
it('negation flips falsy to true', () => {
expect(
evaluateTemplateCondition('!inputs.flag', { flag: false }, noSvcs)
).toBe(true);
});
it('negation of missing input is true', () => {
expect(evaluateTemplateCondition('!inputs.x', {}, noSvcs)).toBe(true);
});
});
// ---------------------------------------------------------------------------
// evaluateTemplateCondition — services
// ---------------------------------------------------------------------------
describe('evaluateTemplateCondition — services', () => {
it('is true when service is in selectedSvcs', () => {
expect(
evaluateTemplateCondition('services.realdebrid', {}, ['realdebrid'])
).toBe(true);
});
it('is false when service is not in selectedSvcs', () => {
expect(
evaluateTemplateCondition('services.realdebrid', {}, ['torbox'])
).toBe(false);
});
it('negated service is false when service present', () => {
expect(evaluateTemplateCondition('!services.torbox', {}, ['torbox'])).toBe(
false
);
});
it('negated service is true when service absent', () => {
expect(evaluateTemplateCondition('!services.torbox', {}, [])).toBe(true);
});
it('services with operator always returns false (not supported)', () => {
// services.* only supports bare truthiness; operator form falls through to false
expect(
evaluateTemplateCondition('services.torbox == pro', {}, ['torbox'])
).toBe(false);
});
});
// ---------------------------------------------------------------------------
// evaluateTemplateCondition — bare services (any service selected)
// ---------------------------------------------------------------------------
describe('evaluateTemplateCondition — bare services', () => {
it('is true when at least one service is selected', () => {
expect(evaluateTemplateCondition('services', {}, ['torbox'])).toBe(true);
});
it('is true when multiple services are selected', () => {
expect(
evaluateTemplateCondition('services', {}, ['torbox', 'realdebrid'])
).toBe(true);
});
it('is false when no services are selected', () => {
expect(evaluateTemplateCondition('services', {}, [])).toBe(false);
});
it('negated: !services is false when services are present', () => {
expect(evaluateTemplateCondition('!services', {}, ['torbox'])).toBe(false);
});
it('negated: !services is true when no services', () => {
expect(evaluateTemplateCondition('!services', {}, [])).toBe(true);
});
it('and compound: services and inputs.flag — true only when both hold', () => {
expect(
evaluateTemplateCondition('services and inputs.flag', { flag: true }, [
'torbox',
])
).toBe(true);
expect(
evaluateTemplateCondition('services and inputs.flag', { flag: true }, [])
).toBe(false);
expect(
evaluateTemplateCondition('services and inputs.flag', { flag: false }, [
'torbox',
])
).toBe(false);
});
it('and compound — trailing: inputs.flag and services', () => {
expect(
evaluateTemplateCondition('inputs.flag and services', { flag: true }, [
'torbox',
])
).toBe(true);
expect(
evaluateTemplateCondition('inputs.flag and services', { flag: true }, [])
).toBe(false);
});
it('or compound — trailing: inputs.flag or services', () => {
// flag false, no services → false
expect(
evaluateTemplateCondition('inputs.flag or services', { flag: false }, [])
).toBe(false);
// flag false, services present → true
expect(
evaluateTemplateCondition('inputs.flag or services', { flag: false }, [
'torbox',
])
).toBe(true);
});
});
// ---------------------------------------------------------------------------
// evaluateTemplateCondition — == operator
// ---------------------------------------------------------------------------
describe('evaluateTemplateCondition — == operator', () => {
const noSvcs: string[] = [];
it('matches a string value', () => {
expect(
evaluateTemplateCondition('inputs.tier == pro', { tier: 'pro' }, noSvcs)
).toBe(true);
});
it('does not match a different value', () => {
expect(
evaluateTemplateCondition('inputs.tier == pro', { tier: 'basic' }, noSvcs)
).toBe(false);
});
it('does not match when rhs is empty (op regex requires ≥1 char on rhs)', () => {
// The operator regex requires at least one character on the right-hand side.
// "inputs.x == " (empty rhs) falls through to bare truthiness, which is false
// for an undefined input.
expect(evaluateTemplateCondition('inputs.x == ', {}, noSvcs)).toBe(false);
});
it('negated equality: false when equal', () => {
expect(
evaluateTemplateCondition('!inputs.tier == pro', { tier: 'pro' }, noSvcs)
).toBe(false);
});
it('negated equality: true when not equal', () => {
expect(
evaluateTemplateCondition(
'!inputs.tier == pro',
{ tier: 'basic' },
noSvcs
)
).toBe(true);
});
it('works with subsection dot-path', () => {
expect(
evaluateTemplateCondition(
'inputs.proxy.type == socks5',
{ proxy: { type: 'socks5' } },
noSvcs
)
).toBe(true);
});
});
// ---------------------------------------------------------------------------
// evaluateTemplateCondition — != operator
// ---------------------------------------------------------------------------
describe('evaluateTemplateCondition — != operator', () => {
const noSvcs: string[] = [];
it('is true when value does not match', () => {
expect(
evaluateTemplateCondition(
'inputs.lang != none',
{ lang: 'English' },
noSvcs
)
).toBe(true);
});
it('is false when value matches', () => {
expect(
evaluateTemplateCondition('inputs.lang != none', { lang: 'none' }, noSvcs)
).toBe(false);
});
it('undefined treated as empty string', () => {
expect(evaluateTemplateCondition('inputs.lang != none', {}, noSvcs)).toBe(
true
);
});
});
// ---------------------------------------------------------------------------
// evaluateTemplateCondition — includes operator
// ---------------------------------------------------------------------------
describe('evaluateTemplateCondition — includes operator', () => {
const noSvcs: string[] = [];
it('is true when array contains the value', () => {
expect(
evaluateTemplateCondition(
'inputs.filters includes dvPassthrough',
{ filters: ['dvPassthrough', 'nzbOnly'] },
noSvcs
)
).toBe(true);
});
it('is false when array does not contain the value', () => {
expect(
evaluateTemplateCondition(
'inputs.filters includes sdrPassthrough',
{ filters: ['dvPassthrough'] },
noSvcs
)
).toBe(false);
});
it('works on a string value (substring match)', () => {
expect(
evaluateTemplateCondition(
'inputs.flags includes foo',
{ flags: 'foobar' },
noSvcs
)
).toBe(true);
});
it('is false when value is undefined', () => {
expect(
evaluateTemplateCondition(
'inputs.filters includes dvPassthrough',
{},
noSvcs
)
).toBe(false);
});
it('negation of includes', () => {
expect(
evaluateTemplateCondition(
'!inputs.filters includes dvPassthrough',
{ filters: ['dvPassthrough'] },
noSvcs
)
).toBe(false);
});
});
// ---------------------------------------------------------------------------
// resolveRef
// ---------------------------------------------------------------------------
describe('resolveRef', () => {
it('resolves an inputs reference', () => {
expect(resolveRef('inputs.lang', { lang: 'French' }, [])).toBe('French');
});
it('resolves a nested inputs reference', () => {
expect(
resolveRef('inputs.proxy.url', { proxy: { url: 'http://x' } }, [])
).toBe('http://x');
});
it('resolves a services reference to true when present', () => {
expect(resolveRef('services.torbox', {}, ['torbox'])).toBe(true);
});
it('resolves a services reference to false when absent', () => {
expect(resolveRef('services.torbox', {}, [])).toBe(false);
});
it('returns undefined for unknown namespace', () => {
expect(resolveRef('unknown.thing', {}, [])).toBeUndefined();
});
it('bare services: resolves to the selected services array when services present', () => {
expect(resolveRef('services', {}, ['torbox'])).toEqual(['torbox']);
expect(resolveRef('services', {}, ['torbox', 'realdebrid'])).toEqual([
'torbox',
'realdebrid',
]);
});
it('bare services: resolves to an empty array when no services selected', () => {
expect(resolveRef('services', {}, [])).toEqual([]);
});
});
// ---------------------------------------------------------------------------
// applyTemplateConditionals — __if
// ---------------------------------------------------------------------------
describe('applyTemplateConditionals — __if', () => {
const noSvcs: string[] = [];
it('includes an object when __if condition is true', () => {
const result = applyTemplateConditionals(
[{ __if: 'inputs.flag', value: 'A' }],
{ flag: true },
noSvcs
);
expect(result).toEqual([{ value: 'A' }]);
});
it('excludes an object when __if condition is false', () => {
const result = applyTemplateConditionals(
[{ __if: 'inputs.flag', value: 'A' }],
{ flag: false },
noSvcs
);
expect(result).toEqual([]);
});
it('strips the __if key from the kept object', () => {
const result = applyTemplateConditionals(
[{ __if: 'inputs.flag', expression: 'expr1', enabled: true }],
{ flag: true },
noSvcs
);
expect(result[0]).not.toHaveProperty('__if');
expect(result[0]).toEqual({ expression: 'expr1', enabled: true });
});
it('keeps non-conditional objects untouched', () => {
const result = applyTemplateConditionals(
[
{ expression: 'always' },
{ __if: 'inputs.flag', expression: 'conditional' },
],
{ flag: false },
noSvcs
);
expect(result).toEqual([{ expression: 'always' }]);
});
it('handles multiple conditions independently', () => {
const result = applyTemplateConditionals(
[
{ __if: 'inputs.a', value: 'A' },
{ __if: 'inputs.b', value: 'B' },
{ __if: 'inputs.c', value: 'C' },
],
{ a: true, b: false, c: true },
noSvcs
);
expect(result).toEqual([{ value: 'A' }, { value: 'C' }]);
});
it('works with services.* condition', () => {
const result = applyTemplateConditionals(
[
{ __if: 'services.torbox', type: 'torbox-search' },
{ __if: 'services.realdebrid', type: 'rd-addon' },
],
{},
['torbox']
);
expect(result).toEqual([{ type: 'torbox-search' }]);
});
it('works with == operator condition', () => {
const result = applyTemplateConditionals(
[
{ __if: 'inputs.tier == pro', expression: 'pro-expr' },
{ __if: 'inputs.tier == basic', expression: 'basic-expr' },
],
{ tier: 'pro' },
noSvcs
);
expect(result).toEqual([{ expression: 'pro-expr' }]);
});
it('works with includes operator condition', () => {
const result = applyTemplateConditionals(
[
{ __if: 'inputs.opts includes dvPassthrough', expression: 'dv' },
{ __if: 'inputs.opts includes sdrPassthrough', expression: 'sdr' },
],
{ opts: ['dvPassthrough'] },
noSvcs
);
expect(result).toEqual([{ expression: 'dv' }]);
});
it('recurses into kept object values', () => {
const result = applyTemplateConditionals(
[
{
__if: 'inputs.flag',
options: { name: '{{inputs.label}}' },
},
],
{ flag: true, label: 'MyLabel' },
noSvcs
);
expect(result).toEqual([{ options: { name: 'MyLabel' } }]);
});
});
// ---------------------------------------------------------------------------
// applyTemplateConditionals — __value
// ---------------------------------------------------------------------------
describe('applyTemplateConditionals — __value', () => {
const noSvcs: string[] = [];
it('injects a single string when __if passes', () => {
const result = applyTemplateConditionals(
['3D', { __if: 'inputs.excludeDV', __value: 'DV' }],
{ excludeDV: true },
noSvcs
);
expect(result).toEqual(['3D', 'DV']);
});
it('omits the value when __if fails', () => {
const result = applyTemplateConditionals(
['3D', { __if: 'inputs.excludeDV', __value: 'DV' }],
{ excludeDV: false },
noSvcs
);
expect(result).toEqual(['3D']);
});
it('spreads an array __value when __if passes', () => {
const result = applyTemplateConditionals(
[
'3D',
{ __if: 'inputs.excludeHdr', __value: ['HDR', 'HDR10', 'HDR10+'] },
],
{ excludeHdr: true },
noSvcs
);
expect(result).toEqual(['3D', 'HDR', 'HDR10', 'HDR10+']);
});
it('omits array __value when __if fails', () => {
const result = applyTemplateConditionals(
[
'3D',
{ __if: 'inputs.excludeHdr', __value: ['HDR', 'HDR10', 'HDR10+'] },
],
{ excludeHdr: false },
noSvcs
);
expect(result).toEqual(['3D']);
});
it('handles multiple independent __value items', () => {
const result = applyTemplateConditionals(
[
'3D',
{ __if: 'inputs.excludeDV', __value: 'DV' },
{ __if: 'inputs.excludeHdr', __value: ['HDR', 'HDR10', 'HDR10+'] },
{ __if: 'inputs.excludeAi', __value: 'AI' },
],
{ excludeDV: true, excludeHdr: true, excludeAi: false },
noSvcs
);
expect(result).toEqual(['3D', 'DV', 'HDR', 'HDR10', 'HDR10+']);
});
it('injects __value without __if unconditionally (string)', () => {
const result = applyTemplateConditionals(
[{ __value: 'always' }, 'other'],
{},
noSvcs
);
expect(result).toEqual(['always', 'other']);
});
it('spreads __value without __if unconditionally (array)', () => {
const result = applyTemplateConditionals(
[{ __value: ['H-OU', 'H-SBS'] }, 'other'],
{},
noSvcs
);
expect(result).toEqual(['H-OU', 'H-SBS', 'other']);
});
it('supports {{}} interpolation inside __value string', () => {
const result = applyTemplateConditionals(
[{ __if: 'inputs.lang != none', __value: '{{inputs.lang}}' }],
{ lang: 'French' },
noSvcs
);
expect(result).toEqual(['French']);
});
it('does not insert a nested object when __value is used', () => {
const result = applyTemplateConditionals(
[{ __if: 'inputs.flag', __value: 'X' }],
{ flag: true },
noSvcs
);
// Must be a flat string, not an object
expect(result).toEqual(['X']);
expect(typeof result[0]).toBe('string');
});
});
// ---------------------------------------------------------------------------
// applyTemplateConditionals — __switch
// ---------------------------------------------------------------------------
describe('applyTemplateConditionals — __switch', () => {
const noSvcs: string[] = [];
it('returns the matching case', () => {
const result = applyTemplateConditionals(
{
formatter: {
__switch: 'inputs.style',
cases: { torrentio: { id: 'torrentio' }, gdrive: { id: 'gdrive' } },
default: { id: 'prism' },
},
},
{ style: 'torrentio' },
noSvcs
);
expect(result.formatter).toEqual({ id: 'torrentio' });
});
it('falls back to default when no case matches', () => {
const result = applyTemplateConditionals(
{
formatter: {
__switch: 'inputs.style',
cases: { torrentio: { id: 'torrentio' } },
default: { id: 'prism' },
},
},
{ style: 'unknown' },
noSvcs
);
expect(result.formatter).toEqual({ id: 'prism' });
});
it('returns null when no case matches and no default', () => {
const result = applyTemplateConditionals(
{
formatter: {
__switch: 'inputs.style',
cases: { torrentio: { id: 'torrentio' } },
},
},
{ style: 'unknown' },
noSvcs
);
expect(result.formatter).toBeNull();
});
it('recurses into the chosen case value', () => {
const result = applyTemplateConditionals(
{
__switch: 'inputs.tier',
cases: {
pro: {
limit: '{{inputs.proLimit}}',
},
},
default: { limit: '0' },
},
{ tier: 'pro', proLimit: '1TB' },
noSvcs
);
expect(result).toEqual({ limit: '1TB' });
});
it('handles __if inside a __switch case', () => {
const result = applyTemplateConditionals(
{
__switch: 'inputs.mode',
cases: {
advanced: {
items: [
{ __if: 'inputs.showExtra', label: 'extra' },
{ label: 'base' },
],
},
},
default: { items: [] },
},
{ mode: 'advanced', showExtra: false },
noSvcs
);
expect(result).toEqual({ items: [{ label: 'base' }] });
});
it('resolves __switch for services ref', () => {
const result = applyTemplateConditionals(
{
__switch: 'services.torbox',
cases: { true: { id: 'tb-addon' } },
default: { id: 'generic' },
},
{},
['torbox']
);
expect(result).toEqual({ id: 'tb-addon' });
});
it('resolves __switch for bare services ref: falls to default when no services (empty array key)', () => {
// String([]) == "" — not found in cases → uses default
const result = applyTemplateConditionals(
{
__switch: 'services',
cases: { '': { mode: 'p2p' } },
default: { mode: 'debrid' },
},
{},
[]
);
expect(result).toEqual({ mode: 'p2p' });
});
it('resolves __switch for bare services ref: default used when services present (no matching case)', () => {
// String(["torbox"]) == "torbox" — not in cases → uses default
const result = applyTemplateConditionals(
{
__switch: 'services',
cases: { '': { mode: 'p2p' } },
default: { mode: 'debrid' },
},
{},
['torbox']
);
expect(result).toEqual({ mode: 'debrid' });
});
it('stringifies number input for case matching', () => {
const result = applyTemplateConditionals(
{
__switch: 'inputs.count',
cases: { '5': { label: 'five' } },
default: { label: 'other' },
},
{ count: 5 },
noSvcs
);
expect(result).toEqual({ label: 'five' });
});
});
// ---------------------------------------------------------------------------
// applyTemplateConditionals — {{}} interpolation
// ---------------------------------------------------------------------------
describe('applyTemplateConditionals — interpolation', () => {
const noSvcs: string[] = [];
it('{{services}} sole token: returns selected services array when services present', () => {
expect(applyTemplateConditionals('{{services}}', {}, ['torbox'])).toEqual([
'torbox',
]);
});
it('{{services}} sole token: returns empty array when no services', () => {
expect(applyTemplateConditionals('{{services}}', {}, [])).toEqual([]);
});
it('{{services}} multi-token: stringifies as comma-joined IDs', () => {
expect(applyTemplateConditionals('mode={{services}}', {}, ['torbox'])).toBe(
'mode=torbox'
);
expect(applyTemplateConditionals('mode={{services}}', {}, [])).toBe(
'mode='
);
});
it('sole token: preserves a string value', () => {
expect(
applyTemplateConditionals('{{inputs.name}}', { name: 'Test' }, noSvcs)
).toBe('Test');
});
it('sole token: preserves a number value', () => {
expect(
applyTemplateConditionals('{{inputs.limit}}', { limit: 42 }, noSvcs)
).toBe(42);
});
it('sole token: preserves a boolean value', () => {
expect(
applyTemplateConditionals('{{inputs.flag}}', { flag: true }, noSvcs)
).toBe(true);
});
it('sole token: preserves an array value', () => {
expect(
applyTemplateConditionals(
'{{inputs.langs}}',
{ langs: ['en', 'fr'] },
noSvcs
)
).toEqual(['en', 'fr']);
});
it('sole token in array: spreads an array value into parent', () => {
const result = applyTemplateConditionals(
['{{inputs.langs}}', 'Original', 'Unknown'],
{ langs: ['French', 'German'] },
noSvcs
);
expect(result).toEqual(['French', 'German', 'Original', 'Unknown']);
});
it('sole token: returns empty string for undefined', () => {
expect(applyTemplateConditionals('{{inputs.missing}}', {}, noSvcs)).toBe(
''
);
});
it('sole token: services ref returns boolean', () => {
expect(
applyTemplateConditionals('{{services.torbox}}', {}, ['torbox'])
).toBe(true);
expect(applyTemplateConditionals('{{services.torbox}}', {}, [])).toBe(
false
);
});
it('multi-token: concatenates string values', () => {
expect(
applyTemplateConditionals(
'/*{{inputs.label}} Passthrough*/ passthrough()',
{ label: 'English' },
noSvcs
)
).toBe('/*English Passthrough*/ passthrough()');
});
it('multi-token: stringifies non-string values', () => {
expect(
applyTemplateConditionals('count={{inputs.n}}', { n: 7 }, noSvcs)
).toBe('count=7');
});
it('multi-token: resolves multiple tokens in one string', () => {
expect(
applyTemplateConditionals(
'{{inputs.a}}_{{inputs.b}}',
{ a: 'hello', b: 'world' },
noSvcs
)
).toBe('hello_world');
});
it('multi-token: missing reference resolves to empty string', () => {
expect(
applyTemplateConditionals('prefix_{{inputs.missing}}_suffix', {}, noSvcs)
).toBe('prefix__suffix');
});
it('multi-token: services ref resolves to "true"/"false" string', () => {
expect(
applyTemplateConditionals('svc={{services.torbox}}', {}, ['torbox'])
).toBe('svc=true');
});
// --- credential refs: {{services.X.Y}} ---
it('sole token: services credential ref is preserved unchanged (service selected)', () => {
expect(
applyTemplateConditionals('{{services.torbox.apiKey}}', {}, ['torbox'])
).toBe('{{services.torbox.apiKey}}');
});
it('sole token: services credential ref is preserved unchanged (service NOT selected)', () => {
expect(
applyTemplateConditionals('{{services.torbox.apiKey}}', {}, [])
).toBe('{{services.torbox.apiKey}}');
});
it('multi-token: services credential ref is preserved inside a larger string', () => {
expect(
applyTemplateConditionals('key={{services.torbox.apiKey}}&other=1', {}, [
'torbox',
])
).toBe('key={{services.torbox.apiKey}}&other=1');
});
it('sole token: services boolean ref (no credential) still returns boolean', () => {
expect(
applyTemplateConditionals('{{services.torbox}}', {}, ['torbox'])
).toBe(true);
expect(applyTemplateConditionals('{{services.torbox}}', {}, [])).toBe(
false
);
});
it('object with credential ref field is preserved through full traversal', () => {
// __if (without __value) is only evaluated for array items, so wrap in array.
const config = [
{
__if: 'services.torbox',
type: 'newznab',
options: { apiKey: '{{services.torbox.apiKey}}' },
},
];
const result = applyTemplateConditionals(config, {}, ['torbox']);
// __if passes (torbox selected), apiKey ref preserved for later resolution
expect(result).toEqual([
{
type: 'newznab',
options: { apiKey: '{{services.torbox.apiKey}}' },
},
]);
});
it('credential ref inside a __switch case is preserved', () => {
const config = {
__switch: 'inputs.service',
cases: {
torbox: { apiKey: '{{services.torbox.apiKey}}' },
},
default: { apiKey: '' },
};
const result = applyTemplateConditionals(config, { service: 'torbox' }, [
'torbox',
]);
expect(result).toEqual({ apiKey: '{{services.torbox.apiKey}}' });
});
}); // applyTemplateConditionals — interpolation
// ---------------------------------------------------------------------------
// applyTemplateConditionals — general traversal
// ---------------------------------------------------------------------------
describe('applyTemplateConditionals — general traversal', () => {
const noSvcs: string[] = [];
it('passes through primitives unchanged', () => {
expect(applyTemplateConditionals(42, {}, noSvcs)).toBe(42);
expect(applyTemplateConditionals(true, {}, noSvcs)).toBe(true);
expect(applyTemplateConditionals(null, {}, noSvcs)).toBeNull();
});
it('walks object keys recursively', () => {
const result = applyTemplateConditionals(
{
outer: {
inner: '{{inputs.x}}',
},
},
{ x: 'resolved' },
noSvcs
);
expect(result).toEqual({ outer: { inner: 'resolved' } });
});
it('handles deeply nested __if inside objects inside arrays', () => {
const result = applyTemplateConditionals(
{
sections: [
{
items: [{ __if: 'inputs.show', label: '{{inputs.name}}' }],
},
],
},
{ show: true, name: 'Foo' },
noSvcs
);
expect(result).toEqual({ sections: [{ items: [{ label: 'Foo' }] }] });
});
it('handles a realistic config slice', () => {
const config = {
preferredLanguages: ['{{inputs.languages}}', 'Original', 'Unknown'],
excludedVisualTags: [
'3D',
{ __if: 'inputs.excludeDV', __value: 'DV' },
{ __if: 'inputs.excludeHdr', __value: ['HDR', 'HDR10'] },
],
formatter: {
__switch: 'inputs.formatterStyle',
cases: {
custom: {
id: 'custom',
definition: { name: '{{inputs.formatterName}}' },
},
},
default: { id: 'default' },
},
presets: [
{ type: 'always' },
{
__if: 'inputs.includeDebridio',
type: 'debridio',
options: { apiKey: '{{inputs.debridioKey}}' },
},
],
};
const result = applyTemplateConditionals(
config,
{
languages: ['French', 'German'],
excludeDV: true,
excludeHdr: false,
formatterStyle: 'custom',
formatterName: 'MyFormatter',
includeDebridio: true,
debridioKey: 'key123',
},
noSvcs
);
expect(result.preferredLanguages).toEqual([
'French',
'German',
'Original',
'Unknown',
]);
expect(result.excludedVisualTags).toEqual(['3D', 'DV']);
expect(result.formatter).toEqual({
id: 'custom',
definition: { name: 'MyFormatter' },
});
expect(result.presets).toEqual([
{ type: 'always' },
{ type: 'debridio', options: { apiKey: 'key123' } },
]);
});
it('passes through a plain number inside an array', () => {
expect(applyTemplateConditionals([1, 2, 3], {}, noSvcs)).toEqual([1, 2, 3]);
});
it('sole {{}} token resolving to empty array spreads nothing into parent', () => {
const result = applyTemplateConditionals(
['{{inputs.langs}}', 'Original'],
{ langs: [] },
noSvcs
);
// Empty array spreads as nothing; only 'Original' remains
expect(result).toEqual(['Original']);
});
});
// ---------------------------------------------------------------------------
// Edge cases — untested branches
// ---------------------------------------------------------------------------
describe('evaluateTemplateCondition — edge cases', () => {
const noSvcs: string[] = [];
it('returns false when condition has no dot (invalid form)', () => {
// No dot means dotIdx === -1; function returns `negated` which is false
expect(evaluateTemplateCondition('noDot', {}, noSvcs)).toBe(false);
});
it('negation returns true when condition has no dot', () => {
expect(evaluateTemplateCondition('!noDot', {}, noSvcs)).toBe(true);
});
it('returns false for unknown namespace in bare form', () => {
// ns is neither 'inputs' nor 'services' — result stays false
expect(evaluateTemplateCondition('unknown.key', {}, noSvcs)).toBe(false);
});
it('negation of unknown namespace returns true', () => {
expect(evaluateTemplateCondition('!unknown.key', {}, noSvcs)).toBe(true);
});
});
// ---------------------------------------------------------------------------
// evaluateTemplateCondition — compound operators (and / or / xor)
// ---------------------------------------------------------------------------
describe('evaluateTemplateCondition — and operator', () => {
const noSvcs: string[] = [];
it('true and true → true', () => {
expect(
evaluateTemplateCondition(
'inputs.a and inputs.b',
{ a: true, b: true },
noSvcs
)
).toBe(true);
});
it('true and false → false', () => {
expect(
evaluateTemplateCondition(
'inputs.a and inputs.b',
{ a: true, b: false },
noSvcs
)
).toBe(false);
});
it('false and true → false', () => {
expect(
evaluateTemplateCondition(
'inputs.a and inputs.b',
{ a: false, b: true },
noSvcs
)
).toBe(false);
});
it('false and false → false', () => {
expect(
evaluateTemplateCondition(
'inputs.a and inputs.b',
{ a: false, b: false },
noSvcs
)
).toBe(false);
});
it('three operands: all true → true', () => {
expect(
evaluateTemplateCondition(
'inputs.a and inputs.b and inputs.c',
{ a: 1, b: 2, c: 3 },
noSvcs
)
).toBe(true);
});
it('three operands: one false → false', () => {
expect(
evaluateTemplateCondition(
'inputs.a and inputs.b and inputs.c',
{ a: 1, b: false, c: 3 },
noSvcs
)
).toBe(false);
});
it('negated sub-expression: !inputs.a and inputs.b', () => {
expect(
evaluateTemplateCondition(
'!inputs.a and inputs.b',
{ a: false, b: true },
noSvcs
)
).toBe(true);
expect(
evaluateTemplateCondition(
'!inputs.a and inputs.b',
{ a: true, b: true },
noSvcs
)
).toBe(false);
});
it('works with operator sub-expressions: inputs.tier == pro and inputs.flag', () => {
expect(
evaluateTemplateCondition(
'inputs.tier == pro and inputs.flag',
{ tier: 'pro', flag: true },
noSvcs
)
).toBe(true);
expect(
evaluateTemplateCondition(
'inputs.tier == pro and inputs.flag',
{ tier: 'pro', flag: false },
noSvcs
)
).toBe(false);
});
it('works with services sub-expressions', () => {
expect(
evaluateTemplateCondition(
'services.torbox and inputs.flag',
{ flag: true },
['torbox']
)
).toBe(true);
expect(
evaluateTemplateCondition(
'services.torbox and inputs.flag',
{ flag: true },
[]
)
).toBe(false);
});
});
describe('evaluateTemplateCondition — or operator', () => {
const noSvcs: string[] = [];
it('true or false → true', () => {
expect(
evaluateTemplateCondition(
'inputs.a or inputs.b',
{ a: true, b: false },
noSvcs
)
).toBe(true);
});
it('false or true → true', () => {
expect(
evaluateTemplateCondition(
'inputs.a or inputs.b',
{ a: false, b: true },
noSvcs
)
).toBe(true);
});
it('false or false → false', () => {
expect(
evaluateTemplateCondition(
'inputs.a or inputs.b',
{ a: false, b: false },
noSvcs
)
).toBe(false);
});
it('true or true → true', () => {
expect(
evaluateTemplateCondition(
'inputs.a or inputs.b',
{ a: true, b: true },
noSvcs
)
).toBe(true);
});
it('three operands: all false → false', () => {
expect(
evaluateTemplateCondition(
'inputs.a or inputs.b or inputs.c',
{ a: false, b: false, c: false },
noSvcs
)
).toBe(false);
});
it('three operands: one true → true', () => {
expect(
evaluateTemplateCondition(
'inputs.a or inputs.b or inputs.c',
{ a: false, b: true, c: false },
noSvcs
)
).toBe(true);
});
it('works with == operator sub-expressions', () => {
expect(
evaluateTemplateCondition(
'inputs.tier == pro or inputs.tier == premium',
{ tier: 'premium' },
noSvcs
)
).toBe(true);
expect(
evaluateTemplateCondition(
'inputs.tier == pro or inputs.tier == premium',
{ tier: 'basic' },
noSvcs
)
).toBe(false);
});
it('works with services', () => {
expect(
evaluateTemplateCondition('services.torbox or services.realdebrid', {}, [
'realdebrid',
])
).toBe(true);
expect(
evaluateTemplateCondition(
'services.torbox or services.realdebrid',
{},
[]
)
).toBe(false);
});
});
describe('evaluateTemplateCondition — xor operator', () => {
const noSvcs: string[] = [];
it('true xor false → true', () => {
expect(
evaluateTemplateCondition(
'inputs.a xor inputs.b',
{ a: true, b: false },
noSvcs
)
).toBe(true);
});
it('false xor true → true', () => {
expect(
evaluateTemplateCondition(
'inputs.a xor inputs.b',
{ a: false, b: true },
noSvcs
)
).toBe(true);
});
it('true xor true → false (both true cancels out)', () => {
expect(
evaluateTemplateCondition(
'inputs.a xor inputs.b',
{ a: true, b: true },
noSvcs
)
).toBe(false);
});
it('false xor false → false', () => {
expect(
evaluateTemplateCondition(
'inputs.a xor inputs.b',
{ a: false, b: false },
noSvcs
)
).toBe(false);
});
it('three operands: odd count of true → true', () => {
// true xor true xor true → count=3 (odd) → true
expect(
evaluateTemplateCondition(
'inputs.a xor inputs.b xor inputs.c',
{ a: true, b: true, c: true },
noSvcs
)
).toBe(true);
});
it('three operands: even count of true → false', () => {
// true xor true xor false → count=2 (even) → false
expect(
evaluateTemplateCondition(
'inputs.a xor inputs.b xor inputs.c',
{ a: true, b: true, c: false },
noSvcs
)
).toBe(false);
});
});
describe('evaluateTemplateCondition — compound keywords inside operator values', () => {
const noSvcs: string[] = [];
it('== value containing " and " is not split as compound', () => {
// "action and adventure" should be treated as the full rhs, not split
expect(
evaluateTemplateCondition(
'inputs.genre == action and adventure',
{ genre: 'action and adventure' },
noSvcs
)
).toBe(true);
// Should not match just "action"
expect(
evaluateTemplateCondition(
'inputs.genre == action and adventure',
{ genre: 'action' },
noSvcs
)
).toBe(false);
});
it('== value containing " or " is not split as compound', () => {
expect(
evaluateTemplateCondition(
'inputs.title == foo or bar',
{ title: 'foo or bar' },
noSvcs
)
).toBe(true);
expect(
evaluateTemplateCondition(
'inputs.title == foo or bar',
{ title: 'foo' },
noSvcs
)
).toBe(false);
});
it('includes value containing " or " is not split as compound', () => {
// Checks if the array contains the literal string "foo or bar"
expect(
evaluateTemplateCondition(
'inputs.tags includes foo or bar',
{ tags: ['foo or bar', 'baz'] },
noSvcs
)
).toBe(true);
expect(
evaluateTemplateCondition(
'inputs.tags includes foo or bar',
{ tags: ['foo', 'bar'] },
noSvcs
)
).toBe(false);
});
it('!= value containing " and " is not split as compound', () => {
expect(
evaluateTemplateCondition(
'inputs.x != action and adventure',
{ x: 'something else' },
noSvcs
)
).toBe(true);
expect(
evaluateTemplateCondition(
'inputs.x != action and adventure',
{ x: 'action and adventure' },
noSvcs
)
).toBe(false);
});
it('genuine compound after a value-with-keyword still works', () => {
// "inputs.genre == action and adventure or inputs.flag"
// Should split on " or " before "inputs.flag", giving:
// sub1: "inputs.genre == action and adventure" (value is "action and adventure")
// sub2: "inputs.flag"
expect(
evaluateTemplateCondition(
'inputs.genre == action and adventure or inputs.flag',
{ genre: 'other', flag: true },
noSvcs
)
).toBe(true); // sub2 is true
expect(
evaluateTemplateCondition(
'inputs.genre == action and adventure or inputs.flag',
{ genre: 'action and adventure', flag: false },
noSvcs
)
).toBe(true); // sub1 is true
expect(
evaluateTemplateCondition(
'inputs.genre == action and adventure or inputs.flag',
{ genre: 'other', flag: false },
noSvcs
)
).toBe(false); // both false
});
});
describe('evaluateTemplateCondition — compound operator precedence (or < xor < and)', () => {
const noSvcs: string[] = [];
it('and has higher precedence than or: a or b and c = a or (b and c)', () => {
// a=false, b=true, c=true → false or (true and true) = true
expect(
evaluateTemplateCondition(
'inputs.a or inputs.b and inputs.c',
{ a: false, b: true, c: true },
noSvcs
)
).toBe(true);
// a=false, b=true, c=false → false or (true and false) = false
expect(
evaluateTemplateCondition(
'inputs.a or inputs.b and inputs.c',
{ a: false, b: true, c: false },
noSvcs
)
).toBe(false);
});
it('and has higher precedence than xor: a xor b and c = a xor (b and c)', () => {
// a=true, b=true, c=true → true xor (true and true) = true xor true = false
expect(
evaluateTemplateCondition(
'inputs.a xor inputs.b and inputs.c',
{ a: true, b: true, c: true },
noSvcs
)
).toBe(false);
// a=true, b=false, c=true → true xor (false and true) = true xor false = true
expect(
evaluateTemplateCondition(
'inputs.a xor inputs.b and inputs.c',
{ a: true, b: false, c: true },
noSvcs
)
).toBe(true);
});
it('practical: (services.torbox or services.realdebrid) and inputs.premium', () => {
// Use explicit `and` + `or`: torbox or realdebrid and inputs.premium
// = torbox or (realdebrid and premium)
// torbox=true, realdebrid=false, premium=false → true or false = true
expect(
evaluateTemplateCondition(
'services.torbox or services.realdebrid and inputs.premium',
{ premium: false },
['torbox']
)
).toBe(true);
// torbox=false, realdebrid=true, premium=true → false or true = true
expect(
evaluateTemplateCondition(
'services.torbox or services.realdebrid and inputs.premium',
{ premium: true },
['realdebrid']
)
).toBe(true);
// torbox=false, realdebrid=true, premium=false → false or false = false
expect(
evaluateTemplateCondition(
'services.torbox or services.realdebrid and inputs.premium',
{ premium: false },
['realdebrid']
)
).toBe(false);
});
});
// ---------------------------------------------------------------------------
// evaluateTemplateCondition — numeric comparison operators
// ---------------------------------------------------------------------------
describe('evaluateTemplateCondition — numeric comparison operators', () => {
const noSvcs: string[] = [];
it('>: true when value exceeds threshold', () => {
expect(evaluateTemplateCondition('inputs.n > 5', { n: 6 }, noSvcs)).toBe(
true
);
});
it('>: false when value equals threshold', () => {
expect(evaluateTemplateCondition('inputs.n > 5', { n: 5 }, noSvcs)).toBe(
false
);
});
it('>: false when value is below threshold', () => {
expect(evaluateTemplateCondition('inputs.n > 5', { n: 4 }, noSvcs)).toBe(
false
);
});
it('>=: true when value equals threshold', () => {
expect(evaluateTemplateCondition('inputs.n >= 5', { n: 5 }, noSvcs)).toBe(
true
);
});
it('>=: true when value exceeds threshold', () => {
expect(evaluateTemplateCondition('inputs.n >= 5', { n: 10 }, noSvcs)).toBe(
true
);
});
it('>=: false when value is below threshold', () => {
expect(evaluateTemplateCondition('inputs.n >= 5', { n: 4 }, noSvcs)).toBe(
false
);
});
it('<: true when value is below threshold', () => {
expect(evaluateTemplateCondition('inputs.n < 10', { n: 9 }, noSvcs)).toBe(
true
);
});
it('<: false when value equals threshold', () => {
expect(evaluateTemplateCondition('inputs.n < 10', { n: 10 }, noSvcs)).toBe(
false
);
});
it('<=: true when value equals threshold', () => {
expect(evaluateTemplateCondition('inputs.n <= 10', { n: 10 }, noSvcs)).toBe(
true
);
});
it('<=: false when value exceeds threshold', () => {
expect(evaluateTemplateCondition('inputs.n <= 10', { n: 11 }, noSvcs)).toBe(
false
);
});
it('negation: !inputs.n > 5 is true when n <= 5', () => {
expect(evaluateTemplateCondition('!inputs.n > 5', { n: 3 }, noSvcs)).toBe(
true
);
expect(evaluateTemplateCondition('!inputs.n > 5', { n: 6 }, noSvcs)).toBe(
false
);
});
it('works with decimal thresholds', () => {
expect(
evaluateTemplateCondition('inputs.ratio >= 1.5', { ratio: 1.6 }, noSvcs)
).toBe(true);
expect(
evaluateTemplateCondition('inputs.ratio >= 1.5', { ratio: 1.4 }, noSvcs)
).toBe(false);
});
it('works with negative thresholds', () => {
expect(
evaluateTemplateCondition('inputs.score > -1', { score: 0 }, noSvcs)
).toBe(true);
expect(
evaluateTemplateCondition('inputs.score > -1', { score: -2 }, noSvcs)
).toBe(false);
});
it('coerces string-valued number inputs', () => {
// Number inputs from text fields may arrive as strings
expect(
evaluateTemplateCondition('inputs.limit >= 3', { limit: '5' }, noSvcs)
).toBe(true);
expect(
evaluateTemplateCondition('inputs.limit >= 3', { limit: '2' }, noSvcs)
).toBe(false);
});
it('returns false for non-numeric value (NaN guard)', () => {
expect(
evaluateTemplateCondition('inputs.n > 5', { n: 'hello' }, noSvcs)
).toBe(false);
});
it('returns false for undefined value', () => {
expect(evaluateTemplateCondition('inputs.n > 5', {}, noSvcs)).toBe(false);
});
it('services.* with numeric operator returns false (not supported)', () => {
// ns !== 'inputs' → result stays false
expect(
evaluateTemplateCondition('services.torbox > 0', {}, ['torbox'])
).toBe(false);
});
it('combined with and: inputs.n >= 1 and inputs.n <= 10 (range check)', () => {
expect(
evaluateTemplateCondition(
'inputs.n >= 1 and inputs.n <= 10',
{ n: 5 },
noSvcs
)
).toBe(true);
expect(
evaluateTemplateCondition(
'inputs.n >= 1 and inputs.n <= 10',
{ n: 0 },
noSvcs
)
).toBe(false);
expect(
evaluateTemplateCondition(
'inputs.n >= 1 and inputs.n <= 10',
{ n: 11 },
noSvcs
)
).toBe(false);
});
it('numeric and + or: (A > 3 and A < 5) or B == torbox — and binds tighter than or', () => {
// resultLimit=4 is in (3,5), service != torbox → true or false = true
expect(
evaluateTemplateCondition(
'inputs.resultLimit > 3 and inputs.resultLimit < 5 or inputs.service == torbox',
{ resultLimit: 4, service: 'other' },
noSvcs
)
).toBe(true);
// resultLimit=6 is NOT in (3,5), service == torbox → false or true = true
expect(
evaluateTemplateCondition(
'inputs.resultLimit > 3 and inputs.resultLimit < 5 or inputs.service == torbox',
{ resultLimit: 6, service: 'torbox' },
noSvcs
)
).toBe(true);
// resultLimit=6 is NOT in (3,5), service != torbox → false or false = false
expect(
evaluateTemplateCondition(
'inputs.resultLimit > 3 and inputs.resultLimit < 5 or inputs.service == torbox',
{ resultLimit: 6, service: 'other' },
noSvcs
)
).toBe(false);
// resultLimit=4 in (3,5), service == torbox → true or true = true
expect(
evaluateTemplateCondition(
'inputs.resultLimit > 3 and inputs.resultLimit < 5 or inputs.service == torbox',
{ resultLimit: 4, service: 'torbox' },
noSvcs
)
).toBe(true);
});
it('or + numeric and: A == torbox or (B > 3 and B < 5) — same precedence rule from the other side', () => {
// service == torbox → true, regardless of resultLimit
expect(
evaluateTemplateCondition(
'inputs.service == torbox or inputs.resultLimit > 3 and inputs.resultLimit < 5',
{ service: 'torbox', resultLimit: 1 },
noSvcs
)
).toBe(true);
// service != torbox, resultLimit=4 in (3,5) → false or true = true
expect(
evaluateTemplateCondition(
'inputs.service == torbox or inputs.resultLimit > 3 and inputs.resultLimit < 5',
{ service: 'other', resultLimit: 4 },
noSvcs
)
).toBe(true);
// service != torbox, resultLimit=2 NOT in (3,5) → false or false = false
expect(
evaluateTemplateCondition(
'inputs.service == torbox or inputs.resultLimit > 3 and inputs.resultLimit < 5',
{ service: 'other', resultLimit: 2 },
noSvcs
)
).toBe(false);
});
});
// ---------------------------------------------------------------------------
// applyTemplateConditionals — compound operators in __if (end-to-end)
// ---------------------------------------------------------------------------
describe('applyTemplateConditionals — __if with compound operators', () => {
const noSvcs: string[] = [];
it('and: item included only when both conditions pass', () => {
const result = applyTemplateConditionals(
[
{
__if: 'inputs.flag and inputs.tier == pro',
expression: 'pro-only',
enabled: true,
},
{ expression: 'always', enabled: true },
],
{ flag: true, tier: 'pro' },
noSvcs
);
expect(result.map((e: any) => e.expression)).toEqual([
'pro-only',
'always',
]);
const result2 = applyTemplateConditionals(
[
{
__if: 'inputs.flag and inputs.tier == pro',
expression: 'pro-only',
enabled: true,
},
{ expression: 'always', enabled: true },
],
{ flag: true, tier: 'basic' },
noSvcs
);
expect(result2.map((e: any) => e.expression)).toEqual(['always']);
});
it('or: item included when either condition passes', () => {
const result = applyTemplateConditionals(
[
{
__if: 'services.torbox or services.realdebrid',
type: 'debrid-preset',
enabled: true,
},
{ type: 'base', enabled: true },
],
{},
['realdebrid']
);
expect(result.map((e: any) => e.type)).toEqual(['debrid-preset', 'base']);
const result2 = applyTemplateConditionals(
[
{
__if: 'services.torbox or services.realdebrid',
type: 'debrid-preset',
enabled: true,
},
{ type: 'base', enabled: true },
],
{},
[]
);
expect(result2.map((e: any) => e.type)).toEqual(['base']);
});
it('numeric range: inputs.n >= 1 and inputs.n <= 5', () => {
const template = [
{
__if: 'inputs.n >= 1 and inputs.n <= 5',
expression: 'in-range',
enabled: true,
},
{ expression: 'always', enabled: true },
];
const inRange = applyTemplateConditionals(template, { n: 3 }, noSvcs);
expect(inRange.map((e: any) => e.expression)).toEqual([
'in-range',
'always',
]);
const outOfRange = applyTemplateConditionals(template, { n: 0 }, noSvcs);
expect(outOfRange.map((e: any) => e.expression)).toEqual(['always']);
});
});
describe('applyTemplateConditionals — __switch edge cases', () => {
const noSvcs: string[] = [];
it('uses default when resolved input is undefined (key becomes null)', () => {
// inputs.missing is undefined → resolveRef returns undefined → key = null → default chosen
const result = applyTemplateConditionals(
{
__switch: 'inputs.missing',
cases: { pro: { id: 'pro' } },
default: { id: 'fallback' },
},
{},
noSvcs
);
expect(result).toEqual({ id: 'fallback' });
});
it('uses default when cases object is absent', () => {
// No cases key → cases = {} → nothing matches → default
const result = applyTemplateConditionals(
{
__switch: 'inputs.tier',
default: { id: 'fallback' },
},
{ tier: 'pro' },
noSvcs
);
expect(result).toEqual({ id: 'fallback' });
});
it('resolves to null when both cases miss and default is absent', () => {
const result = applyTemplateConditionals(
{ __switch: 'inputs.tier', cases: { pro: { id: 'pro' } } },
{ tier: 'basic' },
noSvcs
);
expect(result).toBeNull();
});
it('__switch nested inside another __switch', () => {
const result = applyTemplateConditionals(
{
__switch: 'inputs.outer',
cases: {
a: {
__switch: 'inputs.inner',
cases: { x: { label: 'ax' }, y: { label: 'ay' } },
default: { label: 'a-default' },
},
},
default: { label: 'outer-default' },
},
{ outer: 'a', inner: 'x' },
noSvcs
);
expect(result).toEqual({ label: 'ax' });
});
it('__switch inside an array item (via __if) is recursed', () => {
const result = applyTemplateConditionals(
[
{
__if: 'inputs.flag',
widget: {
__switch: 'inputs.style',
cases: { compact: { size: 'sm' } },
default: { size: 'lg' },
},
},
],
{ flag: true, style: 'compact' },
noSvcs
);
expect(result).toEqual([{ widget: { size: 'sm' } }]);
});
});
describe('applyTemplateConditionals — __value edge cases', () => {
const noSvcs: string[] = [];
it('__value with a {{}} token that resolves to an array is spread', () => {
// sole token in __value string resolves to array → spread into parent
const result = applyTemplateConditionals(
[{ __if: 'inputs.flag', __value: '{{inputs.extras}}' }],
{ flag: true, extras: ['HDR', 'HDR10'] },
noSvcs
);
expect(result).toEqual(['HDR', 'HDR10']);
});
it('__value with a {{}} token that resolves to a scalar is inserted as one item', () => {
const result = applyTemplateConditionals(
[{ __if: 'inputs.flag', __value: '{{inputs.tag}}' }],
{ flag: true, tag: 'DV' },
noSvcs
);
expect(result).toEqual(['DV']);
});
it('unconditional __value with {{}} token resolving to array is spread', () => {
const result = applyTemplateConditionals(
[{ __value: '{{inputs.langs}}' }, 'Original'],
{ langs: ['French', 'German'] },
noSvcs
);
expect(result).toEqual(['French', 'German', 'Original']);
});
it('__value order is preserved relative to static items', () => {
const result = applyTemplateConditionals(
[
'A',
{ __if: 'inputs.x', __value: 'X' },
'B',
{ __if: 'inputs.y', __value: 'Y' },
'C',
],
{ x: true, y: true },
noSvcs
);
expect(result).toEqual(['A', 'X', 'B', 'Y', 'C']);
});
});
// ---------------------------------------------------------------------------
// preferredLanguages
// ---------------------------------------------------------------------------
describe('preferredLanguages multi-select spread', () => {
const tail = ['Original', 'Dual Audio', 'Multi', 'Dubbed', 'Unknown'];
const template = ['{{inputs.languages}}', ...tail];
it('spreads multiple selected languages before the fixed tail', () => {
const result = applyTemplateConditionals(
template,
{ languages: ['French', 'German'] },
[]
);
expect(result).toEqual(['French', 'German', ...tail]);
});
it('spreads a single language before the tail', () => {
const result = applyTemplateConditionals(
template,
{ languages: ['English'] },
[]
);
expect(result).toEqual(['English', ...tail]);
});
it('empty selection produces only the fixed tail', () => {
const result = applyTemplateConditionals(template, { languages: [] }, []);
expect(result).toEqual(tail);
});
it('all supported languages spread correctly', () => {
const all = [
'English',
'Japanese',
'French',
'German',
'Spanish',
'Italian',
'Portuguese',
'Korean',
'Chinese',
'Arabic',
'Hindi',
'Russian',
'Dutch',
'Turkish',
'Polish',
'Unknown',
];
const result = applyTemplateConditionals(template, { languages: all }, []);
expect(result.slice(0, all.length)).toEqual(all);
expect(result.slice(all.length)).toEqual(tail);
});
});
// ---------------------------------------------------------------------------
// optionalFilters multi-select → excludedStreamExpressions
// ---------------------------------------------------------------------------
describe('optionalFilters excludedStreamExpressions', () => {
const eseSlice = [
{
__if: 'inputs.optionalFilters includes nzbOnly',
expression: 'nzb-only',
enabled: true,
},
{
__if: 'inputs.optionalFilters includes dvOnlyNonRemux',
expression: 'dv-only-non-remux',
enabled: true,
},
{
__if: 'inputs.torboxTier == nonPro',
expression: 'tb-nonpro-200gb',
enabled: true,
},
{
__if: 'inputs.torboxTier == pro',
expression: 'tb-pro-1tb',
enabled: true,
},
{
__if: 'inputs.optionalFilters includes bitrateHardcapMobile',
expression: 'bitrate-hardcap',
enabled: true,
},
{ expression: 'synced-eses', enabled: true },
{
__if: 'inputs.resultLimit',
expression: 'slice(streams, {{inputs.resultLimit}})',
enabled: true,
},
];
it('no optional filters selected — only the always-on ESE is included', () => {
const result = applyTemplateConditionals(
eseSlice,
{ optionalFilters: [], torboxTier: 'none', resultLimit: undefined },
[]
);
expect(result).toHaveLength(1);
expect(result[0].expression).toBe('synced-eses');
});
it('nzbOnly filter enabled — only nzb-only and always-on', () => {
const result = applyTemplateConditionals(
eseSlice,
{
optionalFilters: ['nzbOnly'],
torboxTier: 'none',
resultLimit: undefined,
},
[]
);
expect(result.map((e: any) => e.expression)).toEqual([
'nzb-only',
'synced-eses',
]);
});
it('dvOnlyNonRemux filter enabled', () => {
const result = applyTemplateConditionals(
eseSlice,
{
optionalFilters: ['dvOnlyNonRemux'],
torboxTier: 'none',
resultLimit: undefined,
},
[]
);
expect(result.map((e: any) => e.expression)).toEqual([
'dv-only-non-remux',
'synced-eses',
]);
});
it('bitrateHardcapMobile enabled', () => {
const result = applyTemplateConditionals(
eseSlice,
{
optionalFilters: ['bitrateHardcapMobile'],
torboxTier: 'none',
resultLimit: undefined,
},
[]
);
expect(result.map((e: any) => e.expression)).toEqual([
'bitrate-hardcap',
'synced-eses',
]);
});
it('multiple filters enabled simultaneously', () => {
const result = applyTemplateConditionals(
eseSlice,
{
optionalFilters: ['nzbOnly', 'dvOnlyNonRemux', 'bitrateHardcapMobile'],
torboxTier: 'none',
resultLimit: undefined,
},
[]
);
expect(result.map((e: any) => e.expression)).toEqual([
'nzb-only',
'dv-only-non-remux',
'bitrate-hardcap',
'synced-eses',
]);
});
it('all optional filters plus all others enabled', () => {
const result = applyTemplateConditionals(
eseSlice,
{
optionalFilters: ['nzbOnly', 'dvOnlyNonRemux', 'bitrateHardcapMobile'],
torboxTier: 'pro',
resultLimit: 6,
},
[]
);
expect(result.map((e: any) => e.expression)).toEqual([
'nzb-only',
'dv-only-non-remux',
'tb-pro-1tb',
'bitrate-hardcap',
'synced-eses',
'slice(streams, 6)',
]);
});
});
// ---------------------------------------------------------------------------
// torboxTier select → size cap ESEs
// ---------------------------------------------------------------------------
describe('torboxTier select', () => {
const torboxEses = [
{
__if: 'inputs.torboxTier == nonPro',
expression: 'tb-nonpro-200gb',
enabled: true,
},
{
__if: 'inputs.torboxTier == pro',
expression: 'tb-pro-1tb',
enabled: true,
},
{ expression: 'always', enabled: true },
];
it('none → no tier ESEs', () => {
const result = applyTemplateConditionals(
torboxEses,
{ torboxTier: 'none' },
[]
);
expect(result.map((e: any) => e.expression)).toEqual(['always']);
});
it('nonPro → 200 GB cap included', () => {
const result = applyTemplateConditionals(
torboxEses,
{ torboxTier: 'nonPro' },
[]
);
expect(result.map((e: any) => e.expression)).toEqual([
'tb-nonpro-200gb',
'always',
]);
});
it('pro → 1 TB cap included', () => {
const result = applyTemplateConditionals(
torboxEses,
{ torboxTier: 'pro' },
[]
);
expect(result.map((e: any) => e.expression)).toEqual([
'tb-pro-1tb',
'always',
]);
});
it('only one tier expression is ever included at a time', () => {
const nonPro = applyTemplateConditionals(
torboxEses,
{ torboxTier: 'nonPro' },
[]
);
const pro = applyTemplateConditionals(
torboxEses,
{ torboxTier: 'pro' },
[]
);
expect(nonPro.some((e: any) => e.expression === 'tb-pro-1tb')).toBe(false);
expect(pro.some((e: any) => e.expression === 'tb-nonpro-200gb')).toBe(
false
);
});
});
// ---------------------------------------------------------------------------
// resultLimit number input
// ---------------------------------------------------------------------------
describe('resultLimit number', () => {
const sliceExpr = [
{ expression: 'base-expr', enabled: true },
{
__if: 'inputs.resultLimit',
expression: 'slice(streams, {{inputs.resultLimit}})',
enabled: true,
},
];
it('resultLimit 6 → slice expression included with value interpolated', () => {
const result = applyTemplateConditionals(sliceExpr, { resultLimit: 6 }, []);
expect(result).toHaveLength(2);
expect(result[1].expression).toBe('slice(streams, 6)');
});
it('resultLimit 1 → interpolated correctly', () => {
const result = applyTemplateConditionals(sliceExpr, { resultLimit: 1 }, []);
expect(result[1].expression).toBe('slice(streams, 1)');
});
it('resultLimit undefined → slice excluded', () => {
const result = applyTemplateConditionals(
sliceExpr,
{ resultLimit: undefined },
[]
);
expect(result).toHaveLength(1);
expect(result[0].expression).toBe('base-expr');
});
it('resultLimit 0 → included (0 is truthy — an explicitly set number)', () => {
// The bare-truthiness check only excludes undefined/null/''/ false/[].
// Numeric 0 is treated as truthy so that an explicitly entered value of 0
// still fires the condition (e.g. "slice 0" = unlimited, still a valid choice).
const result = applyTemplateConditionals(sliceExpr, { resultLimit: 0 }, []);
expect(result).toHaveLength(2);
expect(result[1].expression).toBe('slice(streams, 0)');
});
it('resultLimit blank (empty string) → excluded', () => {
const result = applyTemplateConditionals(
sliceExpr,
{ resultLimit: '' },
[]
);
expect(result).toHaveLength(1);
});
});
// ---------------------------------------------------------------------------
// languagePassthrough select (ISE)
// ---------------------------------------------------------------------------
describe('languagePassthrough ISE', () => {
// The actual ISE expression from the template (simplified to keep the test readable)
const iseSlice = [
{
__if: 'inputs.languagePassthrough != none',
expression:
"/*{{inputs.languagePassthrough}} Passthrough*/ passthrough(language(streams, '{{inputs.languagePassthrough}}'), 'title')",
enabled: true,
},
];
it('"none" → no ISE included', () => {
const result = applyTemplateConditionals(
iseSlice,
{ languagePassthrough: 'none' },
[]
);
expect(result).toHaveLength(0);
});
it('"English" → ISE included with language interpolated', () => {
const result = applyTemplateConditionals(
iseSlice,
{ languagePassthrough: 'English' },
[]
);
expect(result).toHaveLength(1);
expect(result[0].expression).toContain('English Passthrough');
expect(result[0].expression).toContain("'English'");
});
it('"Japanese" → ISE included with correct interpolation', () => {
const result = applyTemplateConditionals(
iseSlice,
{ languagePassthrough: 'Japanese' },
[]
);
expect(result[0].expression).toContain('Japanese Passthrough');
expect(result[0].expression).toContain("'Japanese'");
});
it('"French" → ISE included', () => {
const result = applyTemplateConditionals(
iseSlice,
{ languagePassthrough: 'French' },
[]
);
expect(result[0].expression).toContain('French Passthrough');
});
it('undefined languagePassthrough → ISE included (undefined != "none" is true)', () => {
// String(undefined ?? '') === '' which !== 'none' → condition passes
// This is the documented edge case: a missing value is treated as '' which != 'none'
const result = applyTemplateConditionals(iseSlice, {}, []);
expect(result).toHaveLength(1);
});
});
// ---------------------------------------------------------------------------
// formatterVariant __switch
// ---------------------------------------------------------------------------
describe('formatterVariant __switch', () => {
const formatterSwitch = {
__switch: 'inputs.formatterVariant',
cases: {
default: {
id: 'custom',
definition: { name: 'clean-name', description: 'clean-desc' },
},
fullRse: {
id: 'custom',
definition: { name: 'full-name', description: 'full-desc' },
},
},
default: { id: 'tamtaro' },
};
it('"default" variant → clean formatter', () => {
const result = applyTemplateConditionals(
{ formatter: formatterSwitch },
{ formatterVariant: 'default' },
[]
);
expect(result.formatter.id).toBe('custom');
expect(result.formatter.definition.name).toBe('clean-name');
});
it('"fullRse" variant → full RSE formatter', () => {
const result = applyTemplateConditionals(
{ formatter: formatterSwitch },
{ formatterVariant: 'fullRse' },
[]
);
expect(result.formatter.id).toBe('custom');
expect(result.formatter.definition.name).toBe('full-name');
});
it('unknown variant → falls to default { id: "tamtaro" }', () => {
const result = applyTemplateConditionals(
{ formatter: formatterSwitch },
{ formatterVariant: 'unknown' },
[]
);
expect(result.formatter).toEqual({ id: 'tamtaro' });
});
it('missing formatterVariant → falls to default', () => {
const result = applyTemplateConditionals(
{ formatter: formatterSwitch },
{},
[]
);
expect(result.formatter).toEqual({ id: 'tamtaro' });
});
});
// ---------------------------------------------------------------------------
// presets (includeDebridio boolean + services.torbox)
// ---------------------------------------------------------------------------
describe('presets conditional inclusion', () => {
const presetsTemplate = [
{
type: 'seadex',
instanceId: '326',
enabled: true,
options: { name: 'SeaDex' },
},
{
__if: 'services.torbox',
type: 'torbox-search',
instanceId: '8ae',
enabled: false,
options: { name: 'Search' },
},
{
type: 'torznab',
instanceId: 'a08',
enabled: true,
options: { name: 'STorz' },
},
{
__if: 'inputs.includeDebridio',
type: 'debridio',
instanceId: '911',
enabled: true,
options: {
name: 'Debridio',
debridioApiKey: '{{inputs.debridioApiKey}}',
},
},
{
type: 'comet',
instanceId: 'b0f',
enabled: true,
options: { name: 'Comet' },
},
];
it('no torbox, no debridio → only always-on presets', () => {
const result = applyTemplateConditionals(
presetsTemplate,
{ includeDebridio: false },
[]
);
expect(result.map((p: any) => p.type)).toEqual([
'seadex',
'torznab',
'comet',
]);
});
it('torbox service enabled → torbox-search included', () => {
const result = applyTemplateConditionals(
presetsTemplate,
{ includeDebridio: false },
['torbox']
);
expect(result.map((p: any) => p.type)).toEqual([
'seadex',
'torbox-search',
'torznab',
'comet',
]);
});
it('includeDebridio true → debridio preset included with API key', () => {
const result = applyTemplateConditionals(
presetsTemplate,
{ includeDebridio: true, debridioApiKey: 'my-api-key' },
[]
);
const debridio = result.find((p: any) => p.type === 'debridio');
expect(debridio).toBeDefined();
expect(debridio.options.debridioApiKey).toBe('my-api-key');
});
it('torbox + debridio both active → all presets present in order', () => {
const result = applyTemplateConditionals(
presetsTemplate,
{ includeDebridio: true, debridioApiKey: 'key' },
['torbox']
);
expect(result.map((p: any) => p.type)).toEqual([
'seadex',
'torbox-search',
'torznab',
'debridio',
'comet',
]);
});
it('__if strips cleanly — no __if key on kept preset', () => {
const result = applyTemplateConditionals(
presetsTemplate,
{ includeDebridio: true, debridioApiKey: 'k' },
['torbox']
);
for (const preset of result) {
expect(preset).not.toHaveProperty('__if');
}
});
});
// ---------------------------------------------------------------------------
// includedStreamExpressions (optionalFilters passthrough ISEs)
// ---------------------------------------------------------------------------
describe('includedStreamExpressions passthroughs', () => {
const iseTemplate = [
{
__if: 'inputs.optionalFilters includes dvPassthrough',
expression: 'dv-passthrough',
enabled: true,
},
{
__if: 'inputs.optionalFilters includes dvHdrPassthrough',
expression: 'dvhdr-passthrough',
enabled: true,
},
{
__if: 'inputs.optionalFilters includes sdrPassthrough',
expression: 'sdr-passthrough',
enabled: true,
},
{
__if: 'inputs.languagePassthrough != none',
expression:
'/*{{inputs.languagePassthrough}} Passthrough*/ passthrough()',
enabled: true,
},
];
it('no filters, no language passthrough → empty', () => {
const result = applyTemplateConditionals(
iseTemplate,
{ optionalFilters: [], languagePassthrough: 'none' },
[]
);
expect(result).toHaveLength(0);
});
it('dvPassthrough only', () => {
const result = applyTemplateConditionals(
iseTemplate,
{ optionalFilters: ['dvPassthrough'], languagePassthrough: 'none' },
[]
);
expect(result.map((e: any) => e.expression)).toEqual(['dv-passthrough']);
});
it('all three passthroughs + language', () => {
const result = applyTemplateConditionals(
iseTemplate,
{
optionalFilters: [
'dvPassthrough',
'dvHdrPassthrough',
'sdrPassthrough',
],
languagePassthrough: 'English',
},
[]
);
expect(result).toHaveLength(4);
expect(result[3].expression).toContain('English Passthrough');
});
it('language passthrough expression interpolates both label and value', () => {
const result = applyTemplateConditionals(
iseTemplate,
{ optionalFilters: [], languagePassthrough: 'Korean' },
[]
);
expect(result[0].expression).toContain('Korean Passthrough');
});
});
// ---------------------------------------------------------------------------
// full integration test
// ---------------------------------------------------------------------------
describe('full config integration', () => {
const tamtaroConfig = {
preferredLanguages: [
'{{inputs.languages}}',
'Original',
'Dual Audio',
'Multi',
'Dubbed',
'Unknown',
],
excludedVisualTags: ['3D', 'H-OU', 'H-SBS', 'AI'],
syncedRankedRegexUrls: [
'https://raw.githubusercontent.com/Vidhin05/Releases-Regex/main/English/regexes.json',
],
excludedStreamExpressions: [
{
__if: 'inputs.optionalFilters includes nzbOnly',
expression: 'nzb-only',
enabled: true,
},
{
__if: 'inputs.optionalFilters includes dvOnlyNonRemux',
expression: 'dv-only',
enabled: true,
},
{
__if: 'inputs.torboxTier == nonPro',
expression: 'tb-200gb',
enabled: true,
},
{ __if: 'inputs.torboxTier == pro', expression: 'tb-1tb', enabled: true },
{
__if: 'inputs.optionalFilters includes bitrateHardcapMobile',
expression: 'bitrate-cap',
enabled: true,
},
{ expression: 'synced-ese', enabled: true },
{
__if: 'inputs.resultLimit',
expression: 'slice(streams, {{inputs.resultLimit}})',
enabled: true,
},
],
includedStreamExpressions: [
{
__if: 'inputs.optionalFilters includes dvPassthrough',
expression: 'dv-pass',
enabled: true,
},
{
__if: 'inputs.optionalFilters includes sdrPassthrough',
expression: 'sdr-pass',
enabled: true,
},
{
__if: 'inputs.languagePassthrough != none',
expression:
'/*{{inputs.languagePassthrough}} Passthrough*/ passthrough()',
enabled: true,
},
],
formatter: {
__switch: 'inputs.formatterVariant',
cases: {
default: { id: 'custom', definition: { name: 'clean' } },
fullRse: { id: 'custom', definition: { name: 'full' } },
},
default: { id: 'tamtaro' },
},
presets: [
{ type: 'seadex', enabled: true },
{ __if: 'services.torbox', type: 'torbox-search', enabled: false },
{
__if: 'inputs.includeDebridio',
type: 'debridio',
enabled: true,
options: { debridioApiKey: '{{inputs.debridioApiKey}}' },
},
{ type: 'comet', enabled: true },
],
tmdbApiKey: '<template_placeholder>',
rpdbApiKey: '<optional_template_placeholder>',
};
it('standard debrid user: English only, default formatter, no extras', () => {
const result = applyTemplateConditionals(
tamtaroConfig,
{
languages: ['English'],
optionalFilters: [],
torboxTier: 'none',
resultLimit: undefined,
languagePassthrough: 'none',
formatterVariant: 'default',
includeDebridio: false,
},
[]
);
expect(result.preferredLanguages).toEqual([
'English',
'Original',
'Dual Audio',
'Multi',
'Dubbed',
'Unknown',
]);
expect(result.excludedStreamExpressions).toHaveLength(1);
expect(result.excludedStreamExpressions[0].expression).toBe('synced-ese');
expect(result.includedStreamExpressions).toHaveLength(0);
expect(result.formatter).toEqual({
id: 'custom',
definition: { name: 'clean' },
});
expect(result.presets.map((p: any) => p.type)).toEqual(['seadex', 'comet']);
// Placeholders pass through unchanged
expect(result.tmdbApiKey).toBe('<template_placeholder>');
expect(result.rpdbApiKey).toBe('<optional_template_placeholder>');
});
it('power user: multi-language, torbox pro + nzbOnly, Japanese passthrough, fullRse, debridio', () => {
const result = applyTemplateConditionals(
tamtaroConfig,
{
languages: ['English', 'Japanese'],
optionalFilters: ['nzbOnly', 'dvPassthrough'],
torboxTier: 'pro',
resultLimit: 6,
languagePassthrough: 'Japanese',
formatterVariant: 'fullRse',
includeDebridio: true,
debridioApiKey: 'deb-key-xyz',
},
['torbox']
);
// Languages
expect(result.preferredLanguages.slice(0, 2)).toEqual([
'English',
'Japanese',
]);
expect(result.preferredLanguages).toContain('Original');
// ESEs: nzb-only, tb-1tb, synced-ese, slice
expect(
result.excludedStreamExpressions.map((e: any) => e.expression)
).toEqual(['nzb-only', 'tb-1tb', 'synced-ese', 'slice(streams, 6)']);
// ISEs: dvPassthrough + Japanese passthrough
expect(result.includedStreamExpressions).toHaveLength(2);
expect(result.includedStreamExpressions[0].expression).toBe('dv-pass');
expect(result.includedStreamExpressions[1].expression).toContain(
'Japanese Passthrough'
);
// Formatter
expect(result.formatter).toEqual({
id: 'custom',
definition: { name: 'full' },
});
// Presets: seadex + torbox-search + debridio + comet
expect(result.presets.map((p: any) => p.type)).toEqual([
'seadex',
'torbox-search',
'debridio',
'comet',
]);
expect(
result.presets.find((p: any) => p.type === 'debridio').options
.debridioApiKey
).toBe('deb-key-xyz');
});
it('minimal setup: single language, no service, no extras, fallback formatter', () => {
const result = applyTemplateConditionals(
tamtaroConfig,
{
languages: ['French'],
optionalFilters: [],
torboxTier: 'none',
resultLimit: undefined,
languagePassthrough: 'none',
formatterVariant: 'something-unknown',
includeDebridio: false,
},
[]
);
expect(result.preferredLanguages[0]).toBe('French');
expect(result.excludedStreamExpressions).toHaveLength(1);
expect(result.includedStreamExpressions).toHaveLength(0);
expect(result.formatter).toEqual({ id: 'tamtaro' });
expect(result.presets.map((p: any) => p.type)).toEqual(['seadex', 'comet']);
});
it('nonPro TorBox + all optional filters', () => {
const result = applyTemplateConditionals(
tamtaroConfig,
{
languages: ['English'],
optionalFilters: ['nzbOnly', 'dvOnlyNonRemux', 'bitrateHardcapMobile'],
torboxTier: 'nonPro',
resultLimit: 10,
languagePassthrough: 'none',
formatterVariant: 'default',
includeDebridio: false,
},
[]
);
expect(
result.excludedStreamExpressions.map((e: any) => e.expression)
).toEqual([
'nzb-only',
'dv-only',
'tb-200gb',
'bitrate-cap',
'synced-ese',
'slice(streams, 10)',
]);
});
});
// ---------------------------------------------------------------------------
// applyTemplateConditionals — __if + __value at the object-key level
// ---------------------------------------------------------------------------
describe('applyTemplateConditionals — __if + __value at object-key level', () => {
const noSvcs: string[] = [];
it('key is set to value when condition is true', () => {
const result = applyTemplateConditionals(
{
formatter: { __if: 'inputs.useFormatter', __value: { id: 'tamtaro' } },
},
{ useFormatter: true },
noSvcs
);
expect(result).toEqual({ formatter: { id: 'tamtaro' } });
});
it('key is dropped entirely when condition is false', () => {
const result = applyTemplateConditionals(
{
formatter: { __if: 'inputs.useFormatter', __value: { id: 'tamtaro' } },
},
{ useFormatter: false },
noSvcs
);
expect(result).toEqual({});
expect('formatter' in result).toBe(false);
});
it('key is dropped when condition is false — does not become null or undefined', () => {
const result = applyTemplateConditionals(
{
a: 1,
b: { __if: 'inputs.flag', __value: 'hello' },
c: 3,
},
{ flag: false },
noSvcs
);
expect(result).toEqual({ a: 1, c: 3 });
expect('b' in result).toBe(false);
});
it('static keys alongside conditional key are always preserved', () => {
const result = applyTemplateConditionals(
{
static1: 'always',
conditional: { __if: 'inputs.flag', __value: 'yes' },
static2: 42,
},
{ flag: true },
noSvcs
);
expect(result).toEqual({
static1: 'always',
conditional: 'yes',
static2: 42,
});
});
it('negated condition: key present when flag is false', () => {
const result = applyTemplateConditionals(
{
formatter: { __if: '!inputs.retainFormatter', __value: { id: 'mine' } },
},
{ retainFormatter: false },
noSvcs
);
expect(result).toEqual({ formatter: { id: 'mine' } });
});
it('negated condition: key dropped when flag is true', () => {
const result = applyTemplateConditionals(
{
formatter: { __if: '!inputs.retainFormatter', __value: { id: 'mine' } },
},
{ retainFormatter: true },
noSvcs
);
expect(result).toEqual({});
});
it('compound condition (and): key included only when both pass', () => {
const cfg = {
presets: {
__if: 'inputs.flag and inputs.tier == pro',
__value: ['a', 'b'],
},
};
expect(
applyTemplateConditionals(cfg, { flag: true, tier: 'pro' }, noSvcs)
).toEqual({ presets: ['a', 'b'] });
expect(
applyTemplateConditionals(cfg, { flag: true, tier: 'basic' }, noSvcs)
).toEqual({});
});
it('numeric condition: key included when value exceeds threshold', () => {
const cfg = {
extra: { __if: 'inputs.count > 5', __value: 'big' },
};
expect(applyTemplateConditionals(cfg, { count: 10 }, noSvcs)).toEqual({
extra: 'big',
});
expect(applyTemplateConditionals(cfg, { count: 3 }, noSvcs)).toEqual({});
});
it('__value is recursed — array items with their own __if are processed', () => {
const result = applyTemplateConditionals(
{
presets: {
__if: 'inputs.include',
__value: [{ __if: 'inputs.flag', type: 'a' }, { type: 'b' }],
},
},
{ include: true, flag: false },
noSvcs
);
expect(result).toEqual({ presets: [{ type: 'b' }] });
});
it('{{}} interpolation works inside __value', () => {
const result = applyTemplateConditionals(
{ label: { __if: 'inputs.show', __value: 'Hello {{inputs.name}}' } },
{ show: true, name: 'World' },
noSvcs
);
expect(result).toEqual({ label: 'Hello World' });
});
it('multiple conditional keys in one object — each evaluated independently', () => {
const result = applyTemplateConditionals(
{
a: { __if: 'inputs.showA', __value: 1 },
b: { __if: 'inputs.showB', __value: 2 },
c: { __if: 'inputs.showC', __value: 3 },
},
{ showA: true, showB: false, showC: true },
noSvcs
);
expect(result).toEqual({ a: 1, c: 3 });
});
it('service condition: key present when service selected', () => {
const result = applyTemplateConditionals(
{
torboxOptions: { __if: 'services.torbox', __value: { tier: 'pro' } },
},
{},
['torbox']
);
expect(result).toEqual({ torboxOptions: { tier: 'pro' } });
});
it('service condition: key dropped when service not selected', () => {
const result = applyTemplateConditionals(
{
torboxOptions: { __if: 'services.torbox', __value: { tier: 'pro' } },
},
{},
[]
);
expect(result).toEqual({});
});
});
// ---------------------------------------------------------------------------
// applyTemplateConditionals — __remove
// ---------------------------------------------------------------------------
describe('applyTemplateConditionals — __remove', () => {
const noSvcs: string[] = [];
it('__remove: true always drops the key', () => {
const result = applyTemplateConditionals(
{ formatter: { __remove: true } },
{},
noSvcs
);
expect(result).toEqual({});
expect('formatter' in result).toBe(false);
});
it('static keys alongside __remove are preserved', () => {
const result = applyTemplateConditionals(
{ keep: 'yes', drop: { __remove: true }, alsoKeep: 42 },
{},
noSvcs
);
expect(result).toEqual({ keep: 'yes', alsoKeep: 42 });
});
it('multiple __remove keys — all dropped', () => {
const result = applyTemplateConditionals(
{
a: { __remove: true },
b: 'stays',
c: { __remove: true },
},
{},
noSvcs
);
expect(result).toEqual({ b: 'stays' });
});
it('__remove inside a __switch case — key is dropped when that case is chosen', () => {
const result = applyTemplateConditionals(
{
formatter: {
__switch: 'inputs.formatterChoice',
cases: {
retain: { __remove: true },
tamtaro: { id: 'tamtaro' },
},
default: { id: 'prism' },
},
},
{ formatterChoice: 'retain' },
noSvcs
);
expect(result).toEqual({});
expect('formatter' in result).toBe(false);
});
it('__remove inside a __switch case — other cases still set the key', () => {
const result = applyTemplateConditionals(
{
formatter: {
__switch: 'inputs.formatterChoice',
cases: {
retain: { __remove: true },
tamtaro: { id: 'tamtaro' },
},
default: { id: 'prism' },
},
},
{ formatterChoice: 'tamtaro' },
noSvcs
);
expect(result).toEqual({ formatter: { id: 'tamtaro' } });
});
it('__remove in __switch default — key dropped when no case matches', () => {
const result = applyTemplateConditionals(
{
formatter: {
__switch: 'inputs.formatterChoice',
cases: {
tamtaro: { id: 'tamtaro' },
},
default: { __remove: true },
},
},
{ formatterChoice: 'unknown' },
noSvcs
);
expect(result).toEqual({});
});
it('__remove: false does NOT remove the key (only true triggers removal)', () => {
// __remove: false is treated as a plain object with __remove property
const result = applyTemplateConditionals(
{ formatter: { __remove: false, id: 'prism' } },
{},
noSvcs
);
// Falls through to regular property loop — keeps both __remove and id
expect(result.formatter).toEqual({ __remove: false, id: 'prism' });
});
});
// ---------------------------------------------------------------------------
// Practical scenarios — conditional fields (includeAddons / retainFormatter)
// ---------------------------------------------------------------------------
describe('practical: conditional config fields via __if+__value and __remove', () => {
const noSvcs: string[] = [];
it('includeAddons=true → presets field is set to the addon list', () => {
const result = applyTemplateConditionals(
{
presets: {
__if: 'inputs.includeAddons',
__value: [
{ type: 'torrentio', enabled: true },
{ __if: 'services.torbox', type: 'torbox-search', enabled: true },
],
},
},
{ includeAddons: true },
['torbox']
);
expect(result.presets).toEqual([
{ type: 'torrentio', enabled: true },
{ type: 'torbox-search', enabled: true },
]);
});
it('includeAddons=false → presets key is absent (user addons preserved)', () => {
const result = applyTemplateConditionals(
{
presets: {
__if: 'inputs.includeAddons',
__value: [{ type: 'torrentio', enabled: true }],
},
},
{ includeAddons: false },
noSvcs
);
expect('presets' in result).toBe(false);
});
it('retainFormatter=false → formatter key is set to template default', () => {
const result = applyTemplateConditionals(
{
formatter: {
__if: '!inputs.retainFormatter',
__value: { id: 'tamtaro' },
},
},
{ retainFormatter: false },
noSvcs
);
expect(result).toEqual({ formatter: { id: 'tamtaro' } });
});
it('retainFormatter=true → formatter key is absent (user formatter preserved)', () => {
const result = applyTemplateConditionals(
{
formatter: {
__if: '!inputs.retainFormatter',
__value: { id: 'tamtaro' },
},
},
{ retainFormatter: true },
noSvcs
);
expect('formatter' in result).toBe(false);
});
it('__switch with retain option removes the key, other choices set it', () => {
const config = {
formatter: {
__switch: 'inputs.formatterChoice',
cases: {
retain: { __remove: true },
default: { id: 'default-fmt' },
custom: { id: 'custom-fmt' },
},
default: { id: 'fallback-fmt' },
},
};
expect(
applyTemplateConditionals(config, { formatterChoice: 'retain' }, noSvcs)
).toEqual({});
expect(
applyTemplateConditionals(config, { formatterChoice: 'default' }, noSvcs)
).toEqual({ formatter: { id: 'default-fmt' } });
expect(
applyTemplateConditionals(config, { formatterChoice: 'other' }, noSvcs)
).toEqual({ formatter: { id: 'fallback-fmt' } });
});
it('combined: both presets and formatter conditionally present', () => {
const config = {
formatter: {
__if: '!inputs.retainFormatter',
__value: { id: 'tamtaro' },
},
presets: {
__if: 'inputs.includeAddons',
__value: [{ type: 'comet' }],
},
alwaysHere: true,
};
// Both included
expect(
applyTemplateConditionals(
config,
{ retainFormatter: false, includeAddons: true },
noSvcs
)
).toEqual({
formatter: { id: 'tamtaro' },
presets: [{ type: 'comet' }],
alwaysHere: true,
});
// Both dropped
expect(
applyTemplateConditionals(
config,
{ retainFormatter: true, includeAddons: false },
noSvcs
)
).toEqual({ alwaysHere: true });
// Only formatter retained (drop formatter, include presets)
const r3 = applyTemplateConditionals(
config,
{ retainFormatter: true, includeAddons: true },
noSvcs
);
expect('formatter' in r3).toBe(false);
expect(r3.presets).toEqual([{ type: 'comet' }]);
});
});
// ---------------------------------------------------------------------------
// sortCriteria — input-driven sort criteria
// ---------------------------------------------------------------------------
/**
* Helpers — mirrors the Tamtaro sort criteria layout used in the real template.
*
* A sort-criteria entry is always: { key: string, direction: "asc" | "desc" }
*
* Template structure:
* sortCriteria.cached — ordered list of criteria applied to cached streams
* sortCriteria.uncached — ordered list of criteria applied to uncached streams
* sortCriteria.global — prepended to every sub-list (e.g. "cached" gate)
*/
describe('sortCriteria — include / exclude library criterion', () => {
const noSvcs: string[] = [];
// Template fragment: the `cached` sort list includes `library` only when
// the `includeLibrary` boolean input is true.
const cachedCriteriaTemplate = [
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
{
__if: 'inputs.includeLibrary',
key: 'library',
direction: 'desc',
},
{ key: 'streamExpressionScore', direction: 'desc' },
{ key: 'bitrate', direction: 'desc' },
];
it('includes library criterion when includeLibrary is true', () => {
const result = applyTemplateConditionals(
cachedCriteriaTemplate,
{ includeLibrary: true },
noSvcs
);
expect(result).toEqual([
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
{ key: 'library', direction: 'desc' },
{ key: 'streamExpressionScore', direction: 'desc' },
{ key: 'bitrate', direction: 'desc' },
]);
});
it('excludes library criterion when includeLibrary is false', () => {
const result = applyTemplateConditionals(
cachedCriteriaTemplate,
{ includeLibrary: false },
noSvcs
);
expect(result).toEqual([
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
{ key: 'streamExpressionScore', direction: 'desc' },
{ key: 'bitrate', direction: 'desc' },
]);
});
it('excludes library criterion when includeLibrary is absent (default-off)', () => {
const result = applyTemplateConditionals(
cachedCriteriaTemplate,
{},
noSvcs
);
const keys = result.map((c: any) => c.key);
expect(keys).not.toContain('library');
expect(keys).toContain('resolution');
expect(keys).toContain('bitrate');
});
});
describe('sortCriteria — de-prioritise library (move to end)', () => {
const noSvcs: string[] = [];
// Template: when `deprioritiseLibrary` is true the library criterion goes at
// the bottom of the list (after bitrate/seeders); when false it sits in the
// "normal" position, right after quality.
const cachedTemplate = [
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
{
__if: '!inputs.deprioritiseLibrary',
key: 'library',
direction: 'desc',
},
{ key: 'streamExpressionScore', direction: 'desc' },
{ key: 'bitrate', direction: 'desc' },
{ key: 'seeders', direction: 'desc' },
{
__if: 'inputs.deprioritiseLibrary',
key: 'library',
direction: 'desc',
},
];
it('library is in normal position when deprioritiseLibrary is false', () => {
const result = applyTemplateConditionals(
cachedTemplate,
{ deprioritiseLibrary: false },
noSvcs
);
const keys = result.map((c: any) => c.key);
expect(keys).toEqual([
'resolution',
'quality',
'library',
'streamExpressionScore',
'bitrate',
'seeders',
]);
// library must come BEFORE streamExpressionScore
expect(keys.indexOf('library')).toBeLessThan(
keys.indexOf('streamExpressionScore')
);
});
it('library is moved to the end when deprioritiseLibrary is true', () => {
const result = applyTemplateConditionals(
cachedTemplate,
{ deprioritiseLibrary: true },
noSvcs
);
const keys = result.map((c: any) => c.key);
expect(keys).toEqual([
'resolution',
'quality',
'streamExpressionScore',
'bitrate',
'seeders',
'library',
]);
// library must come AFTER seeders
expect(keys.indexOf('library')).toBeGreaterThan(keys.indexOf('seeders'));
});
});
describe('sortCriteria — sort direction driven by input', () => {
const noSvcs: string[] = [];
// Template: the user can choose whether seeders sort ascending (fewest first —
// obscure torrents) or descending (most popular first). Use __switch on a
// select input: "desc" | "asc".
const seedersCriterionTemplate = {
key: 'seeders',
direction: {
__switch: 'inputs.seedersDirection',
cases: { asc: 'asc', desc: 'desc' },
default: 'desc',
},
};
it('direction is desc when seedersDirection == desc', () => {
const result = applyTemplateConditionals(
seedersCriterionTemplate,
{ seedersDirection: 'desc' },
noSvcs
);
expect(result).toEqual({ key: 'seeders', direction: 'desc' });
});
it('direction is asc when seedersDirection == asc', () => {
const result = applyTemplateConditionals(
seedersCriterionTemplate,
{ seedersDirection: 'asc' },
noSvcs
);
expect(result).toEqual({ key: 'seeders', direction: 'asc' });
});
it('falls back to desc when seedersDirection is not set', () => {
const result = applyTemplateConditionals(
seedersCriterionTemplate,
{},
noSvcs
);
expect(result).toEqual({ key: 'seeders', direction: 'desc' });
});
});
describe('sortCriteria — seadex criterion toggled by boolean input', () => {
const noSvcs: string[] = [];
const cachedTemplate = [
{
__if: 'inputs.enableSeadex',
key: 'seadex',
direction: 'desc',
},
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
{ key: 'streamExpressionScore', direction: 'desc' },
];
it('seadex leads the list when enableSeadex is true', () => {
const result = applyTemplateConditionals(
cachedTemplate,
{ enableSeadex: true },
noSvcs
);
const keys = result.map((c: any) => c.key);
expect(keys[0]).toBe('seadex');
expect(keys).toContain('resolution');
});
it('seadex is absent when enableSeadex is false', () => {
const result = applyTemplateConditionals(
cachedTemplate,
{ enableSeadex: false },
noSvcs
);
const keys = result.map((c: any) => c.key);
expect(keys).not.toContain('seadex');
expect(keys[0]).toBe('resolution');
});
});
describe('sortCriteria — full sortCriteria object with global + sub-lists', () => {
const noSvcs: string[] = [];
// Mirrors the real Tamtaro shape:
// global — always [cached gate]
// cached — full list, library + seadex toggled by inputs
// uncached — always [resolution, quality, seeders]
const sortCriteriaTemplate = {
global: [{ key: 'cached', direction: 'desc' }],
cached: [
{
__if: 'inputs.enableSeadex',
key: 'seadex',
direction: 'desc',
},
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
{
__if: '!inputs.deprioritiseLibrary',
key: 'library',
direction: 'desc',
},
{ key: 'streamExpressionMatched', direction: 'desc' },
{ key: 'streamExpressionScore', direction: 'desc' },
{ key: 'language', direction: 'desc' },
{ key: 'visualTag', direction: 'desc' },
{ key: 'audioTag', direction: 'desc' },
{ key: 'encode', direction: 'desc' },
{ key: 'bitrate', direction: 'desc' },
{ key: 'seeders', direction: 'desc' },
{
__if: 'inputs.deprioritiseLibrary',
key: 'library',
direction: 'desc',
},
],
uncached: [
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
{ key: 'seeders', direction: 'desc' },
],
series: [],
anime: [],
};
it('default (seadex on, library normal position)', () => {
const result = applyTemplateConditionals(
sortCriteriaTemplate,
{ enableSeadex: true, deprioritiseLibrary: false },
noSvcs
);
expect(result.global).toEqual([{ key: 'cached', direction: 'desc' }]);
const cachedKeys = result.cached.map((c: any) => c.key);
expect(cachedKeys[0]).toBe('seadex');
expect(cachedKeys.indexOf('library')).toBeLessThan(
cachedKeys.indexOf('streamExpressionMatched')
);
expect(result.uncached).toEqual([
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
{ key: 'seeders', direction: 'desc' },
]);
});
it('seadex off, library de-prioritised', () => {
const result = applyTemplateConditionals(
sortCriteriaTemplate,
{ enableSeadex: false, deprioritiseLibrary: true },
noSvcs
);
const cachedKeys = result.cached.map((c: any) => c.key);
expect(cachedKeys).not.toContain('seadex');
// library must be the last key in the cached list
expect(cachedKeys[cachedKeys.length - 1]).toBe('library');
});
it('seadex off, library in normal position', () => {
const result = applyTemplateConditionals(
sortCriteriaTemplate,
{ enableSeadex: false, deprioritiseLibrary: false },
noSvcs
);
const cachedKeys = result.cached.map((c: any) => c.key);
expect(cachedKeys).not.toContain('seadex');
expect(cachedKeys[0]).toBe('resolution');
expect(cachedKeys.indexOf('library')).toBeLessThan(
cachedKeys.indexOf('streamExpressionMatched')
);
});
it('global and uncached lists are unaffected by any input', () => {
for (const inputs of [
{ enableSeadex: true, deprioritiseLibrary: true },
{ enableSeadex: false, deprioritiseLibrary: false },
{},
]) {
const result = applyTemplateConditionals(
sortCriteriaTemplate,
inputs,
noSvcs
);
expect(result.global).toEqual([{ key: 'cached', direction: 'desc' }]);
expect(result.uncached).toEqual([
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
{ key: 'seeders', direction: 'desc' },
]);
expect(result.series).toEqual([]);
expect(result.anime).toEqual([]);
}
});
it('library appears exactly once regardless of input combination', () => {
for (const deprioritiseLibrary of [true, false]) {
const result = applyTemplateConditionals(
sortCriteriaTemplate,
{ enableSeadex: true, deprioritiseLibrary },
noSvcs
);
const libraryOccurrences = result.cached.filter(
(c: any) => c.key === 'library'
);
expect(libraryOccurrences).toHaveLength(1);
}
});
});
describe('sortCriteria — service-gated criteria', () => {
// Some sort keys (e.g. "cached") only make sense when a debrid service is
// selected. Model this with a services.* condition.
const globalTemplate = [
{
__if: 'services.realdebrid or services.torbox or services.alldebrid',
key: 'cached',
direction: 'desc',
},
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
];
it('cached criterion present when a debrid service is selected', () => {
const result = applyTemplateConditionals(globalTemplate, {}, ['torbox']);
const keys = result.map((c: any) => c.key);
expect(keys).toContain('cached');
expect(keys[0]).toBe('cached');
});
it('cached criterion absent when no debrid service is selected', () => {
const result = applyTemplateConditionals(globalTemplate, {}, []);
const keys = result.map((c: any) => c.key);
expect(keys).not.toContain('cached');
expect(keys[0]).toBe('resolution');
});
it('cached criterion present for realdebrid', () => {
const result = applyTemplateConditionals(globalTemplate, {}, [
'realdebrid',
]);
expect(result[0]).toEqual({ key: 'cached', direction: 'desc' });
});
it('cached criterion present for alldebrid', () => {
const result = applyTemplateConditionals(globalTemplate, {}, ['alldebrid']);
expect(result[0]).toEqual({ key: 'cached', direction: 'desc' });
});
});
describe('sortCriteria — prioritise language criterion via select input', () => {
const noSvcs: string[] = [];
// languagePriority: "top" → language sorts high (after seadex/resolution/quality),
// "bottom" → language sorts at the bottom,
// "none" → language criterion omitted entirely.
const cachedTemplate = [
{ key: 'seadex', direction: 'desc' },
{ key: 'resolution', direction: 'desc' },
{ key: 'quality', direction: 'desc' },
{
__if: 'inputs.languagePriority == top',
key: 'language',
direction: 'desc',
},
{ key: 'streamExpressionScore', direction: 'desc' },
{ key: 'bitrate', direction: 'desc' },
{ key: 'seeders', direction: 'desc' },
{
__if: 'inputs.languagePriority == bottom',
key: 'language',
direction: 'desc',
},
];
it('language is high-priority when languagePriority == top', () => {
const result = applyTemplateConditionals(
cachedTemplate,
{ languagePriority: 'top' },
noSvcs
);
const keys = result.map((c: any) => c.key);
expect(keys).toContain('language');
expect(keys.indexOf('language')).toBeLessThan(
keys.indexOf('streamExpressionScore')
);
});
it('language is low-priority when languagePriority == bottom', () => {
const result = applyTemplateConditionals(
cachedTemplate,
{ languagePriority: 'bottom' },
noSvcs
);
const keys = result.map((c: any) => c.key);
expect(keys).toContain('language');
expect(keys[keys.length - 1]).toBe('language');
});
it('language is omitted when languagePriority == none', () => {
const result = applyTemplateConditionals(
cachedTemplate,
{ languagePriority: 'none' },
noSvcs
);
const keys = result.map((c: any) => c.key);
expect(keys).not.toContain('language');
});
it('language appears exactly once for both top and bottom', () => {
for (const prio of ['top', 'bottom']) {
const result = applyTemplateConditionals(
cachedTemplate,
{ languagePriority: prio },
noSvcs
);
const count = result.filter((c: any) => c.key === 'language').length;
expect(count).toBe(1);
}
});
});
// ---------------------------------------------------------------------------
// resolveCredentialRefs
// ---------------------------------------------------------------------------
describe('resolveCredentialRefs', () => {
// ---- string inputs -------------------------------------------------------
it('replaces a single credential placeholder with the mapped value', () => {
expect(
resolveCredentialRefs('{{services.torbox.apiKey}}', {
service_torbox_apiKey: 'MY_API_KEY',
})
).toBe('MY_API_KEY');
});
it('replaces a placeholder embedded in a larger string', () => {
expect(
resolveCredentialRefs('key={{services.torbox.apiKey}}&extra=1', {
service_torbox_apiKey: 'ABC123',
})
).toBe('key=ABC123&extra=1');
});
it('replaces multiple placeholders in one string', () => {
expect(
resolveCredentialRefs(
'{{services.torbox.apiKey}}:{{services.torbox.userId}}',
{ service_torbox_apiKey: 'K1', service_torbox_userId: 'U1' }
)
).toBe('K1:U1');
});
it('replaces placeholders for different services in one string', () => {
expect(
resolveCredentialRefs(
'{{services.torbox.apiKey}}/{{services.realdebrid.token}}',
{
service_torbox_apiKey: 'TB_KEY',
service_realdebrid_token: 'RD_TOKEN',
}
)
).toBe('TB_KEY/RD_TOKEN');
});
it('uses empty string when credential key is missing from the map', () => {
expect(resolveCredentialRefs('{{services.torbox.apiKey}}', {})).toBe('');
});
it('leaves unrelated text unchanged', () => {
expect(
resolveCredentialRefs('hello world', { service_torbox_apiKey: 'X' })
).toBe('hello world');
});
it('leaves non-credential {{inputs.*}} placeholders unchanged', () => {
// resolveCredentialRefs only targets services.<id>.<key> refs
expect(
resolveCredentialRefs('{{inputs.someField}}', {
service_torbox_apiKey: 'X',
})
).toBe('{{inputs.someField}}');
});
it('returns primitives other than strings unchanged', () => {
expect(resolveCredentialRefs(42, {})).toBe(42);
expect(resolveCredentialRefs(true, {})).toBe(true);
expect(resolveCredentialRefs(null, {})).toBeNull();
});
// ---- array inputs --------------------------------------------------------
it('resolves placeholders inside each array element string', () => {
const result = resolveCredentialRefs(
['{{services.torbox.apiKey}}', 'static', '{{services.torbox.userId}}'],
{ service_torbox_apiKey: 'K', service_torbox_userId: 'U' }
);
expect(result).toEqual(['K', 'static', 'U']);
});
it('recurses into nested arrays', () => {
const result = resolveCredentialRefs([['{{services.torbox.apiKey}}']], {
service_torbox_apiKey: 'NESTED',
});
expect(result).toEqual([['NESTED']]);
});
// ---- object inputs -------------------------------------------------------
it('resolves a credential ref in an object field', () => {
const obj = { apiKey: '{{services.torbox.apiKey}}', type: 'newznab' };
const result = resolveCredentialRefs(obj, {
service_torbox_apiKey: 'MY_KEY',
});
expect(result).toEqual({ apiKey: 'MY_KEY', type: 'newznab' });
});
it('mutates the object in place', () => {
const obj = { apiKey: '{{services.torbox.apiKey}}' };
resolveCredentialRefs(obj, { service_torbox_apiKey: 'MUTATED' });
expect(obj.apiKey).toBe('MUTATED');
});
it('recurses into nested objects', () => {
const obj = {
presets: [
{
type: 'newznab',
options: { apiKey: '{{services.torbox.apiKey}}', url: 'https://x' },
},
],
};
resolveCredentialRefs(obj, { service_torbox_apiKey: 'DEEP_KEY' });
expect(obj).toEqual({
presets: [
{ type: 'newznab', options: { apiKey: 'DEEP_KEY', url: 'https://x' } },
],
});
});
it('handles multiple services across a nested structure', () => {
const obj = {
a: { key: '{{services.torbox.apiKey}}' },
b: { token: '{{services.realdebrid.token}}' },
};
resolveCredentialRefs(obj, {
service_torbox_apiKey: 'TB',
service_realdebrid_token: 'RD',
});
expect(obj).toEqual({ a: { key: 'TB' }, b: { token: 'RD' } });
});
it('leaves keys with no matching credential as empty string', () => {
const obj = { apiKey: '{{services.torbox.apiKey}}', other: 'keep' };
resolveCredentialRefs(obj, {});
expect(obj).toEqual({ apiKey: '', other: 'keep' });
});
});