import * as envfile from 'envfile'; import { existsSync, readFileSync, writeFileSync } from 'fs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ConfigTransform, EnvConfigTransform, getEnvConfigTransforms, transformComparison, } from './apply-templates'; import { Vault } from './bitwarden-vault'; // mock file system functions const mockState = vi.hoisted(() => ({ files: {} as Record, })); vi.mock('fs', () => ({ existsSync: vi.fn((f) => f.toString() in mockState.files), readFileSync: vi.fn((f, _) => mockState.files[f.toString()]), copyFileSync: vi.fn((f1, f2) => (mockState.files[f2] = mockState.files[f1])), writeFileSync: vi.fn((f, c) => (mockState.files[f.toString()] = c)), readdirSync: vi.fn((_) => Object.keys(mockState.files)), })); vi.mock('../../common', async () => { const actual = await vi.importActual( '../../common', ); return { ...actual, getTimestamp: vi.fn(() => '[2023-03-14 10:33:52.223]'), }; }); class MockVault implements Vault { constructor( private values: Record = { 'item1::username': 'Username from vault', 'item1::password': 'Password from vault', }, ) {} error = undefined; getValue(name: string, fieldName: string): string | undefined { const key = `${name}::${fieldName}`; return key in this.values ? this.values[key] : undefined; } } // .env file helpers const loadEnv = (name: string): Record => envfile.parse(readFileSync(name).toString()); const saveEnv = ( name: string, vars: Record, prefix = '', ): void => writeFileSync(name, prefix + envfile.stringify(vars)); describe('applyTemplates', () => { beforeEach(() => { mockState.files = {}; }); describe('sanity', () => { it('mock file system sanity', () => { // blank filename is invalid for real files but is accepted by the mock writeFileSync('', 'test-content'); // Assert expect(existsSync('')).toBeTrue(); expect(readFileSync('').toString()).toBe('test-content'); }); }); describe('compareTemplate (.env)', () => { it('target exists -> same', () => { // Arrange const template = { VAR1: '1', VAR2: '2', }; const prefix = '#@TEMPLATE = 1.env.template\n'; saveEnv('1.env.template', template); saveEnv('1.env', template, prefix); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('same'); expect(loadEnv('1.env.template')).toEqual(template); expect(loadEnv('1.env')).toEqual(template); }); it('no target -> created', () => { // Arrange const template = { VAR1: '1', VAR2: '2', }; saveEnv('1.env.template', template); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('created'); expect(loadEnv('1.env.template')).toEqual(template); expect(loadEnv('1.env')).toEqual(template); }); it('target with differences -> different', () => { // Arrange const template = 'VAR1=1\n' + 'VAR2=2\n' + 'VAR3=3\n' + 'VAR4=\n' + 'VAR5=\n' + 'VAR6=\n' + '# new template comment\n' + // ignored 'VAR7=\n' + 'VAR9=new-template-value'; const prefix = '#@TEMPLATE = 1.env.template\n'; const target = prefix + 'VAR1=1\n' + 'VAR2=Old value\n' + '#VAR3=3\n' + 'VAR3=Custom value\n' + 'VAR4=Placeholder replaced\n' + 'VAR6=Blank value replaced\n' + 'VAR8=My new value\n' + '#VAR9=old-template-value\n' + 'VAR9=custom-value\n'; writeFileSync('1.env.template', template); writeFileSync('1.env', target); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('different'); expect(readFileSync('1.env').toString()).toEqual( prefix + 'VAR1=1\n' + 'VAR2=2\n' + '#VAR3=3\n' + 'VAR3=Custom value\n' + 'VAR4=Placeholder replaced\n' + 'VAR6=Blank value replaced\n' + '# Variable VAR8 is not a part of the template.\n' + 'VAR8=My new value\n' + '#VAR9=old-template-value\n' + 'VAR9=custom-value\n' + '\n' + '# [2023-03-14 10:33:52.223] Newly added variables:\n' + 'VAR5=\n' + 'VAR7=\n', ); }); it('target with differences and commentObsolete option -> different, non-template variables commented', () => { // Arrange const template = 'VAR1=1\n'; const prefix = '#@TEMPLATE = 1.env.template\n'; const target = prefix + 'VAR1=1\n' + 'VAR8=My new value\n'; writeFileSync('1.env.template', template); writeFileSync('1.env', target); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, false, false, true); transform.writeTarget(); // Assert expect(result).toBe('different'); expect(readFileSync('1.env').toString()).toEqual( prefix + 'VAR1=1\n' + '# Variable VAR8 is not a part of the template.\n' + '# VAR8=My new value\n', ); }); it('replace option used -> different', () => { // Arrange const template = 'VAR1=1\n' + 'VAR2=2\n' + 'VAR3=3\n' + 'VAR4=\n' + 'VAR5=\n' + 'VAR6=\n' + 'VAR7=\n'; const prefix = '#@TEMPLATE = 1.env.template\n'; const target = prefix + 'VAR1=1\n' + 'VAR2=Old value\n' + '#VAR3=3\n' + 'VAR3=Custom value\n' + 'VAR4=Placeholder replaced\n' + 'VAR6=Blank value replaced\n' + 'VAR8=My new value\n'; writeFileSync('1.env.template', template); writeFileSync('1.env', target); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, true, false, false); transform.writeTarget(); // Assert expect(result).toBe('different'); expect(readFileSync('1.env').toString()).toEqual(prefix + template); }); it('comment changes -> same', () => { // Arrange const template = '# some comment\n' + 'BLAH=1\n' + '# a new comment\n' + 'BAR=2\n' + 'FOO=3'; const prefix = '#@TEMPLATE = 1.env.template\n'; const target0 = prefix + '# some slightly different comment\n' + 'BLAH=1\n' + 'BAR=2\n' + 'FOO=3\n'; writeFileSync('1.env.template', template); writeFileSync('1.env', target0); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('same'); expect(readFileSync('1.env.template').toString()).toEqual(template); expect(readFileSync('1.env').toString()).toEqual(target0); }); it('whitespace changes -> same', () => { // Arrange const template = '# a really really interesting comment\n' + 'BLAH=1\n' + 'FOO=2\n' + 'BAR=3\n'; const prefix = '#@TEMPLATE = 1.env.template\n'; const target0 = prefix + '# a really really interesting comment\n' + 'BLAH=1\n' + '\n' + 'FOO=2\n' + '\n' + 'BAR=3\n'; writeFileSync('1.env.template', template); writeFileSync('1.env', target0); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('same'); expect(readFileSync('1.env.template').toString()).toEqual(template); expect(readFileSync('1.env').toString()).toEqual(target0); }); it('ordering changes -> same', () => { // Arrange const template = '# a really really interesting comment\n' + 'BLAH=1\n' + 'FOO=2\n' + 'BAR=3\n'; const prefix = '#@TEMPLATE = 1.env.template\n'; const target0 = prefix + 'FOO=2\n' + 'BLAH=1\n' + 'BAR=3\n' + '# a really really interesting comment\n'; writeFileSync('1.env.template', template); writeFileSync('1.env', target0); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('same'); expect(readFileSync('1.env.template').toString()).toEqual(template); expect(readFileSync('1.env').toString()).toEqual(target0); }); it('new target created -> created (with added template directive)', () => { // Arrange const template = 'BLAH=1\n' + 'FOO=2\n' + 'BAR=3\n'; writeFileSync('1.env.template', template); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); const expectedTarget = '#@TEMPLATE = 1.env.template\n' + template; // Assert expect(result).toBe('created'); expect(readFileSync('1.env.template').toString()).toEqual(template); expect(readFileSync('1.env').toString()).toEqual(expectedTarget); }); it('target without template directive -> different-layout', () => { // Arrange const template = 'BLAH=1\n' + 'FOO=2\n' + 'BAR=3\n'; writeFileSync('1.env.template', template); writeFileSync('1.env', template); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); const expectedTarget = '#@TEMPLATE = 1.env.template\n' + template; // Assert expect(result).toBe('differentLayout'); expect(readFileSync('1.env.template').toString()).toEqual(template); expect(readFileSync('1.env').toString()).toEqual(expectedTarget); }); it('target with template directive -> same', () => { // Arrange const template = 'BLAH=1\n' + 'FOO=2\n' + 'BAR=3\n'; const target = '#@TEMPLATE = 1.env.template\n' + template; writeFileSync('1.env.template', template); writeFileSync('1.env', target); // Act const transform = new EnvConfigTransform('1.env.template', '1.env'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('same'); expect(readFileSync('1.env.template').toString()).toEqual(template); expect(readFileSync('1.env').toString()).toEqual(target); }); }); describe('compareTemplate (.env, bitwarden)', () => { it('no target -> created with placeholders resolved', () => { // Arrange const template = { VAR1: '1', VAR2: '', }; const target = { VAR1: '1', VAR2: 'Username from vault', }; saveEnv('1.env.template', template); // Act const transform = new EnvConfigTransform( '1.env.template', '1.env', new MockVault(), ); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('created'); expect(loadEnv('1.env.template')).toEqual(template); expect(loadEnv('1.env')).toEqual(target); }); it('placeholder not resolved in target -> different', () => { // Arrange const template = { VAR1: '1', VAR2: '', }; saveEnv('1.env.template', template); saveEnv('1.env', template); // Act const transform = new EnvConfigTransform( '1.env.template', '1.env', new MockVault(), ); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('different'); expect(loadEnv('1.env.template')).toEqual(template); expect(loadEnv('1.env')).toEqual({ VAR1: '1', VAR2: 'Username from vault', }); }); it('target exists with placeholders resolved -> same', () => { // Arrange const template = { VAR1: '1', VAR2: '', }; const target = { VAR1: '1', VAR2: 'Username from vault', }; saveEnv('1.env.template', template); saveEnv('1.env', target, '#@TEMPLATE = 1.env.template\n'); // Act const transform = new EnvConfigTransform( '1.env.template', '1.env', new MockVault(), ); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('same'); expect(loadEnv('1.env.template')).toEqual(template); expect(loadEnv('1.env')).toEqual(target); }); it('vault value changed -> different', () => { // Arrange const template = { VAR1: '1', VAR2: '', }; const target = { VAR1: '1', VAR2: 'Old value from vault', }; saveEnv('1.env.template', template); saveEnv('1.env', target); // Act const transform = new EnvConfigTransform( '1.env.template', '1.env', new MockVault(), ); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('different'); expect(loadEnv('1.env.template')).toEqual(template); expect(loadEnv('1.env')).toEqual({ VAR1: '1', VAR2: 'Username from vault', }); }); }); describe('getEnvConfigTargets (.env)', () => { it('no template directive -> default target updated', () => { // Arrange writeFileSync('1.env', 'x'); writeFileSync('2.env', 'x'); // Act const targets = getEnvConfigTransforms('2.env.template'); // Assert expect(targets).toIncludeAllMembers(['2.env']); }); it('no template directive -> missing default target is created', () => { // Arrange writeFileSync('2.env', 'x'); // Act const targets = getEnvConfigTransforms('1.env.template'); // Assert expect(targets).toIncludeAllMembers(['1.env']); }); it('template directive defined -> defined target used', () => { // Arrange writeFileSync('1.env', '#@TEMPLATE = 2.env.template\n'); writeFileSync('2.env', '#@TEMPLATE = 1.env.template\n'); // Act const targets = getEnvConfigTransforms('1.env.template'); // Assert expect(targets).toIncludeAllMembers(['2.env']); }); it('template directive defined -> default and defined target updated', () => { // Arrange writeFileSync('1.env', 'x'); writeFileSync('2.env', '#@TEMPLATE = 1.env.template\n'); // Act const targets = getEnvConfigTransforms('1.env.template'); // Assert expect(targets).toIncludeAllMembers(['1.env', '2.env']); }); it('template directive defined -> missing default target is not created', () => { // Arrange writeFileSync('2.env', '#@TEMPLATE = 1.env.template\n'); // Act const targets = getEnvConfigTransforms('1.env.template'); // Assert expect(targets).toIncludeAllMembers(['2.env']); }); it('template directive defined on default target -> target not updated', () => { // Arrange writeFileSync('1.env', '#@TEMPLATE = 2.env.template\n'); // Act const targets = getEnvConfigTransforms('1.env.template'); // Assert expect(targets).toIncludeAllMembers([]); }); it('multiple targets with directive defined -> all targets updated', () => { // Arrange writeFileSync('2.env', '#@TEMPLATE = 1.env.template\n'); writeFileSync('3.env', '#@TEMPLATE = 1.env.template\n'); // Act const targets = getEnvConfigTransforms('1.env.template'); // Assert expect(targets).toIncludeAllMembers(['2.env', '3.env']); }); it('template directive malformed -> directive ignored', () => { // Arrange writeFileSync('1.env', '#@@TEMPLATE = 2.env.template\n'); writeFileSync('2.env', '#@@TEMPLATE = 1.env.template\n'); // Act const targets = getEnvConfigTransforms('1.env.template'); // Assert expect(targets).toIncludeAllMembers(['1.env']); }); it('template directive bad filename -> target ignored', () => { // Arrange writeFileSync('1.env', '#@TEMPLATE = 3.env.template\n'); writeFileSync('2.env', '#@TEMPLATE = 3.env.template\n'); // Act const targets = getEnvConfigTransforms('1.env.template'); // Assert expect(targets).toIncludeAllMembers([]); }); }); describe('compareTemplate (not .env)', () => { it('target exists -> same', () => { // Arrange const template = ` BLAH FOO BAR `; writeFileSync('1.template', template); writeFileSync('1', template); // Act const transform = new ConfigTransform('1.template', '1'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('same'); expect(readFileSync('1.template').toString()).toEqual(template); expect(readFileSync('1').toString()).toEqual(template); }); it('no target -> created', () => { // Arrange const template = ` BLAH FOO BAR `; writeFileSync('1.template', template); // Act const transform = new ConfigTransform('1.template', '1'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('created'); expect(readFileSync('1.template').toString()).toEqual(template); expect(readFileSync('1').toString()).toEqual(template); }); it('target with differences in order -> different', () => { // Arrange const template = ` BLAH FOO BAR `; const target0 = ` BLAH BAR FOO `; writeFileSync('1.template', template); writeFileSync('1', target0); // Act const transform = new ConfigTransform('1.template', '1'); const result = transformComparison(transform, false, false, false); transform.writeTarget(); // Assert expect(result).toBe('different'); expect(readFileSync('1.template').toString()).toEqual(template); expect(readFileSync('1').toString()).toEqual(template); }); }); });