#!/usr/bin/env python3
# Copyright 2015 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import copy
import feature_compiler
import unittest

class FeatureCompilerTest(unittest.TestCase):
  """Test the FeatureCompiler. Note that we test that the expected features are
  generated more thoroughly in features_generation_unittest.cc. And, of course,
  this is most exhaustively tested through Chrome's compilation process (if a
  feature fails to parse, the compile fails).
  These tests primarily focus on catching errors during parsing.
  """
  def _parseFeature(self, value):
    """Parses a feature from the given value and returns the result."""
    f = feature_compiler.Feature('alpha')
    f.Parse(value, {})
    return f

  def _createTestFeatureCompiler(self, feature_class):
    return feature_compiler.FeatureCompiler('chrome_root', [], feature_class,
        'provider_class', 'out_root', 'gen', 'out_base_filename')

  def _hasError(self, f, error):
    """Asserts that |error| is present somewhere in the given feature's
    errors."""
    errors = f.GetErrors()
    self.assertTrue(errors)
    self.assertNotEqual(-1, str(errors).find(error), str(errors))

  def setUp(self):
    feature_compiler.ENABLE_ASSERTIONS = False

  def testFeature(self):
    # Test some basic feature parsing for a sanity check.
    f = self._parseFeature({
      'blocklist': [
        'ABCDEF0123456789ABCDEF0123456789ABCDEF01',
        '10FEDCBA9876543210FEDCBA9876543210FEDCBA'
      ],
      'channel': 'stable',
      'command_line_switch': 'switch',
      'component_extensions_auto_granted': False,
      'contexts': [
        'blessed_extension',
        'blessed_web_page',
        'lock_screen_extension'
      ],
      'default_parent': True,
      'dependencies': ['dependency1', 'dependency2'],
      'developer_mode_only': True,
      'disallow_for_service_workers': True,
      'extension_types': ['extension'],
      'location': 'component',
      'internal': True,
      'matches': ['*://*/*'],
      'max_manifest_version': 1,
      'requires_delegated_availability_check': True,
      'noparent': True,
      'platforms': ['mac', 'win'],
      'session_types': ['kiosk', 'regular'],
      'allowlist': [
        '0123456789ABCDEF0123456789ABCDEF01234567',
        '76543210FEDCBA9876543210FEDCBA9876543210'
      ]
    })
    self.assertFalse(f.GetErrors())

  def testInvalidAll(self):
    f = self._parseFeature({
      'channel': 'stable',
      'dependencies': 'all',
    })
    self._hasError(f, 'Illegal value: "all"')

  def testUnknownKeyError(self):
    f = self._parseFeature({
      'contexts': ['blessed_extension'],
      'channel': 'stable',
      'unknownkey': 'unknownvalue'
    })
    self._hasError(f, 'Unrecognized key')

  def testUnknownEnumValue(self):
    f = self._parseFeature({
      'contexts': ['blessed_extension', 'unknown_context'],
      'channel': 'stable'
    })
    self._hasError(f, 'Illegal value: "unknown_context"')

  def testImproperType(self):
    f = self._parseFeature({'min_manifest_version': '1'})
    self._hasError(f, 'Illegal value: "1"')

  def testImproperSubType(self):
    f = self._parseFeature({'dependencies': [1, 2, 3]})
    self._hasError(f, 'Illegal value: "1"')

  def testImproperValue(self):
    f = self._parseFeature({'noparent': False})
    self._hasError(f, 'Illegal value: "False"')

  def testEmptyList(self):
    f = self._parseFeature({'extension_types': []})
    self._hasError(f, 'List must specify at least one element.')

  def testEmptyListWithAllowEmpty(self):
    # `dependencies` is the only key that allows an empty list.
    f = self._parseFeature({'dependencies': []})
    self.assertFalse(f.GetErrors())

  def testApiFeaturesNeedContexts(self):
    f = self._parseFeature({'extension_types': ['extension'],
                            'channel': 'trunk'})
    f.Validate('APIFeature', {})
    self._hasError(f, 'APIFeatures must specify the contexts property')

  def testAPIFeaturesCanSpecifyEmptyContexts(self):
    f = self._parseFeature({'extension_types': ['extension'],
                            'channel': 'trunk',
                            'contexts': []})
    f.Validate('APIFeature', {})
    self.assertFalse(f.GetErrors())

  def testManifestFeaturesNeedExtensionTypes(self):
    f = self._parseFeature({'dependencies': 'alpha', 'channel': 'beta'})
    f.Validate('ManifestFeature', {})
    self._hasError(f,
                   'ManifestFeatures must specify at least one extension type')

  def testManifestFeaturesCantHaveContexts(self):
    f = self._parseFeature({'dependencies': 'alpha',
                            'channel': 'beta',
                            'extension_types': ['extension'],
                            'contexts': ['blessed_extension']})
    f.Validate('ManifestFeature', {})
    self._hasError(f, 'ManifestFeatures do not support contexts')

  def testPermissionFeaturesNeedExtensionTypes(self):
    f = self._parseFeature({'dependencies': 'alpha', 'channel': 'beta'})
    f.Validate('PermissionFeature', {})
    self._hasError(
        f, 'PermissionFeatures must specify at least one extension type')

  def testPermissionFeaturesCantHaveContexts(self):
    f = self._parseFeature({'dependencies': 'alpha',
                            'channel': 'beta',
                            'extension_types': ['extension'],
                            'contexts': ['blessed_extension']})
    f.Validate('PermissionFeature', {})
    self._hasError(f, 'PermissionFeatures do not support contexts')

  def testAllPermissionsNeedChannelOrDependencies(self):
    api_feature = self._parseFeature({'contexts': ['blessed_extension']})
    api_feature.Validate('APIFeature', {})
    self._hasError(
        api_feature, 'Features must specify either a channel or dependencies')
    permission_feature = self._parseFeature({'extension_types': ['extension']})
    permission_feature.Validate('PermissionFeature', {})
    self._hasError(permission_feature,
                   'Features must specify either a channel or dependencies')
    manifest_feature = self._parseFeature({'extension_types': ['extension']})
    manifest_feature.Validate('ManifestFeature', {})
    self._hasError(manifest_feature,
                   'Features must specify either a channel or dependencies')
    channel_feature = self._parseFeature({'contexts': ['blessed_extension'],
                                          'channel': 'trunk'})
    channel_feature.Validate('APIFeature', {})
    self.assertFalse(channel_feature.GetErrors())
    dependency_feature = self._parseFeature(
                             {'contexts': ['blessed_extension'],
                              'dependencies': ['alpha']})
    dependency_feature.Validate('APIFeature', {})
    self.assertFalse(dependency_feature.GetErrors())

  def testBothAliasAndSource(self):
    compiler = self._createTestFeatureCompiler('APIFeature')
    compiler._json = {
      'feature_alpha': {
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'alias': 'feature_alpha',
        'source': 'feature_alpha'
      }
    }
    compiler.Compile()

    feature = compiler._features.get('feature_alpha')
    self.assertTrue(feature)
    self._hasError(feature, 'Features cannot specify both alias and source.')

  def testAliasOnNonApiFeature(self):
    compiler = self._createTestFeatureCompiler('PermissionFeature')
    compiler._json = {
      'feature_alpha': {
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'alias': 'feature_beta'
      },
      'feature_beta': [{
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'source': 'feature_alpha'
      },{
        'channel': 'dev',
        'context': ['blessed_extension']
      }]
    };
    compiler.Compile()

    feature = compiler._features.get('feature_alpha')
    self.assertTrue(feature)
    self._hasError(feature, 'PermissionFeatures do not support alias.')

    feature = compiler._features.get('feature_beta')
    self.assertTrue(feature)
    self._hasError(feature, 'PermissionFeatures do not support source.')

  def testAliasFeature(self):
    compiler = self._createTestFeatureCompiler('APIFeature')
    compiler._json = {
      'feature_alpha': {
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'alias': 'feature_beta'
      },
      'feature_beta': {
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'source': 'feature_alpha'
      }
    };
    compiler.Compile()

    feature = compiler._features.get('feature_alpha')
    self.assertTrue(feature)
    self.assertFalse(feature.GetErrors())

    feature = compiler._features.get('feature_beta')
    self.assertTrue(feature)
    self.assertFalse(feature.GetErrors())

  def testMultipleAliasesInComplexFeature(self):
    compiler = self._createTestFeatureCompiler('APIFeature')
    compiler._json = {
      'feature_alpha': [{
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'alias': 'feature_beta'
      }, {
        'contexts': ['blessed_extension'],
        'channel': 'beta',
        'alias': 'feature_beta'
      }]
    };
    compiler.Compile()

    feature = compiler._features.get('feature_alpha')
    self.assertTrue(feature)
    self._hasError(feature, 'Error parsing feature "feature_alpha" at key ' +
                            '"alias": Key can be set at most once per feature.')

  def testAliasReferenceInComplexFeature(self):
    compiler = self._createTestFeatureCompiler('APIFeature')
    compiler._json = {
      'feature_alpha': [{
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'alias': 'feature_beta'
      }, {
        'contexts': ['blessed_extension'],
        'channel': 'beta',
      }],
      'feature_beta': {
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'source': 'feature_alpha'
      }
    };
    compiler.Compile()

    feature = compiler._features.get('feature_alpha')
    self.assertTrue(feature)
    self.assertFalse(feature.GetErrors())

    feature = compiler._features.get('feature_beta')
    self.assertTrue(feature)
    self.assertFalse(feature.GetErrors())

  def testSourceMissingReference(self):
    compiler = self._createTestFeatureCompiler('APIFeature')
    compiler._json = {
      'feature_alpha': {
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'alias': 'feature_beta'
      },
      'feature_beta': {
        'contexts': ['blessed_extension'],
        'channel': 'beta',
        'source': 'does_not_exist'
      }
    };
    compiler.Compile()

    feature = compiler._features.get('feature_beta')
    self.assertTrue(feature)
    self._hasError(feature, 'A feature source property should reference a ' +
                            'feature whose alias property references it back.')


  def testAliasMissingReferenceInComplexFeature(self):
    compiler = self._createTestFeatureCompiler('APIFeature')
    compiler._json = {
      'feature_alpha': [{
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'alias': 'feature_beta'
      }, {
        'contexts': ['blessed_extension'],
        'channel': 'beta'
      }]
    };
    compiler.Compile()

    feature = compiler._features.get('feature_alpha')
    self.assertTrue(feature)
    self._hasError(feature, 'A feature alias property should reference a ' +
                            'feature whose source property references it back.')

  def testAliasReferenceMissingSourceInComplexFeature(self):
    compiler = self._createTestFeatureCompiler('APIFeature')
    compiler._json = {
      'feature_alpha': {
        'contexts': ['blessed_extension'],
        'channel': 'beta',
      },
      'feature_beta': {
        'channel': 'beta',
        'contexts': ['blessed_extension'],
        'alias': 'feature_alpha'
      }
    };
    compiler.Compile()

    feature = compiler._features.get('feature_alpha')
    self.assertTrue(feature)
    self.assertFalse(feature.GetErrors())

    feature = compiler._features.get('feature_beta')
    self.assertTrue(feature)
    self._hasError(feature, 'A feature alias property should reference a ' +
                            'feature whose source property references it back.')

  def testComplexParentWithoutDefaultParent(self):
    c = feature_compiler.FeatureCompiler(
        None, None, 'APIFeature', None, None, None, None)
    c._CompileFeature('bookmarks',
        [{
          'contexts': ['blessed_extension'],
        }, {
          'channel': 'stable',
          'contexts': ['webui'],
        }])

    with self.assertRaisesRegex(AssertionError,
                                'No default parent found for bookmarks'):
      c._CompileFeature('bookmarks.export', { "allowlist": ["asdf"] })

  def testComplexFeatureWithSinglePropertyBlock(self):
    compiler = self._createTestFeatureCompiler('APIFeature')

    error = ('Error parsing feature "feature_alpha": A complex feature '
             'definition is only needed when there are multiple objects '
             'specifying different groups of properties for feature '
             'availability. You can reduce it down to a single object on the '
             'feature key instead of a list.')
    with self.assertRaisesRegex(AssertionError, error):
      compiler._CompileFeature('feature_alpha',
        [{
          'contexts': ['blessed_extension'],
          'channel': 'stable',
        }])

  def testRealIdsDisallowedInAllowlist(self):
    fake_id = 'a' * 32;
    f = self._parseFeature({'allowlist': [fake_id],
                            'extension_types': ['extension'],
                            'channel': 'beta'})
    f.Validate('PermissionFeature', {})
    self._hasError(
        f, 'list should only have hex-encoded SHA1 hashes of extension ids')

  def testHostedAppsCantUseAllowlistedFeatures_SimpleFeature(self):
    f = self._parseFeature({
        'extension_types': ['extension', 'hosted_app'],
        'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567'],
        'channel': 'beta',
    })
    f.Validate('PermissionFeature', {})
    self._hasError(f, 'Hosted apps are not allowed to use restricted features')

  def testHostedAppsCantUseAllowlistedFeatures_ComplexFeature(self):
    c = feature_compiler.FeatureCompiler(
        None, None, 'PermissionFeature', None, None, None, None)
    c._CompileFeature('invalid_feature',
        [{
          'extension_types': ['extension'],
          'channel': 'beta',
        }, {
          'channel': 'beta',
          'extension_types': ['hosted_app'],
          'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567'],
        }])
    c._CompileFeature('valid_feature',
        [{
          'extension_types': ['extension'],
          'channel': 'beta',
          'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567'],
        }, {
          'channel': 'beta',
          'extension_types': ['hosted_app'],
        }])

    valid_feature = c._features.get('valid_feature')
    self.assertTrue(valid_feature)
    self.assertFalse(valid_feature.GetErrors())

    invalid_feature = c._features.get('invalid_feature')
    self.assertTrue(invalid_feature)
    self._hasError(invalid_feature,
                   'Hosted apps are not allowed to use restricted features')


  def testHostedAppsCantUseAllowlistedFeatures_ChildFeature(self):
    c = feature_compiler.FeatureCompiler(
        None, None, 'PermissionFeature', None, None, None, None)
    c._CompileFeature('parent',
        {
          'extension_types': ['hosted_app'],
          'channel': 'beta',
        })

    c._CompileFeature('parent.child',
        {
          'allowlist': ['0123456789ABCDEF0123456789ABCDEF01234567']
        })
    feature = c._features.get('parent.child')
    self.assertTrue(feature)
    self._hasError(feature,
                   'Hosted apps are not allowed to use restricted features')

  def testEmptyContextsDisallowed(self):
    compiler = self._createTestFeatureCompiler('APIFeature')
    compiler._json = {
      'feature_alpha': {
        'channel': 'beta',
        'contexts': [],
        'extension_types': ['extension']
      }
    }
    compiler.Compile()

    feature = compiler._features.get('feature_alpha')
    self.assertTrue(feature)
    self._hasError(feature,
        'An empty contexts list is not allowed for this feature.')

  def testEmptyContextsAllowed(self):
    compiler = self._createTestFeatureCompiler('APIFeature')
    compiler._json = {
      'empty_contexts': {
        'channel': 'beta',
        'contexts': [],
        'extension_types': ['extension']
      }
    }
    compiler.Compile()

    feature = compiler._features.get('empty_contexts')
    self.assertTrue(feature)
    self.assertFalse(feature.GetErrors())

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