#!/usr/bin/env python
#
# Copyright (C) 2008, 2009 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.


# This module is used for version 2 of the Google Data APIs.


__author__ = 'j.s@google.com (Jeff Scudder)'


import unittest
import gdata.client
import gdata.gauth
import gdata.data
import atom.mock_http_core
import StringIO


class ClientLoginTest(unittest.TestCase):

  def test_token_request(self):
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.SettableHttpClient(200, 'OK', 
        'SID=DQAAAGgA...7Zg8CTN\n'
        'LSID=DQAAAGsA...lk8BBbG\n'
        'Auth=DQAAAGgA...dk3fA5N', {'Content-Type': 'text/plain'})
    token = client.request_client_login_token('email', 'pw', 'cp', 'test')
    self.assert_(isinstance(token, gdata.gauth.ClientLoginToken))
    self.assertEqual(token.token_string, 'DQAAAGgA...dk3fA5N')

    # Test a server response without a ClientLogin token.`
    client.http_client.set_response(200, 'OK', 'SID=12345\nLSID=34567', {})
    self.assertRaises(gdata.client.ClientLoginTokenMissing,
        client.request_client_login_token, 'email', 'pw', '', '')

    # Test a 302 redirect from the server on a login request.
    client.http_client.set_response(302, 'ignored', '', {})
    # TODO: change the exception class to one in gdata.client.
    self.assertRaises(gdata.client.BadAuthenticationServiceURL,
        client.request_client_login_token, 'email', 'pw', '', '')

    # Test a CAPTCHA challenge from the server
    client.http_client.set_response(403, 'Access Forbidden', 
        'Url=http://www.google.com/login/captcha\n'
        'Error=CaptchaRequired\n'
        'CaptchaToken=DQAAAGgA...dkI1LK9\n'
        # TODO: verify this sample CAPTCHA URL matches an
        # actual challenge from the server.
        'CaptchaUrl=Captcha?ctoken=HiteT4bVoP6-yFkHPibe7O9EqxeiI7lUSN', {})
    try:
      token = client.request_client_login_token('email', 'pw', '', '')
      self.fail('should raise a CaptchaChallenge on a 403 with a '
                'CaptchRequired error.')
    except gdata.client.CaptchaChallenge, challenge:
      self.assertEquals(challenge.captcha_url, 
          'http://www.google.com/accounts/'
          'Captcha?ctoken=HiteT4bVoP6-yFkHPibe7O9EqxeiI7lUSN')
      self.assertEquals(challenge.captcha_token, 'DQAAAGgA...dkI1LK9')

    # Test an unexpected response, a 404 for example.
    client.http_client.set_response(404, 'ignored', '', {})
    self.assertRaises(gdata.client.ClientLoginFailed,
        client.request_client_login_token, 'email', 'pw', '', '')

  def test_client_login(self):
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.SettableHttpClient(200, 'OK', 
        'SID=DQAAAGgA...7Zg8CTN\n'
        'LSID=DQAAAGsA...lk8BBbG\n'
        'Auth=DQAAAGgA...dk3fA5N', {'Content-Type': 'text/plain'})
    client.client_login('me@example.com', 'password', 'wise', 'unit test')
    self.assert_(isinstance(client.auth_token, gdata.gauth.ClientLoginToken))
    self.assertEqual(client.auth_token.token_string, 'DQAAAGgA...dk3fA5N')


class AuthSubTest(unittest.TestCase):

  def test_get_and_upgrade_token(self):
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.SettableHttpClient(200, 'OK', 
        'Token=UpgradedTokenVal\n'
        'Extra data', {'Content-Type': 'text/plain'})

    page_url = 'http://example.com/showcalendar.html?token=CKF50YzIHxCTKMAg'

    client.auth_token = gdata.gauth.AuthSubToken.from_url(page_url)

    self.assert_(isinstance(client.auth_token, gdata.gauth.AuthSubToken))
    self.assertEqual(client.auth_token.token_string, 'CKF50YzIHxCTKMAg')

    upgraded = client.upgrade_token()

    self.assert_(isinstance(client.auth_token, gdata.gauth.AuthSubToken))
    self.assertEqual(client.auth_token.token_string, 'UpgradedTokenVal')
    self.assertEqual(client.auth_token, upgraded)

    # Ensure passing in a token returns without modifying client's auth_token.
    client.http_client.set_response(200, 'OK', 'Token=4567', {})
    upgraded = client.upgrade_token(
        gdata.gauth.AuthSubToken.from_url('?token=1234'))
    self.assertEqual(upgraded.token_string, '4567')
    self.assertEqual(client.auth_token.token_string, 'UpgradedTokenVal')
    self.assertNotEqual(client.auth_token, upgraded)

    # Test exception cases
    client.auth_token = None
    self.assertRaises(gdata.client.UnableToUpgradeToken, client.upgrade_token,
                      None)
    self.assertRaises(gdata.client.UnableToUpgradeToken, client.upgrade_token)

  def test_revoke_token(self):
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.SettableHttpClient(
        200, 'OK', '', {})
    page_url = 'http://example.com/showcalendar.html?token=CKF50YzIHxCTKMAg'
    client.auth_token = gdata.gauth.AuthSubToken.from_url(page_url)
    deleted = client.revoke_token()
    self.assert_(deleted)
    self.assertEqual(
        client.http_client.last_request.headers['Authorization'],
        'AuthSub token=CKF50YzIHxCTKMAg')


class OAuthTest(unittest.TestCase):

  def test_hmac_flow(self):
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.SettableHttpClient(
        200, 'OK', 'oauth_token=ab3cd9j4ks7&oauth_token_secret=ZXhhbXBsZS',
        {})
    request_token = client.get_oauth_token(
        ['http://example.com/service'], 'http://example.net/myapp',
        'consumer', consumer_secret='secret')
    # Check that the response was correctly parsed.
    self.assertEqual(request_token.token, 'ab3cd9j4ks7')
    self.assertEqual(request_token.token_secret, 'ZXhhbXBsZS')
    self.assertEqual(request_token.auth_state, gdata.gauth.REQUEST_TOKEN)

    # Also check the Authorization header which was sent in the request.
    auth_header = client.http_client.last_request.headers['Authorization']
    self.assert_('OAuth' in auth_header)
    self.assert_(
        'oauth_callback="http%3A%2F%2Fexample.net%2Fmyapp"' in auth_header)
    self.assert_('oauth_version="1.0"' in auth_header)
    self.assert_('oauth_signature_method="HMAC-SHA1"' in auth_header)
    self.assert_('oauth_consumer_key="consumer"' in auth_header)

    # Check generation of the authorization URL.
    authorize_url = request_token.generate_authorization_url()
    self.assert_(str(authorize_url).startswith(
        'https://www.google.com/accounts/OAuthAuthorizeToken'))
    self.assert_('oauth_token=ab3cd9j4ks7' in str(authorize_url))

    # Check that the token information from the browser's URL is parsed.
    redirected_url = (
        'http://example.net/myapp?oauth_token=CKF5zz&oauth_verifier=Xhhbas')
    gdata.gauth.authorize_request_token(request_token, redirected_url)
    self.assertEqual(request_token.token, 'CKF5zz')
    self.assertEqual(request_token.verifier, 'Xhhbas')
    self.assertEqual(request_token.auth_state,
                     gdata.gauth.AUTHORIZED_REQUEST_TOKEN)

    # Check that the token upgrade response was correctly parsed.
    client.http_client.set_response(
        200, 'OK', 'oauth_token=3cd9Fj417&oauth_token_secret=Xhrh6bXBs', {})
    access_token = client.get_access_token(request_token)
    self.assertEqual(request_token.token, '3cd9Fj417')
    self.assertEqual(request_token.token_secret, 'Xhrh6bXBs')
    self.assert_(request_token.verifier is None)
    self.assertEqual(request_token.auth_state, gdata.gauth.ACCESS_TOKEN)
    self.assertEqual(request_token.token, access_token.token)
    self.assertEqual(request_token.token_secret, access_token.token_secret)
    self.assert_(access_token.verifier is None)
    self.assertEqual(request_token.auth_state, access_token.auth_state)

    # Also check the Authorization header which was sent in the request.
    auth_header = client.http_client.last_request.headers['Authorization']
    self.assert_('OAuth' in auth_header)
    self.assert_('oauth_callback="' not in auth_header)
    self.assert_('oauth_version="1.0"' in auth_header)
    self.assert_('oauth_verifier="Xhhbas"' in auth_header)
    self.assert_('oauth_signature_method="HMAC-SHA1"' in auth_header)
    self.assert_('oauth_consumer_key="consumer"' in auth_header)


class RequestTest(unittest.TestCase):

  def test_simple_request(self):
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.EchoHttpClient()
    response = client.request('GET', 'https://example.com/test')
    self.assertEqual(response.getheader('Echo-Host'), 'example.com:None')
    self.assertEqual(response.getheader('Echo-Uri'), '/test')
    self.assertEqual(response.getheader('Echo-Scheme'), 'https')
    self.assertEqual(response.getheader('Echo-Method'), 'GET')

    http_request = atom.http_core.HttpRequest(
        uri=atom.http_core.Uri(scheme='http', host='example.net', port=8080),
        method='POST', headers={'X': 1})
    http_request.add_body_part('test', 'text/plain')
    response = client.request(http_request=http_request)
    self.assertEqual(response.getheader('Echo-Host'), 'example.net:8080')
    # A Uri with path set to None should default to /.
    self.assertEqual(response.getheader('Echo-Uri'), '/')
    self.assertEqual(response.getheader('Echo-Scheme'), 'http')
    self.assertEqual(response.getheader('Echo-Method'), 'POST')
    self.assertEqual(response.getheader('Content-Type'), 'text/plain')
    self.assertEqual(response.getheader('X'), '1')
    self.assertEqual(response.read(), 'test')

    # Use the same request object from above, but overwrite the request path
    # by passing in a URI.
    response = client.request(uri='/new/path?p=1', http_request=http_request)
    self.assertEqual(response.getheader('Echo-Host'), 'example.net:8080')
    self.assertEqual(response.getheader('Echo-Uri'), '/new/path?p=1')
    self.assertEqual(response.getheader('Echo-Scheme'), 'http')
    self.assertEqual(response.read(), 'test')

  def test_gdata_version_header(self):
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.EchoHttpClient()

    response = client.request('GET', 'http://example.com')
    self.assertEqual(response.getheader('GData-Version'), None)

    client.api_version = '2'
    response = client.request('GET', 'http://example.com')
    self.assertEqual(response.getheader('GData-Version'), '2')

  def test_redirects(self):
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.MockHttpClient()
    # Add the redirect response for the initial request.
    first_request = atom.http_core.HttpRequest('http://example.com/1', 
                                               'POST')
    client.http_client.add_response(first_request, 302, None, 
        {'Location': 'http://example.com/1?gsessionid=12'})
    second_request = atom.http_core.HttpRequest(
        'http://example.com/1?gsessionid=12', 'POST')
    client.http_client.AddResponse(second_request, 200, 'OK', body='Done')

    response = client.Request('POST', 'http://example.com/1')
    self.assertEqual(response.status, 200)
    self.assertEqual(response.reason, 'OK')
    self.assertEqual(response.read(), 'Done')

    redirect_loop_request = atom.http_core.HttpRequest(
        'http://example.com/2?gsessionid=loop', 'PUT')
    client.http_client.add_response(redirect_loop_request, 302, None, 
        {'Location': 'http://example.com/2?gsessionid=loop'})
    try:
      response = client.request(method='PUT', uri='http://example.com/2?gsessionid=loop')
      self.fail('Loop URL should have redirected forever.')
    except gdata.client.RedirectError, err:
      self.assert_(str(err).startswith('Too many redirects from server'))

  def test_lowercase_location(self):
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.MockHttpClient()
    # Add the redirect response for the initial request.
    first_request = atom.http_core.HttpRequest('http://example.com/1', 
                                               'POST')
    # In some environments, notably App Engine, the HTTP headers which come
    # back from a server will be normalized to all lowercase.
    client.http_client.add_response(first_request, 302, None, 
        {'location': 'http://example.com/1?gsessionid=12'})
    second_request = atom.http_core.HttpRequest(
        'http://example.com/1?gsessionid=12', 'POST')
    client.http_client.AddResponse(second_request, 200, 'OK', body='Done')

    response = client.Request('POST', 'http://example.com/1')
    self.assertEqual(response.status, 200)
    self.assertEqual(response.reason, 'OK')
    self.assertEqual(response.read(), 'Done')

  def test_exercise_exceptions(self):
    # TODO
    pass

  def test_converter_vs_desired_class(self):

    def bad_converter(string):
      return 1
  
    class TestClass(atom.core.XmlElement):
      _qname = '{http://www.w3.org/2005/Atom}entry'
    
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.EchoHttpClient()
    test_entry = gdata.data.GDEntry()
    result = client.post(test_entry, 'http://example.com')
    self.assert_(isinstance(result, gdata.data.GDEntry))
    result = client.post(test_entry, 'http://example.com', converter=bad_converter)
    self.assertEquals(result, 1)
    result = client.post(test_entry, 'http://example.com', desired_class=TestClass)
    self.assert_(isinstance(result, TestClass))


class QueryTest(unittest.TestCase):

  def test_query_modifies_request(self):
    request = atom.http_core.HttpRequest()
    gdata.client.Query(
        text_query='foo', categories=['a', 'b']).modify_request(request)
    # categories param from above is named category in URL
    self.assertEqual(request.uri.query, {'q': 'foo', 'category': 'a,b'})

  def test_client_uses_query_modification(self):
    """If the Query is passed as an unexpected param it should apply"""
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.EchoHttpClient()
    query = gdata.client.Query(max_results=7)

    client.http_client = atom.mock_http_core.SettableHttpClient(
        201, 'CREATED', gdata.data.GDEntry().ToString(), {})
    response = client.get('https://example.com/foo', a_random_param=query)
    self.assertEqual(
        client.http_client.last_request.uri.query['max-results'], '7')


class VersionConversionTest(unittest.TestCase):

  def test_use_default_version(self):
    self.assertEquals(gdata.client.get_xml_version(None), 1)

  def test_str_to_int_version(self):
    self.assertEquals(gdata.client.get_xml_version('1'), 1)
    self.assertEquals(gdata.client.get_xml_version('2'), 2)
    self.assertEquals(gdata.client.get_xml_version('2.1.2'), 2)
    self.assertEquals(gdata.client.get_xml_version('10.4'), 10)


class UpdateTest(unittest.TestCase):
  """Test Update/PUT"""

  def test_update_uri_editlink(self):
    """Test that the PUT uri is grabbed from the entry's edit link"""
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.SettableHttpClient(
        200, 'OK', gdata.data.GDEntry().ToString(), {})
    entry = gdata.data.GDEntry()
    entry.link.append(atom.data.Link(rel='edit', href='https://example.com/edit'))
    response = client.update(entry)
    request = client.http_client.last_request
    self.assertEqual(str(client.http_client.last_request.uri),
                     'https://example.com/edit')

  def test_update_uri(self):
    """Test that when passed, a uri overrides the entry's edit link"""
    client = gdata.client.GDClient()
    client.http_client = atom.mock_http_core.SettableHttpClient(
        200, 'OK', gdata.data.GDEntry().ToString(), {})
    entry = gdata.data.GDEntry()
    entry.link.append(atom.data.Link(rel='edit', href='https://example.com/edit'))
    response = client.update(entry, uri='https://example.com/test')
    self.assertEqual(str(client.http_client.last_request.uri),
                     'https://example.com/test')


def suite():
  return unittest.TestSuite((unittest.makeSuite(ClientLoginTest, 'test'),
                             unittest.makeSuite(AuthSubTest, 'test'),
                             unittest.makeSuite(OAuthTest, 'test'),
                             unittest.makeSuite(RequestTest, 'test'),
                             unittest.makeSuite(VersionConversionTest, 'test'),
                             unittest.makeSuite(QueryTest, 'test'),
                             unittest.makeSuite(UpdateTest, 'test')))


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