import { assertZWaveError , ZWaveErrorCodes } from "@zwave-js/core" ; import * as fs from "fs-extra" ; import { tmpdir } from "os" ; import * as path from "path" ; import { readJsonWithTemplate } from "./JsonTemplate" ; const mockDir = path . join ( tmpdir ( ) , `zwave-js-template-test` ) ; async function mockFs ( files ) { await fs . ensureDir ( mockDir ) ; for ( const [ name , content ] of Object . entries ( files ) ) { const relative = name . replace ( /^\// , "./" ) ; const filename = path . join ( mockDir , relative ) ; const dirname = path . join ( mockDir , path . dirname ( relative ) ) ; await fs . ensureDir ( dirname ) ; await fs . writeFile ( filename , content ) ; } } mockFs . restore = async ( ) : Promise < void > => { await fs . remove ( mockDir ) ; } ; describe ( "readJsonWithTemplate" , ( ) => { jest . setTimeout ( 20000 ) ; beforeAll ( ( ) => mockFs . restore ( ) ) ; afterEach ( ( ) => mockFs . restore ( ) ) ; it ( "should read simple JSON files normally" , async ( ) => { const file = { foo : "bar" , baz : 1 } ; await mockFs ( { "/test.json" : JSON . stringify ( file ) } ) ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( file ) ; } ) ; it ( "should follow top-level whole-file $imports" , async ( ) => { const test = { $import : "template.json" } ; const template = { template : true } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template.json" : JSON . stringify ( template ) } ) ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( template ) ; } ) ; it ( "should overwrite keys that are present before the $import" , async ( ) => { const test = { template : false , $import : "template.json" } ; const template = { template : true } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template.json" : JSON . stringify ( template ) } ) ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( template ) ; } ) ; it ( "should preserve keys that are present after the $import" , async ( ) => { const test = { $import : "template.json" , template : false , foobar : "baz" } ; const { $import : _ , ... expected } = test ; const template = { template : true } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template.json" : JSON . stringify ( template ) } ) ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( expected ) ; } ) ; it ( "should throw if the $import specifier is not a string" , async ( ) => { const test = { $import : 1 } ; const template = 1 ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template.json" : JSON . stringify ( template ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) , { errorCode : ZWaveErrorCodes . Config_Invalid } ) ; } ) ; it ( "should throw if the $import specifier is not valid" , async ( ) => { const tests = [ "no-extension" , "wrong.extension" , ".json" , "#" , "file.json#" , "#30[0x]" , ] ; for ( const specifier of tests ) { const test = { $import : specifier } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) , { errorCode : ZWaveErrorCodes . Config_Invalid , messageMatches : "Import specifier" } ) ; } } ) ; it ( "should throw if the $import target is not an object" , async ( ) => { const test = { $import : "template.json#somewhere" } ; const template = { somewhere : 1 } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template.json" : JSON . stringify ( template ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) , { errorCode : ZWaveErrorCodes . Config_Invalid } ) ; } ) ; it ( "should follow deep $imports" , async ( ) => { const test = { toplevel : true , nested : { $import : "template.json" } , template : false } ; const template = { template : true } ; const expected = { toplevel : true , nested : { template : true } , template : false } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template.json" : JSON . stringify ( template ) } ) ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( expected ) ; } ) ; it ( "should follow deep $imports in arrays" , async ( ) => { const test = { toplevel : true , nested : [ { $import : "template1.json" } , { $import : "template2.json" } , ] , template : false } ; const template1 = { template : 1 } ; const template2 = { template : 2 } ; const expected = { toplevel : true , nested : [ { template : 1 } , { template : 2 } , ] , template : false } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template1.json" : JSON . stringify ( template1 ) , "/template2.json" : JSON . stringify ( template2 ) } ) ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( expected ) ; } ) ; it ( "file-based circular references should throw an error (direct, top-level)" , async ( ) => { const test = { $import : "template.json" } ; const template = { $import : "test.json" } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template.json" : JSON . stringify ( template ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) , { errorCode : ZWaveErrorCodes . Config_CircularImport } ) ; } ) ; it ( "file-based circular references should throw an error (three-way)" , async ( ) => { const test = { $import : "template1.json" } ; const template1 = { $import : "template2.json" } ; const template2 = { $import : "test.json" } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template1.json" : JSON . stringify ( template1 ) , "/template2.json" : JSON . stringify ( template2 ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) , { errorCode : ZWaveErrorCodes . Config_CircularImport } ) ; } ) ; it ( "file-based circular references should throw an error (three-way, nested)" , async ( ) => { const test = { nested : { $import : "template1.json" } } ; const template1 = { alsoNested : [ { ok : "foo" } , { $import : "template2.json" } , ] } ; const template2 = { nested : { array : [ { $import : "test.json" } , ] } } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template1.json" : JSON . stringify ( template1 ) , "/template2.json" : JSON . stringify ( template2 ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) , { errorCode : ZWaveErrorCodes . Config_CircularImport } ) ; } ) ; it ( "should be able to resolve relative paths" , async ( ) => { const test = { $import : "../baz/template.json" } ; const template = { template : true } ; await mockFs ( { "/foo/bar/test.json" : JSON . stringify ( test ) , "/foo/baz/template.json" : JSON . stringify ( template ) } ) ; const content = await readJsonWithTemplate ( path . join ( mockDir , "foo/bar/test.json" ) ) ; expect ( content ) . toEqual ( template ) ; } ) ; it ( "should be able to resolve the root directory with ~/" , async ( ) => { const test = { $import : "~/template.json" } ; const template = { template : true } ; await mockFs ( { "/foo/bar/test.json" : JSON . stringify ( test ) , "/foo/template.json" : JSON . stringify ( template ) } ) ; const content = await readJsonWithTemplate ( path . join ( mockDir , "foo/bar/test.json" ) , path . join ( mockDir , "foo" ) ) ; expect ( content ) . toEqual ( template ) ; } ) ; it ( "should throw when using a path that starts with ~/ when no root dir is configured" , async ( ) => { const test = { $import : "~/foo/template.json" } ; await mockFs ( { "/foo/bar/test.json" : JSON . stringify ( test ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , "foo/bar/test.json" ) ) , { messageMatches : "import specifier cannot start with ~/" , errorCode : ZWaveErrorCodes . Config_Invalid } ) ; } ) ; it ( "should be able to resolve in-file selectors" , async ( ) => { const test = { $import : "template.json#sub" } ; const template = { super : "toll" , sub : { template : true } } ; const expected = template . sub ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template.json" : JSON . stringify ( template ) } ) ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( expected ) ; } ) ; it ( "should be able to resolve deep in-file selectors" , async ( ) => { const test = { $import : "template.json#we/all/live/in/1/yellow/submarine" } ; const template = { super : "toll" , we : { all : { live : { in : [ "nope" , { yellow : { submarine : { template : true } } } , ] } } } } ; const expected = { template : true } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template.json" : JSON . stringify ( template ) } ) ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( expected ) ; } ) ; it ( "selector based circular references should throw an error (three-way)" , async ( ) => { const test = { $import : "template1.json#foo" } ; const template1 = { foo : { $import : "template2.json#bar" } } ; const template2 = { test : { hello : "from the other side" } , bar : { $import : "test.json" } , baz : { $import : "#test" } } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template1.json" : JSON . stringify ( template1 ) , "/template2.json" : JSON . stringify ( template2 ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) , { errorCode : ZWaveErrorCodes . Config_CircularImport } ) ; } ) ; it ( "unspecified self-references throw an error" , async ( ) => { const test = { $import : "#" } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) , { } ) ; } ) ; it ( "circular self-references throw an error" , async ( ) => { const test = { key1 : { $import : "#key2" } , key2 : { $import : "#key1" } } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) , { errorCode : ZWaveErrorCodes . Config_CircularImport } ) ; } ) ; it ( "crazy stuff does work (part 1)" , async ( ) => { const test = { $import : "template1.json" } ; const template1 = { foo : { $import : "template2.json#test" } , baz : { $import : "template2.json#baz" } } ; const template2 = { test : { hello : "from the other side" } , baz : { $import : "#test" } } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template1.json" : JSON . stringify ( template1 ) , "/template2.json" : JSON . stringify ( template2 ) } ) ; const expected = { foo : { hello : "from the other side" } , baz : { hello : "from the other side" } } ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( expected ) ; } ) ; it ( "crazy stuff does work (part 2)" , async ( ) => { const test = { $import : "template1.json" } ; const template1 = { foo : { $import : "template2.json#test" } , baz : { $import : "template2.json#baz" } } ; const template2 = { test : { hello : "from the other side" } , baz : { $import : "#test" } } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template1.json" : JSON . stringify ( template1 ) , "/template2.json" : JSON . stringify ( template2 ) } ) ; const expected = { foo : { hello : "from the other side" } , baz : { hello : "from the other side" } } ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( expected ) ; } ) ; it ( "referencing partial parameters works" , async ( ) => { const test = { paramInformation : { 1 : { $import : "template.json#paramInformation/1[0x01]" } } } ; const template = { paramInformation : { "1[0x01]" : { hello : "from the other side" } } } ; await mockFs ( { "/test.json" : JSON . stringify ( test ) , "/template.json" : JSON . stringify ( template ) } ) ; const expected = { paramInformation : { 1 : { hello : "from the other side" } } } ; const content = await readJsonWithTemplate ( path . join ( mockDir , "test.json" ) ) ; expect ( content ) . toEqual ( expected ) ; } ) ; it ( "should throw when the referenced file is outside the rootDir" , async ( ) => { const rootDir = "root/test" ; const test = { $import : "../outside.json" } ; await mockFs ( { [ `/${ rootDir }/test.json` ] : JSON . stringify ( test ) } ) ; await assertZWaveError ( ( ) => readJsonWithTemplate ( path . join ( mockDir , rootDir , "test.json" ) , path . join ( mockDir , rootDir ) ) , { messageMatches : "outside of root" , errorCode : ZWaveErrorCodes . Config_Invalid } ) ; } ) ; } ) ;