import fs from "fs" ; import path from "path" ; import { promisify } from "util" ; import uuid = require ( "uuid/v4" ) ; import { Bedrock } from "../../config" ; import * as config from "../../config" ; import * as bedrockYaml from "../../lib/bedrockYaml" ; import { SERVICE_PIPELINE_FILENAME } from "../../lib/constants" ; import { checkoutCommitPushCreatePRLink } from "../../lib/gitpr" ; import { createTempDir , removeDir } from "../../lib/ioUtil" ; import * as dns from "../../lib/net/dns" ; import { deepClone } from "../../lib/util" ; import { disableVerboseLogging , enableVerboseLogging , logger , } from "../../logger" ; import { createTestBedrockYaml , createTestMaintainersYaml , } from "../../test/mockFactory" ; import { assertValidDnsInputs , createService , execute , fetchValues , CommandValues , validateGitUrl , validUpperUnderscoreCase , } from "./create" ; import { BedrockServiceConfig } from "../../types" ; jest . mock ( "../../lib/gitpr" ) ; jest . mock ( "../../lib/gitutils" ) ; beforeAll ( ( ) => { enableVerboseLogging ( ) ; } ) ; afterAll ( ( ) => { disableVerboseLogging ( ) ; } ) ; beforeEach ( ( ) => { jest . clearAllMocks ( ) ; } ) ; const mockValues = { gitPush : false , helmChartChart : "" , helmChartRepository : "" , helmConfigAccessTokenVariable : "ACCESS_TOKEN_SECRET" , helmConfigBranch : "" , helmConfigGit : "" , helmConfigPath : "" , k8sBackend : "" , k8sBackendPort : "80" , k8sPort : 80 , maintainerEmail : "" , maintainerName : "" , middlewares : "" , middlewaresArray : [ ] , packagesDir : "" , pathPrefix : "" , pathPrefixMajorVersion : "" , ringNames : [ ] , variableGroups : [ ] , serviceBuildVg : "" , serviceVgArray : [ ] , serviceBuildVariables : "" , serviceVariablesArray : [ ] , } ; const getMockValues = ( ) : CommandValues => { return deepClone ( mockValues ) ; } ; const validateDirNFiles = ( dir , servicePath , values ) : void => { expect ( fs . existsSync ( dir ) ) . toBe ( true ) ; const serviceDirPath = path . join ( dir , values . packagesDir , servicePath ) ; expect ( fs . existsSync ( serviceDirPath ) ) . toBe ( true ) ; const filepaths = [ SERVICE_PIPELINE_FILENAME , "Dockerfile" ] . map ( ( filename ) => path . join ( serviceDirPath , filename ) ) ; for ( const filepath of filepaths ) { expect ( fs . existsSync ( filepath ) ) . toBe ( true ) ; } } ; describe ( "Test fetchValues function" , ( ) => { it ( "Negative test: invalid port" , ( ) => { const values = getMockValues ( ) ; values . k8sBackendPort = "abc" ; try { fetchValues ( values ) ; expect ( true ) . toBe ( false ) ; } catch ( err ) { expect ( err ) . toBeDefined ( ) ; } } ) ; it ( "Positive test: with middlewares value" , ( ) => { jest . spyOn ( config , "Bedrock" ) . mockReturnValueOnce ( bedrockYaml . DEFAULT_CONTENT ( ) ) ; const mocked = getMockValues ( ) ; mocked . middlewares = "mid1, mid2" ; const result = fetchValues ( mocked ) ; expect ( result . middlewaresArray ) . toEqual ( [ "mid1" , "mid2" ] ) ; } ) ; it ( "Positive test: with serviceBuildVg value" , ( ) => { jest . spyOn ( config , "Bedrock" ) . mockReturnValueOnce ( bedrockYaml . DEFAULT_CONTENT ( ) ) ; const mocked = getMockValues ( ) ; mocked . serviceBuildVg = "foo,bar" ; const result = fetchValues ( mocked ) ; expect ( result . serviceVgArray ) . toEqual ( [ "foo" , "bar" ] ) ; } ) ; it ( "Negative test: with serviceBuildVg value" , ( ) => { jest . spyOn ( config , "Bedrock" ) . mockReturnValueOnce ( bedrockYaml . DEFAULT_CONTENT ( ) ) ; const mocked = getMockValues ( ) ; mocked . serviceBuildVg = "foo,,bar" ; expect ( ( ) => fetchValues ( mocked ) ) . toThrow ( ) ; } ) ; it ( "Positive test: with serviceBuildVariables value" , ( ) => { jest . spyOn ( config , "Bedrock" ) . mockReturnValueOnce ( bedrockYaml . DEFAULT_CONTENT ( ) ) ; const mocked = getMockValues ( ) ; mocked . serviceBuildVariables = "foo,bar" ; const result = fetchValues ( mocked ) ; expect ( result . serviceVariablesArray ) . toEqual ( [ "foo" , "bar" ] ) ; } ) ; it ( "Positive test: with bedrock rings" , ( ) => { const mockedBedrockFileConfig = { ... bedrockYaml . DEFAULT_CONTENT ( ) } ; mockedBedrockFileConfig . rings = { master : { } , qa : { } , } ; jest . spyOn ( config , "Bedrock" ) . mockReturnValueOnce ( mockedBedrockFileConfig ) ; const mocked = getMockValues ( ) ; mocked . ringNames = [ "master" , "qa" ] ; const result = fetchValues ( mocked ) ; expect ( result . ringNames ) . toEqual ( [ "master" , "qa" ] ) ; } ) ; it ( "Positive test" , ( ) => { const mocked = getMockValues ( ) ; jest . spyOn ( config , "Bedrock" ) . mockReturnValueOnce ( bedrockYaml . DEFAULT_CONTENT ( ) ) ; const result = fetchValues ( mocked ) ; expect ( result ) . toEqual ( mocked ) ; } ) ; } ) ; describe ( "validUpperUnderscoreCase" , ( ) => { test ( "valid variable inputs" , ( ) => { const validVariable = "FOOBAR" ; const invalidVariable = "foo-bar" ; const anotherValidVariable = "FOO_BAR_BAZ" ; expect ( validUpperUnderscoreCase ( validVariable ) ) . toBe ( true ) ; expect ( validUpperUnderscoreCase ( anotherValidVariable ) ) . toBe ( true ) ; expect ( validUpperUnderscoreCase ( invalidVariable ) ) . toBe ( false ) ; } ) ; } ) ; describe ( "isValidDnsInputs" , ( ) => { test ( "valid inputs does not throw" , ( ) => { expect ( ( ) => assertValidDnsInputs ( { k8sBackend : "my-service" , pathPrefix : "service" , pathPrefixMajorVersion : "v1" , } ) ) . not . toThrow ( ) ; } ) ; test ( "invalid inputs throws" , ( ) => { expect ( ( ) => assertValidDnsInputs ( { k8sBackend : "-not_dns_compliant" , pathPrefix : "" , pathPrefixMajorVersion : "" , } ) ) . toThrow ( ) ; expect ( ( ) => assertValidDnsInputs ( { k8sBackend : "" , pathPrefix : "-not_dns_compliant" , pathPrefixMajorVersion : "" , } ) ) . toThrow ( ) ; expect ( ( ) => assertValidDnsInputs ( { k8sBackend : "" , pathPrefix : "" , pathPrefixMajorVersion : "-not_dns_compliant" , } ) ) . toThrow ( ) ; } ) ; } ) ; describe ( "Test execute function" , ( ) => { it ( "Negative test: with non-dns compliant values" , async ( ) => { const exitFn = jest . fn ( ) ; jest . spyOn ( dns , "assertIsValid" ) ; await execute ( "-non_dns@compliant" , "foo" , { ... getMockValues ( ) } , exitFn ) ; expect ( dns . assertIsValid ) . toHaveBeenCalledTimes ( 1 ) ; } ) ; it ( "Negative test: without service name" , async ( ) => { const exitFn = jest . fn ( ) ; await execute ( "" , "my/service/path" , getMockValues ( ) , exitFn ) ; expect ( exitFn ) . toBeCalledTimes ( 1 ) ; expect ( exitFn . mock . calls ) . toEqual ( [ [ 1 ] ] ) ; } ) ; it ( "Negative test: missing bedrock file" , async ( ) => { const testServiceName = uuid ( ) ; const testServicePath = "my/service/path" ; const exitFn = jest . fn ( ) ; jest . spyOn ( bedrockYaml , "fileInfo" ) . mockImplementation ( ( ) => ( { exist : false , hasVariableGroups : false , } ) ) ; try { await execute ( testServiceName , testServicePath , getMockValues ( ) , exitFn ) ; expect ( exitFn ) . toBeCalledTimes ( 1 ) ; expect ( exitFn . mock . calls ) . toEqual ( [ [ 1 ] ] ) ; } finally { removeDir ( testServiceName ) ; } } ) ; it ( "Negative test: missing bedrock variable groups" , async ( ) => { const testServiceName = uuid ( ) ; const testServicePath = "my/service/path" ; const exitFn = jest . fn ( ) ; jest . spyOn ( bedrockYaml , "fileInfo" ) . mockImplementation ( ( ) => ( { exist : true , hasVariableGroups : false , } ) ) ; try { await execute ( testServiceName , testServicePath , getMockValues ( ) , exitFn ) ; expect ( exitFn ) . toBeCalledTimes ( 1 ) ; expect ( exitFn . mock . calls ) . toEqual ( [ [ 1 ] ] ) ; } finally { removeDir ( testServiceName ) ; } } ) ; } ) ; describe ( "Validate Git URLs" , ( ) => { it ( "Should error when providing an invalid git url" , ( ) => { const exitFn = jest . fn ( ) ; const helmConfigPath = "dev.azure.com/foo/bar" ; validateGitUrl ( helmConfigPath , exitFn ) ; expect ( exitFn ) . toBeCalledTimes ( 1 ) ; } ) ; it ( "Should not error when providing a valid github https url" , ( ) => { const exitFn = jest . fn ( ) ; const helmConfigPath = "https://github.com/microsoft/bedrock-cli.git" ; validateGitUrl ( helmConfigPath , exitFn ) ; expect ( exitFn ) . toBeCalledTimes ( 0 ) ; } ) ; it ( "Should not error when providing a valid azdo https url" , ( ) => { const exitFn = jest . fn ( ) ; const helmConfigPath = "https://dev@dev.azure.com/catalystcode/project/_git/repo" ; validateGitUrl ( helmConfigPath , exitFn ) ; expect ( exitFn ) . toBeCalledTimes ( 0 ) ; } ) ; it ( "Should not error when providing a valid azdo git+ssh url" , ( ) => { const exitFn = jest . fn ( ) ; const helmConfigPath = "git@ssh.dev.azure.com:v3/CatalystCode/project/repo" ; validateGitUrl ( helmConfigPath , exitFn ) ; expect ( exitFn ) . toBeCalledTimes ( 0 ) ; } ) ; it ( "Should not error when providing a valid github git+ssh url" , ( ) => { const exitFn = jest . fn ( ) ; const helmConfigPath = "git@github.com:microsoft/bedrock-cli.git" ; validateGitUrl ( helmConfigPath , exitFn ) ; expect ( exitFn ) . toBeCalledTimes ( 0 ) ; } ) ; } ) ; const writeSampleMaintainersFileToDir = async ( maintainersFilePath ) : Promise < void > => { await promisify ( fs . writeFile ) ( maintainersFilePath , createTestMaintainersYaml ( ) , "utf8" ) ; } ; const writeSampleBedrockFileToDir = async ( bedrockFilePath ) : Promise < void > => { await promisify ( fs . writeFile ) ( bedrockFilePath , createTestBedrockYaml ( ) , "utf8" ) ; } ; describe ( "Adding a service to a repo directory" , ( ) => { let randomTmpDir = "" ; beforeEach ( ( ) => { randomTmpDir = createTempDir ( ) ; } ) ; test ( "New service is created in project root directory. With display name given, so this works fine." , async ( ) => { await writeSampleMaintainersFileToDir ( path . join ( randomTmpDir , "maintainers.yaml" ) ) ; await writeSampleBedrockFileToDir ( path . join ( randomTmpDir , "bedrock.yaml" ) ) ; const values = getMockValues ( ) ; values . packagesDir = "" ; values . k8sPort = 1337 ; values . helmConfigAccessTokenVariable = "SOME_ENV_VAR" ; const serviceName = "my-service-name" ; const servicePath = "." ; logger . info ( `creating randomTmpDir ${ randomTmpDir } and service ${ serviceName }` ) ; expect ( createService ( randomTmpDir , serviceName , servicePath , values ) ) . resolves ; validateDirNFiles ( randomTmpDir , servicePath , values ) ; const bedrock = Bedrock ( randomTmpDir ) ; const newService = bedrock . services . find ( ( s ) => s . displayName === "my-service-name" ) as BedrockServiceConfig ; expect ( newService ) . toStrictEqual ( expect . objectContaining ( { path : "./" , k8sBackendPort : values . k8sPort , displayName : serviceName , helm : { chart : expect . objectContaining ( { accessTokenVariable : "SOME_ENV_VAR" , } ) , } , } ) ) ; } ) ; test ( "New directory is created under root directory with required service files." , async ( ) => { await writeSampleMaintainersFileToDir ( path . join ( randomTmpDir , "maintainers.yaml" ) ) ; await writeSampleBedrockFileToDir ( path . join ( randomTmpDir , "bedrock.yaml" ) ) ; const values = getMockValues ( ) ; values . packagesDir = "" ; values . k8sPort = 1337 ; const serviceName = uuid ( ) ; const servicePath = uuid ( ) ; logger . info ( `creating randomTmpDir ${ randomTmpDir } and service ${ serviceName }` ) ; await createService ( randomTmpDir , serviceName , servicePath , values ) ; validateDirNFiles ( randomTmpDir , servicePath , values ) ; const bedrock = Bedrock ( randomTmpDir ) ; const newService = bedrock . services . find ( ( s ) => s . displayName === serviceName ) as BedrockServiceConfig ; expect ( newService ) . toStrictEqual ( expect . objectContaining ( { path : "./" + servicePath , k8sBackendPort : values . k8sPort , displayName : serviceName , } ) ) ; } ) ; test ( "New directory is created under '/packages' directory with required service files." , async ( ) => { await writeSampleMaintainersFileToDir ( path . join ( randomTmpDir , "maintainers.yaml" ) ) ; await writeSampleBedrockFileToDir ( path . join ( randomTmpDir , "bedrock.yaml" ) ) ; const values = getMockValues ( ) ; values . packagesDir = "packages" ; values . k8sPort = 1337 ; const serviceName = uuid ( ) ; const servicePath = uuid ( ) ; logger . info ( `creating randomTmpDir ${ randomTmpDir } and service ${ serviceName }` ) ; await createService ( randomTmpDir , serviceName , servicePath , values ) ; validateDirNFiles ( randomTmpDir , servicePath , values ) ; } ) ; test ( "New directory is created under '/packages' directory with required service files and git push enabled." , async ( ) => { await writeSampleMaintainersFileToDir ( path . join ( randomTmpDir , "maintainers.yaml" ) ) ; await writeSampleBedrockFileToDir ( path . join ( randomTmpDir , "bedrock.yaml" ) ) ; const values = getMockValues ( ) ; values . packagesDir = "packages" ; values . gitPush = true ; values . k8sPort = 1337 ; const serviceName = uuid ( ) ; const servicePath = uuid ( ) ; logger . info ( `creating randomTmpDir ${ randomTmpDir } and service ${ serviceName }` ) ; await createService ( randomTmpDir , serviceName , servicePath , values ) ; validateDirNFiles ( randomTmpDir , servicePath , values ) ; expect ( checkoutCommitPushCreatePRLink ) . toHaveBeenCalled ( ) ; } ) ; test ( "empty middleware list is created when none provided" , async ( ) => { await writeSampleMaintainersFileToDir ( path . join ( randomTmpDir , "maintainers.yaml" ) ) ; await writeSampleBedrockFileToDir ( path . join ( randomTmpDir , "bedrock.yaml" ) ) ; const values = getMockValues ( ) ; values . k8sPort = 1337 ; const serviceName = uuid ( ) ; const servicePath = uuid ( ) ; logger . info ( `creating randomTmpDir ${ randomTmpDir } and service ${ serviceName }` ) ; await createService ( randomTmpDir , serviceName , servicePath , values ) ; validateDirNFiles ( randomTmpDir , servicePath , values ) ; const bedrockConfig = Bedrock ( randomTmpDir ) ; for ( const service of bedrockConfig . services ) { if ( service . displayName === serviceName ) { expect ( service . middlewares ) . toBeDefined ( ) ; if ( service . middlewares ) { expect ( Array . isArray ( service . middlewares ) ) . toBe ( true ) ; expect ( service . middlewares . length ) . toBe ( 0 ) ; } } } } ) ; test ( "middleware gets added when provided" , async ( ) => { await writeSampleMaintainersFileToDir ( path . join ( randomTmpDir , "maintainers.yaml" ) ) ; await writeSampleBedrockFileToDir ( path . join ( randomTmpDir , "bedrock.yaml" ) ) ; const values = getMockValues ( ) ; values . k8sPort = 1337 ; values . middlewaresArray = [ "foo" , "bar" , "baz" ] ; const serviceName = uuid ( ) ; const servicePath = uuid ( ) ; logger . info ( `creating randomTmpDir ${ randomTmpDir } and service ${ serviceName }` ) ; await createService ( randomTmpDir , serviceName , servicePath , values ) ; validateDirNFiles ( randomTmpDir , servicePath , values ) ; const bedrockConfig = Bedrock ( randomTmpDir ) ; for ( const service of bedrockConfig . services ) { if ( service . displayName === serviceName ) { expect ( service . middlewares ) . toBeDefined ( ) ; expect ( Array . isArray ( service . middlewares ) ) . toBe ( true ) ; expect ( service . middlewares ?. length ) . toBe ( values . middlewaresArray . length ) ; expect ( service . middlewares ) . toStrictEqual ( values . middlewaresArray ) ; } } } ) ; test ( "service build vg and variables get added" , async ( ) => { await writeSampleMaintainersFileToDir ( path . join ( randomTmpDir , "maintainers.yaml" ) ) ; await writeSampleBedrockFileToDir ( path . join ( randomTmpDir , "bedrock.yaml" ) ) ; const values = getMockValues ( ) ; const serviceName = uuid ( ) ; const servicePath = uuid ( ) ; values . k8sPort = 1337 ; values . serviceBuildVg = "foo,bar,baz" ; values . serviceBuildVariables = "FOO,BAR,BAZ" ; values . serviceVgArray = [ "foo" , "bar" , "baz" ] ; values . serviceVariablesArray = [ "FOO" , "BAR" , "BAZ" ] ; logger . info ( `creating randomTmpDir ${ randomTmpDir } and service ${ serviceName }` ) ; await createService ( randomTmpDir , serviceName , servicePath , values ) ; validateDirNFiles ( randomTmpDir , servicePath , values ) ; const bedrockConfig = Bedrock ( randomTmpDir ) ; for ( const service of bedrockConfig . services ) { if ( service . displayName === serviceName ) { expect ( service . serviceBuildVg ) . toBeDefined ( ) ; expect ( service . serviceBuildVariables ) . toBeDefined ( ) ; expect ( service . serviceBuildVg ) . toStrictEqual ( values . serviceVgArray ) ; expect ( service . serviceBuildVariables ) . toStrictEqual ( values . serviceVariablesArray ) ; } } } ) ; } ) ;