<filename>tools/complex_permissions_test.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import os
import pty
import select
import subprocess
import sys
import time

from util import build_path, root_path, executable_suffix, green_ok, red_failed

PERMISSIONS_PROMPT_TEST_TS = "tools/complex_permissions_test.ts"

PROMPT_PATTERN = b'⚠️'
PERMISSION_DENIED_PATTERN = b'PermissionDenied: permission denied'


# This function is copied from:
# https://gist.github.com/hayd/4f46a68fc697ba8888a7b517a414583e
# https://stackoverflow.com/q/52954248/1240268
def tty_capture(cmd, bytes_input, timeout=5):
    """Capture the output of cmd with bytes_input to stdin,
    with stdin, stdout and stderr as TTYs."""
    mo, so = pty.openpty()  # provide tty to enable line-buffering
    me, se = pty.openpty()
    mi, si = pty.openpty()
    fdmap = {mo: 'stdout', me: 'stderr', mi: 'stdin'}

    timeout_exact = time.time() + timeout
    p = subprocess.Popen(
        cmd, bufsize=1, stdin=si, stdout=so, stderr=se, close_fds=True)
    os.write(mi, bytes_input)

    select_timeout = .04  #seconds
    res = {'stdout': b'', 'stderr': b''}
    while True:
        ready, _, _ = select.select([mo, me], [], [], select_timeout)
        if ready:
            for fd in ready:
                data = os.read(fd, 512)
                if not data:
                    break
                res[fdmap[fd]] += data
        elif p.poll() is not None or time.time(
        ) > timeout_exact:  # select timed-out
            break  # p exited
    for fd in [si, so, se, mi, mo, me]:
        os.close(fd)  # can't do it sooner: it leads to errno.EIO error
    p.wait()
    return p.returncode, res['stdout'], res['stderr']


# Wraps a test in debug printouts
# so we have visual indicator of what test failed
def wrap_test(test_name, test_method, *argv):
    sys.stdout.write(test_name + " ... ")
    try:
        test_method(*argv)
        print green_ok()
    except AssertionError:
        print red_failed()
        raise


class Prompt(object):
    def __init__(self, deno_exe, test_types):
        self.deno_exe = deno_exe
        self.test_types = test_types

    def run(self, flags, args, bytes_input):
        "Returns (return_code, stdout, stderr)."
        cmd = [self.deno_exe, "run"] + flags + [PERMISSIONS_PROMPT_TEST_TS
                                                ] + args
        print " ".join(cmd)
        return tty_capture(cmd, bytes_input)

    def warm_up(self):
        # ignore the ts compiling message
        self.run(["--allow-read"], ["read", "package.json"], b'')

    def test(self):
        for test_type in ["read", "write"]:
            test_name_base = "test_" + test_type
            wrap_test(test_name_base + "_inside_project_dir",
                      self.test_inside_project_dir, test_type)
            wrap_test(test_name_base + "_outside_tests_dir",
                      self.test_outside_test_dir, test_type)
            wrap_test(test_name_base + "_inside_tests_dir",
                      self.test_inside_test_dir, test_type)
            wrap_test(test_name_base + "_outside_tests_and_js_dir",
                      self.test_outside_test_and_js_dir, test_type)
            wrap_test(test_name_base + "_inside_tests_and_js_dir",
                      self.test_inside_test_and_js_dir, test_type)
            wrap_test(test_name_base + "_relative", self.test_relative,
                      test_type)
            wrap_test(test_name_base + "_no_prefix", self.test_no_prefix,
                      test_type)
        wrap_test(test_name_base + "_allow_localhost_4545",
                  self.test_allow_localhost_4545)
        wrap_test(test_name_base + "_allow_deno_land",
                  self.test_allow_deno_land)
        wrap_test(test_name_base + "_allow_localhost_4545_fail",
                  self.test_allow_localhost_4545_fail)
        wrap_test(test_name_base + "_allow_localhost",
                  self.test_allow_localhost)

    def test_inside_project_dir(self, test_type):
        code, _stdout, stderr = self.run(
            ["--no-prompt", "--allow-" + test_type + "=" + root_path],
            [test_type, "package.json", "tests/subdir/config.json"], b'')
        assert code == 0
        assert not PROMPT_PATTERN in stderr
        assert not PERMISSION_DENIED_PATTERN in stderr

    def test_outside_test_dir(self, test_type):
        code, _stdout, stderr = self.run([
            "--no-prompt",
            "--allow-" + test_type + "=" + os.path.join(root_path, "tests")
        ], [test_type, "package.json"], b'')
        assert code == 1
        assert not PROMPT_PATTERN in stderr
        assert PERMISSION_DENIED_PATTERN in stderr

    def test_inside_test_dir(self, test_type):
        code, _stdout, stderr = self.run([
            "--no-prompt",
            "--allow-" + test_type + "=" + os.path.join(root_path, "tests")
        ], [test_type, "tests/subdir/config.json"], b'')
        assert code == 0
        assert not PROMPT_PATTERN in stderr
        assert not PERMISSION_DENIED_PATTERN in stderr

    def test_outside_test_and_js_dir(self, test_type):
        code, _stdout, stderr = self.run([
            "--no-prompt", "--allow-" + test_type + "=" + os.path.join(
                root_path, "tests") + "," + os.path.join(root_path, "js")
        ], [test_type, "package.json"], b'')
        assert code == 1
        assert not PROMPT_PATTERN in stderr
        assert PERMISSION_DENIED_PATTERN in stderr

    def test_inside_test_and_js_dir(self, test_type):
        code, _stdout, stderr = self.run([
            "--no-prompt", "--allow-" + test_type + "=" + os.path.join(
                root_path, "tests") + "," + os.path.join(root_path, "js")
        ], [test_type, "js/dir_test.ts", "tests/subdir/config.json"], b'')
        assert code == 0
        assert not PROMPT_PATTERN in stderr
        assert not PERMISSION_DENIED_PATTERN in stderr

    def test_allow_localhost_4545(self):
        code, _stdout, stderr = self.run(
            ["--no-prompt", "--allow-net=localhost:4545"],
            ["net", "http://localhost:4545"], b'')
        assert code == 0
        assert not PROMPT_PATTERN in stderr
        assert not PERMISSION_DENIED_PATTERN in stderr

    def test_allow_deno_land(self):
        code, _stdout, stderr = self.run(
            ["--no-prompt", "--allow-net=deno.land"],
            ["net", "http://localhost:4545"], b'')
        assert code == 1
        assert not PROMPT_PATTERN in stderr
        assert PERMISSION_DENIED_PATTERN in stderr

    def test_allow_localhost_4545_fail(self):
        code, _stdout, stderr = self.run(
            ["--no-prompt", "--allow-net=localhost:4545"],
            ["net", "http://localhost:4546"], b'')
        assert code == 1
        assert not PROMPT_PATTERN in stderr
        assert PERMISSION_DENIED_PATTERN in stderr

    def test_allow_localhost(self):
        code, _stdout, stderr = self.run(
            ["--no-prompt", "--allow-net=localhost"], [
                "net", "http://localhost:4545", "http://localhost:4546",
                "http://localhost:4547"
            ], b'')
        assert code == 0
        assert not PROMPT_PATTERN in stderr
        assert not PERMISSION_DENIED_PATTERN in stderr

    def test_relative(self, test_type):
        # Save and restore curdir
        saved_curdir = os.getcwd()
        os.chdir(root_path)
        code, _stdout, stderr = self.run(
            ["--no-prompt", "--allow-" + test_type + "=" + "./tests"],
            [test_type, "tests/subdir/config.json"], b'')
        assert code == 0
        assert not PROMPT_PATTERN in stderr
        assert not PERMISSION_DENIED_PATTERN in stderr
        os.chdir(saved_curdir)

    def test_no_prefix(self, test_type):
        # Save and restore curdir
        saved_curdir = os.getcwd()
        os.chdir(root_path)
        code, _stdout, stderr = self.run(
            ["--no-prompt", "--allow-" + test_type + "=" + "tests"],
            [test_type, "tests/subdir/config.json"], b'')
        assert code == 0
        assert not PROMPT_PATTERN in stderr
        assert not PERMISSION_DENIED_PATTERN in stderr
        os.chdir(saved_curdir)


def complex_permissions_test(deno_exe):
    p = Prompt(deno_exe, ["read", "write", "net"])
    p.test()


def main():
    print "Permissions prompt tests"
    deno_exe = os.path.join(build_path(), "deno" + executable_suffix)
    complex_permissions_test(deno_exe)


if __name__ == "__main__":
    main()
