#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Tests for devappserver2.login."""


import Cookie
import unittest
import urllib
import wsgiref.util

from google.appengine.tools.devappserver2 import login

# Values used by many tests
COOKIE_NAME = 'my_fancy_cookie'
EMAIL = 'johnny@example.com'
USER_ID = '115914779145204185301'


class CookieTest(unittest.TestCase):
  """Tests for the cookie handling functions."""

  def test_get_user_info_admin(self):
    """Tests the get_user_info function when the admin field is True."""
    cookie_value = '%s:True:%s' % (EMAIL, USER_ID)

    http_cookie = 'one=two; %s=%s; three=four' % (COOKIE_NAME, cookie_value)
    email, admin, user_id = login.get_user_info(http_cookie,
                                                cookie_name=COOKIE_NAME)

    self.assertEqual(EMAIL, email)
    self.assertTrue(admin)
    self.assertEqual(USER_ID, user_id)

  def test_get_user_info_not_admin(self):
    """Tests the get_user_info function when the admin field is False."""
    cookie_value = '%s:False:%s' % (EMAIL, USER_ID)

    http_cookie = 'one=two; %s=%s; three=four' % (COOKIE_NAME, cookie_value)
    email, admin, user_id = login.get_user_info(http_cookie,
                                                cookie_name=COOKIE_NAME)

    self.assertEqual(EMAIL, email)
    self.assertFalse(admin)
    self.assertEqual(USER_ID, user_id)

  def test_get_user_info_invalid_email(self):
    """Tests the get_user_info function when the admin field is False."""
    cookie_value = 'foo:True:%s' % USER_ID

    http_cookie = 'one=two; %s=%s; three=four' % (COOKIE_NAME, cookie_value)
    email, admin, user_id = login.get_user_info(http_cookie,
                                                cookie_name=COOKIE_NAME)

    self.assertEqual('', email)
    self.assertFalse(admin)
    self.assertEqual('', user_id)

  def test_get_user_info_does_not_exist(self):
    """Tests the get_user_info function when the cookie is not present."""
    http_cookie = 'one=two; three=four'
    email, admin, user_id = login.get_user_info(http_cookie,
                                                cookie_name=COOKIE_NAME)

    self.assertEqual('', email)
    self.assertFalse(admin)
    self.assertEqual('', user_id)

  def test_get_user_info_bad_cookie(self):
    """Tests the get_user_info function when the cookie is malformed."""
    cookie_name = 'SinaRot/g/get'  # seen in the wild
    cookie_value = 'blah'

    http_cookie = '%s=%s' % (cookie_name, cookie_value)
    email, admin, user_id = login.get_user_info(http_cookie,
                                                cookie_name=cookie_name)

    self.assertEqual('', email)
    self.assertFalse(admin)
    self.assertEqual('', user_id)

  def test_get_user_info_from_dict_admin(self):
    """Tests the get_user_info function when the admin field is True."""
    cookie_value = '%s:True:%s' % (EMAIL, USER_ID)

    cookie_dict = {'one': 'two', COOKIE_NAME: cookie_value, 'three': 'four'}
    email, admin, user_id = login._get_user_info_from_dict(
        cookie_dict, cookie_name=COOKIE_NAME)

    self.assertEqual(EMAIL, email)
    self.assertTrue(admin)
    self.assertEqual(USER_ID, user_id)

  def test_get_user_info_from_dict_not_admin(self):
    """Tests the get_user_info function when the admin field is False."""
    cookie_value = '%s:False:%s' % (EMAIL, USER_ID)

    cookie_dict = {'one': 'two', COOKIE_NAME: cookie_value, 'three': 'four'}
    email, admin, user_id = login._get_user_info_from_dict(
        cookie_dict, cookie_name=COOKIE_NAME)

    self.assertEqual(EMAIL, email)
    self.assertFalse(admin)
    self.assertEqual(USER_ID, user_id)

  def test_get_user_info_from_dict_does_not_exist(self):
    """Tests the get_user_info function when the cookie is not present."""
    cookie_dict = {'one': 'two', 'three': 'four'}
    email, admin, user_id = login._get_user_info_from_dict(
        cookie_dict, cookie_name=COOKIE_NAME)

    self.assertEqual('', email)
    self.assertFalse(admin)
    self.assertEqual('', user_id)

  def test_set_user_info_cookie(self):
    """Tests the set_user_info_cookie function."""
    cookie_value = '%s:True:%s' % (EMAIL, USER_ID)
    expected_result = '%s="%s"; Path=/' % (COOKIE_NAME, cookie_value)

    result = login._set_user_info_cookie(EMAIL, True, cookie_name=COOKIE_NAME)

    self.assertEqual(expected_result, result)

  def test_clear_user_info_cookie(self):
    """Tests the clear_user_info_cookie function."""
    expected_result = '%s=; Max-Age=0; Path=/' % COOKIE_NAME

    result = login._clear_user_info_cookie(cookie_name=COOKIE_NAME)

    self.assertEqual(expected_result, result)


class LoginRedirectTest(unittest.TestCase):
  """Tests the login_redirect function."""

  def test_basic(self):
    """Tests that redirects are written back to the user."""
    application_url = 'http://foo.com:1234'
    continue_url = ('http://foo.com:1234/my/album/of/pictures?'
                    'with=some&query=parameters')

    expected_location = (
        'http://foo.com:1234/_ah/login?continue='
        'http%3A//foo.com%3A1234'
        '/my/album/of/pictures%3Fwith%3Dsome%26query%3Dparameters')

    def start_response(status, headers, exc_info=None):
      self.assertTrue(status.startswith('302'))
      headers = dict(headers)
      self.assertEqual({'Location': expected_location}, headers)
      self.assertEqual(None, exc_info)
    body = login.login_redirect(application_url, continue_url, start_response)

    self.assertEqual('', ''.join(body))


class LoginPageTest(unittest.TestCase):
  """Tests the various ways of invoking the login page."""

  def test_no_params(self):
    """Tests just accessing the login URL with no params."""
    host = 'foo.com:1234'
    path_info = '/_ah/login'
    cookie_dict = {}
    action = ''
    set_email = ''
    set_admin = False
    continue_url = ''
    status, location, set_cookie, content_type = self._run_test(
        host, path_info, cookie_dict, action, set_email, set_admin,
        continue_url)
    self.assertEqual(200, status)
    self.assertFalse(location)
    self.assertFalse(set_cookie)
    self.assertEqual('text/html', content_type)

  def test_login(self):
    """Tests when setting the user info with and without continue URL."""
    host = 'foo.com:1234'
    path_info = '/_ah/login'
    cookie_dict = {}
    action = 'Login'
    set_email = EMAIL
    set_admin = False
    continue_url = ''

    expected_set = login._set_user_info_cookie(set_email, set_admin).strip()

    # No continue URL.
    status, location, set_cookie, _ = self._run_test(
        host, path_info, cookie_dict, action, set_email, set_admin,
        continue_url)
    self.assertEqual(302, status)
    self.assertEqual('http://%s%s' % (host, path_info), location)
    self.assertEqual(expected_set, set_cookie)
    self.assertIsInstance(location, str)
    self.assertIsInstance(set_cookie, str)

    # Continue URL.
    continue_url = 'http://foo.com/blah'
    status, location, set_cookie, _ = self._run_test(
        host, path_info, cookie_dict, action, set_email, set_admin,
        continue_url)
    self.assertEqual(302, status)
    self.assertEqual(continue_url, location)
    self.assertEqual(expected_set, set_cookie)
    self.assertIsInstance(location, str)
    self.assertIsInstance(set_cookie, str)

  def test_logout(self):
    """Tests when logging out with and without continue URL."""
    host = 'foo.com:1234'
    path_info = '/_ah/login'
    cookie_dict = {'dev_appserver_login': '%s:False:%s' % (EMAIL, USER_ID)}
    action = 'Logout'
    set_email = ''
    set_admin = False
    continue_url = ''

    expected_set = login._clear_user_info_cookie().strip()

    # No continue URL.
    status, location, set_cookie, _ = self._run_test(
        host, path_info, cookie_dict, action, set_email, set_admin,
        continue_url)
    self.assertEqual(302, status)
    self.assertEqual('http://%s%s' % (host, path_info), location)
    self.assertEqual(expected_set, set_cookie)
    self.assertIsInstance(location, str)
    self.assertIsInstance(set_cookie, str)

    # Continue URL.
    continue_url = 'http://foo.com/blah'
    status, location, set_cookie, _ = self._run_test(
        host, path_info, cookie_dict, action, set_email, set_admin,
        continue_url)
    self.assertEqual(302, status)
    self.assertEqual(continue_url, location)
    self.assertEqual(expected_set, set_cookie)
    self.assertIsInstance(location, str)
    self.assertIsInstance(set_cookie, str)

  def test_passive(self):
    """Tests when the user is already logged in."""
    host = 'foo.com:1234'
    path_info = '/_ah/login'
    cookie_dict = {'dev_appserver_login': '%s:False:%s' % (EMAIL, USER_ID)}
    action = ''
    set_email = ''
    set_admin = False
    continue_url = '/my/fancy/url'

    # Continue URL.
    continue_url = 'http://foo.com/blah'
    status, location, set_cookie, content_type = self._run_test(
        host, path_info, cookie_dict, action, set_email, set_admin,
        continue_url)
    self.assertEqual(200, status)
    self.assertFalse(location)
    self.assertFalse(set_cookie)
    self.assertEqual('text/html; charset=utf-8', content_type)
    self.assertIsInstance(content_type, str)

  def _run_test(self, host, path_info='/', cookie_dict=None, action=None,
                set_email=None, set_admin=None, continue_url=None,
                method='GET'):
    """Runs the login HTTP handler, returning information about the response.

    Args:
      host: The value of the HTTP Host header.
      path_info: The absolute path of the request.
      cookie_dict: A cookie dictionary with the existing cookies.
      action: Value of the 'action' query argument.
      set_email: Value of the 'email' query argument.
      set_admin: Value of the 'admin' query argument.
      continue_url: Value of the 'continue' query argument.
      method: The HTTP method (e.g., 'GET').

    Returns:
      Tuple (status, location, set_cookie, content_type) where each
        value is the value of the corresponding header from the
        response; if no header exists, the value will be None. In the
        case of status, it will just return the integer status code
        and not the rest of the status message.
    """
    environ = {}
    wsgiref.util.setup_testing_defaults(environ)
    # The SERVER_NAME should never be used by the login module -- always defer
    # to the HTTP Host (so the user is not redirected to a different domain).
    environ['SERVER_NAME'] = 'do_not_use'
    environ['SERVER_PORT'] = '666'
    environ['SERVER_PROTOCOL'] = 'HTTP/1.1'
    environ['HTTP_HOST'] = host
    environ['PATH_INFO'] = path_info
    environ['REQUEST_METHOD'] = method
    if cookie_dict:
      cookie = Cookie.SimpleCookie(cookie_dict)
      cookie_value = ';'.join(m.OutputString() for m in cookie.values())
      environ['HTTP_COOKIE'] = cookie_value
    query_dict = {}
    if action:
      query_dict['action'] = action
    if set_email:
      query_dict['email'] = set_email
    if set_admin:
      query_dict['admin'] = set_admin
    if continue_url:
      query_dict['continue'] = continue_url
    if query_dict:
      environ['QUERY_STRING'] = urllib.urlencode(query_dict)

    response_dict = {}

    def start_response(status, headers):
      response_dict['status'] = int(status.split(' ', 1)[0])
      response_dict['headers'] = dict((k.lower(), v)
                                      for (k, v) in headers)

    login.application(environ, start_response)

    return (response_dict['status'],
            response_dict['headers'].get('location'),
            response_dict['headers'].get('set-cookie'),
            response_dict['headers'].get('content-type'))


if __name__ == '__main__':
  unittest.main()
