packages feed

language-dockerfile-0.1.2.0: test/Test.hs

import           Data.List                      (find)
import           Data.Maybe                     (fromMaybe, isJust)

import           Language.Dockerfile.Normalize
import           Language.Dockerfile.Parser
import           Language.Dockerfile.Rules
import           Language.Dockerfile.Syntax

import           Test.Framework
import           Test.Framework.Providers.HUnit
import           Test.HUnit

assertAst s ast = case parseString (s ++ "\n") of
    Left err          -> assertFailure $ show err
    Right dockerfile  -> assertEqual "ASTs are not equal" ast $ map instruction dockerfile

assertChecks rule s f = case parseString (s ++ "\n") of
    Left err -> assertFailure $ show err
    Right dockerfile  -> f $ analyze [rule] dockerfile

-- Assert a failed check exists for rule
ruleCatches :: Rule -> String -> Assertion
ruleCatches rule s = assertChecks rule s f
    where f checks = assertEqual "No check for rule found" 1 $ length checks

ruleCatchesNot :: Rule -> String -> Assertion
ruleCatchesNot rule s = assertChecks rule s f
    where f checks = assertEqual "Found check of rule" 0 $ length checks

normalizeTests = [ "join escaped lines" ~: assertEqual "Lines are not joined" expected $ normalizeEscapedLines dockerfile
                 , "join long cmd" ~: assertEqual "Lines are not joined" longEscapedCmdExpected $ normalizeEscapedLines longEscapedCmd
                 ]
    where expected = unlines ["ENV foo=bar  baz=foz", ""]
          dockerfile = unlines ["ENV foo=bar \\", "baz=foz"]


longEscapedCmd = unlines
    [ "RUN wget https://download.com/${version}.tar.gz -O /tmp/logstash.tar.gz && \\"
    , "(cd /tmp && tar zxf logstash.tar.gz && mv logstash-${version} /opt/logstash && \\"
    , "rm logstash.tar.gz) && \\"
    , "(cd /opt/logstash && \\"
    ,  "/opt/logstash/bin/plugin install contrib)"
    ]

longEscapedCmdExpected = concat
    [ "RUN wget https://download.com/${version}.tar.gz -O /tmp/logstash.tar.gz &&  "
    , "(cd /tmp && tar zxf logstash.tar.gz && mv logstash-${version} /opt/logstash &&  "
    , "rm logstash.tar.gz) &&  "
    , "(cd /opt/logstash &&  "
    ,  "/opt/logstash/bin/plugin install contrib)\n"
    , "\n"
    , "\n"
    , "\n"
    , "\n"
    ]


astTests =
    [ "from untagged" ~: assertAst "FROM busybox" [From (UntaggedImage "busybox")]
    , "env pair" ~: assertAst "ENV foo=bar" [Env [("foo", "bar")]]
    , "env space pair" ~: assertAst "ENV foo bar" [Env [("foo", "bar")] ]
    , "env quoted pair" ~: assertAst "ENV foo=\"bar\"" [Env [("foo", "bar")]]
    , "env multi raw pair" ~: assertAst "ENV foo=bar baz=foo" [Env [("foo", "bar"), ("baz", "foo")]]
    , "env multi quoted pair" ~: assertAst "ENV foo=\"bar\" baz=\"foo\"" [Env [("foo", "bar"), ("baz", "foo")]]
    , "one line cmd" ~: assertAst "CMD true" [Cmd ["true"]]
    , "multiline cmd" ~: assertAst "CMD true \\\n && true" [Cmd ["true", "&&", "true"], EOL]
    , "maintainer " ~: assertAst "MAINTAINER hudu@mail.com" [Maintainer "hudu@mail.com"]
    , "maintainer from" ~: assertAst maintainerFromProg maintainerFromAst
    , "quoted exec" ~: assertAst "CMD [\"echo\",  \"1\"]" [Cmd ["echo", "1"]]
    , "env works with cmd" ~: assertAst envWorksCmdProg envWorksCmdAst
    , "multicomments first" ~: assertAst multiCommentsProg1 [Comment " line 1", Comment " line 2", Run ["apt-get", "update"], EOL]
    , "multicomments after" ~: assertAst multiCommentsProg2 [Run ["apt-get", "update"], Comment " line 1", Comment " line 2", EOL]
    , "escape with space" ~: assertAst escapedWithSpaceProgram [Run ["yum", "install", "-y", "imagemagick", "mysql"], EOL, EOL]
    , "scratch and maintainer" ~: assertAst "FROM scratch\nMAINTAINER hudu@mail.com" [From (UntaggedImage "scratch"), Maintainer "hudu@mail.com"]
    ] where
        maintainerFromProg = "FROM busybox\nMAINTAINER hudu@mail.com"
        maintainerFromAst = [ From (UntaggedImage "busybox")
                            , Maintainer "hudu@mail.com"
                            ]
        envWorksCmdProg = "ENV PATH=\"/root\"\nCMD [\"hadolint\",\"-i\"]"
        envWorksCmdAst = [ Env [("PATH", "/root")]
                         , Cmd ["hadolint", "-i"]
                         ]
        multiCommentsProg1 = unlines [ "# line 1"
                                     , "# line 2"
                                     , "RUN apt-get update"
                                     ]
        multiCommentsProg2 = unlines [ "RUN apt-get update"
                                     , "# line 1"
                                     , "# line 2"
                                     ]
        escapedWithSpaceProgram = unlines [ "RUN yum install -y \\ "
                                          , "imagemagick \\ "
                                          , "mysql"
                                          ]

ruleTests =
    [ "untagged" ~: ruleCatches noUntagged "FROM debian"
    , "explicit latest" ~: ruleCatches noLatestTag "FROM debian:latest"
    , "explicit tagged" ~: ruleCatchesNot noLatestTag "FROM debian:jessie"
    , "sudo" ~: ruleCatches noSudo "RUN sudo apt-get update"
    , "no root" ~: ruleCatches noRootUser "USER root"
    , "install sudo" ~: ruleCatchesNot noSudo "RUN apt-get install sudo"
    , "sudo chained programs" ~: ruleCatches noSudo "RUN apt-get update && sudo apt-get install"
    , "invalid cmd" ~: ruleCatches invalidCmd "RUN top"
    , "install ssh" ~: ruleCatchesNot invalidCmd "RUN apt-get install ssh"
    , "apt upgrade" ~: ruleCatches noUpgrade "RUN apt-get update && apt-get upgrade"
    , "apt-get version pinning" ~: ruleCatches aptGetVersionPinned "RUN apt-get update && apt-get install python"
    , "apt-get no cleanup" ~: ruleCatches aptGetCleanup "RUN apt-get update && apt-get install python"
    , "apt-get cleanup" ~: ruleCatchesNot aptGetCleanup "RUN apt-get update && apt-get install python && rm -rf /var/lib/apt/lists/*"
    , "use add" ~: ruleCatches useAdd "COPY packaged-app.tar /usr/src/app"
    , "use not add" ~: ruleCatchesNot useAdd "COPY package.json /usr/src/app"
    , "invalid port" ~: ruleCatches invalidPort "EXPOSE 80000"
    , "valid port" ~: ruleCatchesNot invalidPort "EXPOSE 60000"
    , "maintainer address" ~: ruleCatches maintainerAddress "MAINTAINER Lukas"
    , "maintainer uri" ~: ruleCatchesNot maintainerAddress "MAINTAINER Lukas <me@lukasmartinelli.ch>"
    , "maintainer uri" ~: ruleCatchesNot maintainerAddress "MAINTAINER John Doe <john.doe@example.net>"
    , "maintainer mail" ~: ruleCatchesNot maintainerAddress "MAINTAINER http://lukasmartinelli.ch"
    , "pip requirements" ~: ruleCatchesNot pipVersionPinned "RUN pip install -r requirements.txt"
    , "pip version not pinned" ~: ruleCatches pipVersionPinned "RUN pip install MySQL_python"
    , "pip version pinned" ~: ruleCatchesNot pipVersionPinned "RUN pip install MySQL_python==1.2.2"
    , "apt-get auto yes" ~: ruleCatches aptGetYes "RUN apt-get install python"
    , "apt-get yes shortflag" ~: ruleCatchesNot aptGetYes "RUN apt-get install -yq python"
    , "apt-get yes different pos" ~: ruleCatchesNot aptGetYes "RUN apt-get install -y python"
    , "apt-get with auto yes" ~: ruleCatchesNot aptGetYes "RUN apt-get -y install python"
    , "apt-get with auto expanded yes" ~: ruleCatchesNot aptGetYes "RUN apt-get --yes install python"
    , "apt-get install recommends" ~: ruleCatchesNot aptGetNoRecommends "RUN apt-get install --no-install-recommends python"
    , "apt-get no install recommends" ~: ruleCatches aptGetNoRecommends "RUN apt-get install python"
    , "apt-get no install recommends" ~: ruleCatches aptGetNoRecommends "RUN apt-get -y install python"
    , "apt-get version" ~: ruleCatchesNot aptGetVersionPinned "RUN apt-get install -y python=1.2.2"
    , "apt-get pinned" ~: ruleCatchesNot aptGetVersionPinned "RUN apt-get -y --no-install-recommends install nodejs=0.10"
    , "apt-get pinned chained" ~: ruleCatchesNot aptGetVersionPinned $ unlines aptGetPinnedChainedProgram
    , "apt-get pinned regression" ~: ruleCatchesNot aptGetVersionPinned $ unlines aptGetPinnedRegressionProgram
    , "has maintainer named" ~: ruleCatchesNot hasMaintainer "FROM busybox\nMAINTAINER hudu@mail.com"
    , "has maintainer" ~: ruleCatchesNot hasMaintainer "FROM debian\nMAINTAINER Lukas"
    , "has maintainer first" ~: ruleCatchesNot hasMaintainer "MAINTAINER Lukas\nFROM DEBIAN"
    , "has no maintainer" ~: ruleCatches hasMaintainer "FROM debian"
    , "using add" ~: ruleCatches copyInsteadAdd "ADD file /usr/src/app/"
    , "add is ok for archive" ~: ruleCatchesNot copyInsteadAdd "ADD file.tar /usr/src/app/"
    , "add is ok for url" ~: ruleCatchesNot copyInsteadAdd "ADD http://file.com /usr/src/app/"
    , "many cmds" ~: ruleCatches multipleCmds "CMD /bin/true\nCMD /bin/true"
    , "single cmd" ~: ruleCatchesNot multipleCmds "CMD /bin/true"
    , "no cmd" ~: ruleCatchesNot multipleEntrypoints "FROM busybox"
    , "many entries" ~: ruleCatches multipleEntrypoints "ENTRYPOINT /bin/true\nENTRYPOINT /bin/true"
    , "single entry" ~: ruleCatchesNot multipleEntrypoints "ENTRYPOINT /bin/true"
    , "no entry" ~: ruleCatchesNot multipleEntrypoints "FROM busybox"
    , "workdir variable" ~: ruleCatchesNot absoluteWorkdir "WORKDIR ${work}"
    , "scratch" ~: ruleCatchesNot noUntagged "FROM scratch"
    ] where
    aptGetPinnedChainedProgram =
        [ "RUN apt-get update \\"
        , " && apt-get -y --no-install-recommends install nodejs=0.10 \\"
        , " && rm -rf /var/lib/apt/lists/*"
        ]
    aptGetPinnedRegressionProgram =
        [ "RUN apt-get update && apt-get install --no-install-recommends -y \\"
        , "python-demjson=2.2.2* \\"
        , "wget=1.16.1* \\"
        , "git=1:2.5.0* \\"
        , "ruby=1:2.1.*"
        ]

tests = test $ ruleTests ++ astTests ++ normalizeTests
main = defaultMain $ hUnitTestToTests tests