ation and truncation settings.
    pol = policy.ParsePolicy(GOOD_HEADER_1 + SHORT_TERM_NAME, self.naming)
    acl = ACLMock(pol, EXP_INFO)
    for _, terms in pol.filters:
      for term in terms:
        result = acl.FixTermLength(term.name, True, True)
        self.assertEqual(term.name, result)
        result = acl.FixTermLength(term.name, True, False)
        self.assertEqual(term.name, result)
        result = acl.FixTermLength(term.name, False, True)
        self.assertEqual(term.name, result)
        result = acl.FixTermLength(term.name, False, False)
        self.assertEqual(term.name, result)
        result = acl.FixTermLength(term.name, False, False, 30)
        self.assertEqual(term.name, result)

  def testLongTermAbbreviation(self):
    # Term name that is above specified limit should come out abbreviated
    # when abbreviation is enabled.
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_LONG_TERM_NAME, self.naming)
    acl = ACLMock(pol, EXP_INFO)
    for _, terms in pol.filters:
      for term in terms:
        result = acl.FixTermLength(term.name, True, False)
        self.assertIn('-abbreviations', result,
                      'Our strings disappeared during abbreviation.')
        # override the term max length and ensure there are no abbreviations.
        result = acl.FixTermLength(term.name, True, False,
                                   4 * acl._TERM_MAX_LENGTH)
        self.assertNotIn(
            'GOOG', result,
            'Strings incorrect in abbreviation and length overriding.')

  def testTermNameTruncation(self):
    # Term name that is above specified limit should come out truncated
    # when truncation is enabled.
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_LONG_TERM_NAME, self.naming)
    acl = ACLMock(pol, EXP_INFO)
    for _, terms in pol.filters:
      for term in terms:
        result = acl.FixTermLength(term.name, False, True)
        self.assertEqual('google-experiment-abbrev', result)
        result = acl.FixTermLength(term.name, True, False,
                                   4 * acl._TERM_MAX_LENGTH)
        self.assertIn(
            'google-experiment-abbreviations', result,
            'Strings incorrectly disappeared during abbreviation '
            'and length overriding.')

  def testHexDigest(self):
    # Term name that is above specified limit should come out truncated
    # when truncation is enabled.
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_LONG_TERM_NAME, self.naming)
    acl = ACLMock(pol, EXP_INFO)
    for _, terms in pol.filters:
      for term in terms:
        result = acl.HexDigest(term.name)
        self.assertEqual(
            '070582f8b50d3cb01aa432c26a55b5f378d281c98647f59dd7f3b0d8b1c9d0d5',
            result)
        result = acl.HexDigest(term.name, 32)
        self.assertEqual('070582f8b50d3cb01aa432c26a55b5f3', result)

  def testLongTermName(self):
    # Term name that is above specified limit and is impossible to abbreviate
    # should raise an exception.
    pol = policy.ParsePolicy(GOOD_HEADER_1 + BAD_LONG_TERM_NAME, self.naming)
    acl = ACLMock(pol, EXP_INFO)
    for _, terms in pol.filters:
      for term in terms:
        self.assertRaises(aclgenerator.TermNameTooLongError,
                          acl.FixTermLength, term.name, True, False)

  def testProtocolNameToNumber(self):
    proto_map = {'icmp': 1,
                 'ipip': 4,
                 'tcp': 6,
                 'gre': 47,
                 }
    proto_convert = ['gre', 'tcp']

    protocol_list = ['icmp', 'gre', 'tcp', 'ipip']
    expected_protocol_list = ['icmp', 47, 6, 'ipip']

    retprotocol_list = aclgenerator.ProtocolNameToNumber(protocol_list,
                                                         proto_convert,
                                                         proto_map)

    self.assertListEqual(expected_protocol_list, retprotocol_list)

  def testAddRepositoryTags(self):
    # Format print the '$' into the RCS tags in order prevent the tags from
    # being interpolated here.

    # Include all tags.
    self.assertListEqual(
        ['%sId:%s' % ('$', '$'),
         '%sDate:%s' % ('$', '$'),
         '%sRevision:%s' % ('$', '$')], aclgenerator.AddRepositoryTags())
    # Remove the revision tag.
    self.assertListEqual(
        ['%sId:%s' % ('$', '$'),
         '%sDate:%s' % ('$', '$')],
        aclgenerator.AddRepositoryTags(revision=False))
    # Only include the Id: tag.
    self.assertListEqual(
        ['%sId:%s' % ('$', '$')],
        aclgenerator.AddRepositoryTags(date=False, revision=False))
    # Wrap the Date: tag.
    self.assertListEqual(
        ['"%sDate:%s"' % ('$', '$')],
        aclgenerator.AddRepositoryTags(revision=False, rid=False, wrap=True))


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


# tests/lib/arista_test.py

# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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 arista acl rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import arista
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER = """
header {
  comment:: "this is a test extended acl"
  target:: arista test-filter extended
}
"""

GOOD_HEADER_2 = """
header {
  comment:: "this is a test acl"
  target:: arista test-filter
}
"""

GOOD_HEADER_3 = """
header {
  comment:: "this is a test standard acl"
  target:: arista test-filter standard
}
"""

GOOD_HEADER_IPV6 = """
header {
  comment:: "this is a test inet6 acl"
  target:: arista test-filter inet6
}
"""

GOOD_TERM = """
term good-term {
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  protocol:: tcp
  option:: tcp-established
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  source-address:: SOME_HOST
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_3 = """
term good-term-3 {
  source-address:: SOME_HOST2
  destination-port:: GOPENFLOW
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_4 = """
term good-term-4 {
  comment:: "Accept SNMP from internal sources."
  address:: SOME_HOST
  action:: accept
}
"""

GOOD_TERM_5 = """
term good-term-5 {
  comment:: "Accept ESP from internal sources."
  address:: SOME_HOST
  protocol:: esp
  action:: accept
}
"""

GOOD_TERM_6 = """
term good-term-6 {
  comment:: "Accept AH from internal sources."
  address:: SOME_HOST
  protocol:: ah
  action:: accept
}
"""

GOOD_TERM_7 = """
term good-term-6 {
  comment:: "Accept AH from internal sources."
  address:: SOME_HOST
  protocol:: ah esp tcp
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'address',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'dscp_match',
    'expiration',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'owner',
    'platform',
    'platform_exclude',
    'protocol',
    'restrict_address_family',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next',
               'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'tcp-established',
               'is-fragment',
               'fragments'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class AristaTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testRemark(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]

    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_4,
                             self.naming)
    acl = arista.Arista(pol, EXP_INFO)
    expected = 'remark this is a test standard acl'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = 'remark good-term-4'
    self.assertIn(expected, str(acl), str(acl))
    expected = 'test-filter remark'
    self.assertNotIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testExtendedEosSyntax(self):
    # Extended access-lists should not use the "extended" argument to ip
    # access-list.
    acl = arista.Arista(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    self.assertIn('ip access-list test-filter', str(acl))

  def testESPIsAnInteger(self):
    acl = arista.Arista(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_5, self.naming), EXP_INFO)
    self.assertIn('permit 50', str(acl))

  def testAHIsAnInteger(self):
    acl = arista.Arista(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_6, self.naming), EXP_INFO)
    self.assertIn('permit 51', str(acl))

  def testAHAndESPAreIntegers(self):
    acl = arista.Arista(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_7, self.naming), EXP_INFO)
    self.assertIn('permit 50', str(acl))
    self.assertIn('permit 51', str(acl))

  def testBuildTokens(self):
    pol1 = arista.Arista(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM,
                                            self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = arista.Arista(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1,
                                            self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testStandardTermHost(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.0/24')]
    self.naming.GetServiceByProto.return_value = ['22', '6537']

    pol = policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_2 + GOOD_TERM_3,
                             self.naming)
    acl = arista.Arista(pol, EXP_INFO)
    expected = 'ip access-list test-filter'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit tcp 10.1.1.0/24 any eq ssh'
    self.assertIn(expected, str(acl), str(acl))
    expected = ' permit tcp 10.1.1.0/24 any eq 6537'
    self.assertIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_has_calls([mock.call('SOME_HOST'),
                                             mock.call('SOME_HOST2')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('SSH', 'tcp'), mock.call('GOPENFLOW', 'tcp')])

  def testStandardTermHostV6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2620:1::/64')]
    self.naming.GetServiceByProto.return_value = ['22']

    pol = policy.ParsePolicy(GOOD_HEADER_IPV6 + GOOD_TERM_2, self.naming)
    acl = arista.Arista(pol, EXP_INFO)
    expected = 'ipv6 access-list test-filter'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit tcp 2620:1::/64 any eq ssh'
    self.assertIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_has_calls([mock.call('SOME_HOST')])
    self.naming.GetServiceByProto.assert_has_calls([mock.call('SSH', 'tcp')])

  def testStandardTermV4(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.0/24')]

    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_4, self.naming)
    acl = arista.Arista(pol, EXP_INFO)
    expected = 'ip access-list standard test-filter'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit 10.1.1.0/24\n'
    self.assertIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_has_calls([mock.call('SOME_HOST')])


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


# tests/lib/arista_tp_test.py

# Copyright 2021 Google Inc. All Rights Reserved.
#
# 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.
#

"""unittest for arista traffic-policy rendering module."""

import datetime
import re
from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import arista_tp
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: arista_tp test-filter
}
"""
# XXX check
GOOD_HEADER_INET = """
header {
  comment:: "test inet acl"
  target:: arista_tp test-filter inet
}
"""
GOOD_HEADER_INET6 = """
header {
  comment:: "this is a test acl"
  target:: arista_tp test-filter inet6
}
"""
GOOD_NOVERBOSE_MIXED_HEADER = """
header {
  target:: arista_tp test-filter mixed noverbose
}
"""
GOOD_NOVERBOSE_V4_HEADER = """
header {
  target:: arista_tp test-filter inet noverbose
}
"""
GOOD_NOVERBOSE_V6_HEADER = """
header {
  target:: arista_tp test-filter inet6 noverbose
}
"""

BAD_HEADER = """
header {
  target:: arista_tp test-filter bridged
}
"""
EXPIRED_TERM = """
term is_expired {
  expiration:: 2001-01-01
  action:: accept
}
"""
EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""
DUPLICATE_TERMS = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}

term good-term-1 {
  protocol:: tcp
  destination-port:: SMTP
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}
term good-term-2 {
  protocol:: tcp
  destination-port:: SMTP
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_1_V6 = """
term good-term-1 {
  protocol:: icmpv6
  action:: accept
}
term good-term-2 {
  protocol:: tcp
  destination-port:: SMTP
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_2 = """
term good-term-3 {
  protocol:: tcp
  destination-address:: SOME_HOST
  source-port:: HTTP
  option:: established tcp-established
  action:: accept
}
"""
GOOD_TERM_3 = """
term good-term-3 {
  protocol:: icmp
  icmp-type:: echo-reply information-reply information-request
  icmp-type:: router-solicitation timestamp-request
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  protocol:: icmp
  protocol:: tcp
  action:: accept
}
"""

PROTO_EXC_TCP = """
term good-term-7 {
  protocol-except:: tcp
  action:: accept
}
"""
PROTO_EXC_LIST = """
term good-term-7 {
  protocol-except:: igmp egp rdp hopopt
  action:: accept
}
"""

GOOD_TERM_8 = """
term good-term-8 {
  source-prefix:: foo_prefix_list
  destination-prefix:: bar_prefix_list baz_prefix_list
  action:: accept
}
"""
GOOD_TERM_11 = """
term good-term-11 {
  verbatim:: arista_tp "mary had a little lamb"
  verbatim:: iptables "mary had a second lamb"
  verbatim:: cisco "mary had a third lamb"
}
"""
GOOD_TERM_OWNER = """
term owner-term {
  protocol:: tcp
  owner:: foo@google.com
  action:: accept
}
"""
GOOD_TERM_18_SRC = """
term address-exclusions {
  source-address:: INTERNAL
  source-exclude:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_18_DST = """
term address-exclusions {
  destination-address:: INTERNAL
  destination-exclude:: SOME_HOST
  action:: accept
}
"""
# TODO(sulric): write a test for this term
GOOD_TERM_19 = """
term minimize-prefix-list {
  source-address:: INCLUDES
  source-exclude:: EXCLUDES
  action:: accept
}
"""
GOOD_TERM_V6_HOP_LIMIT = """
term good-term-v6-hl {
  hop-limit:: 25
  action:: accept
}
"""
GOOD_TERM_20_V6 = """
term good-term-20-v6 {
  protocol-except:: icmpv6
  action:: accept
}
"""
GOOD_TERM_21 = """
term good_term_21 {
  ttl:: 10
  action:: accept
}
"""
GOOD_TERM_28 = """
term good_term_28 {
  action:: accept
}
"""
GOOD_TERM_35 = """
term good_term_35 {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 3 4
  action:: accept
}
"""
GOOD_TERM_COMMENT = """
term good-term-comment {
  protocol:: udp
  comment:: "This is a COMMENT"
  action:: accept
}
"""
BAD_TERM_1 = """
term bad-term-1 {
  protocol:: tcp udp
  source-port:: DNS
  option:: tcp-established
  action:: accept
}
"""
ESTABLISHED_TERM_1 = """
term established-term-1 {
  protocol:: tcp
  source-port:: DNS
  option:: established
  action:: accept
}
"""
MISSING_MATCH = """
term missing-match {
  action:: accept
}
"""
OPTION_TERM_1 = """
term option-term {
  protocol:: tcp
  option:: is-fragment
  action:: accept
}
"""
BAD_ICMPTYPE_TERM_1 = """
term icmptype-mismatch {
  comment:: "error when icmpv6 paired with inet filter"
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""
BAD_ICMPTYPE_TERM_2 = """
term icmptype-mismatch {
  comment:: "error when icmp paired with inet6 filter"
  protocol:: icmp
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""
DEFAULT_TERM_1 = """
term default-term-1 {
  action:: deny
}
"""
LONG_COMMENT_TERM_1 = """
term long-comment-term-1 {
  comment:: "this is very very very very very very very very very very very
  comment:: "very very very very very very very long."
  action:: deny
}
"""

LONG_COMMENT_TERM_ANET = """
term long-comment-term-1 {
  comment:: "0 this is very very very very very very very very very very very"
  comment:: "1 very very very very very very very very very very very"
  comment:: "2 very very very very very very very very very very very"
  comment:: "3 very very very very very very very very very very very"
  comment:: "4 very very very very very very very long comment. "
  protocol:: icmpv6
  action:: deny
}
"""
HOPOPT_TERM = """
term good-term-1 {
  protocol:: hopopt
  action:: accept
}
"""
FRAGOFFSET_TERM = """
term good-term-1 {
  fragment-offset:: 1-7
  action:: accept
}
"""
COUNTER_CLEANUP_TERM = """
term good-term-1 {
  protocol:: tcp
  counter:: test.cleanup.check
  action:: accept
}
"""
# test the various mixed filter_type permutations
MIXED_INET = """
term MIXED_INET {
  source-address:: GOOGLE_DNS
  destination-address:: INTERNAL
  protocol:: tcp udp
  action:: accept
}
"""
INET_MIXED = """
term INET_MIXED {
  source-address:: INTERNAL
  destination-address:: GOOGLE_DNS
  protocol:: tcp udp
  action:: accept
}
"""
MIXED_INET6 = """
term MIXED_INET6 {
  source-address:: GOOGLE_DNS
  destination-address:: SOME_HOST
  action:: accept
}
"""

INET6_MIXED = """
term INET6_MIXED {
  source-address:: SOME_HOST
  destination-address:: GOOGLE_DNS
  action:: accept
}
"""

MIXED_MIXED = """
term MIXED_MIXED {
  source-address:: GOOGLE_DNS
  destination-address:: GOOGLE_DNS
  action:: accept
}
"""

MIXED_ANY = """
term MIXED_ANY {
  source-address:: GOOGLE_DNS
  action:: accept
}
"""

ANY_MIXED = """
term ANY_MIXED {
  destination-address:: GOOGLE_DNS
  action:: accept
}
"""

INET_INET = """
term INET_INET {
  source-address:: NTP_SERVERS
  destination-address:: INTERNAL
  action:: accept
}
"""

INET6_INET6 = """
term INET6_INET6 {
  source-address:: SOME_HOST
  destination-address:: SOME_HOST
  action:: accept
}
"""

INET_INET6 = """
term INET_INET6 {
  source-address:: INTERNAL
  destination-address:: SOME_HOST
  action:: accept
}
"""

INET6_INET = """
term INET6_INET {
  source-address:: SOME_HOST
  destination-address:: INTERNAL
  action:: accept
}
"""

SRC_FIELD_SET_INET = """
term FS_INET {
  source-address:: INTERNAL
  source-exclude:: SOME_HOST
  action:: accept
}
"""
SRC_FIELD_SET_INET6 = """
term FS_INET6 {
  source-address:: INTERNAL
  source-exclude:: SOME_HOST
  action:: accept
}
"""
SRC_FIELD_SET_MIXED = """
term FS_MIXED {
  source-address:: INTERNAL
  source-exclude:: SOME_HOST
  action:: accept
}
"""

DST_FIELD_SET_INET = """
term FS_INET {
  destination-address:: INTERNAL
  destination-exclude:: SOME_HOST
  action:: accept
}
"""
DST_FIELD_SET_INET6 = """
term FS_INET6 {
  destination-address:: INTERNAL
  destination-exclude:: SOME_HOST
  action:: accept
}
"""
DST_FIELD_SET_MIXED = """
term FS_MIXED {
  destination-address:: INTERNAL
  destination-exclude:: SOME_HOST
  action:: accept
}
"""

# this term should not have the logging element rendered
LOGGING_ACCEPT = """
term logging-term-1 {
  protocol:: icmp
  action:: accept
  logging:: true
}
"""
# this term _should_ have the logging element rendered
LOGGING_DENY = """
term logging-term-1 {
  protocol:: icmp
  action:: deny
  logging:: true
}
"""

SUPPORTED_TOKENS = frozenset([
    "action",
    "comment",
    "counter",
    "destination_address",
    "destination_address_exclude",
    "destination_port",
    "destination_prefix",
    "dscp_set",
    "expiration",
    "fragment_offset",
    "hop_limit",
    "icmp_code",
    "icmp_type",
    "logging",
    "name",
    "option",
    "owner",
    "packet_length",
    "platform",
    "platform_exclude",
    "port",
    "protocol",
    "protocol_except",
    "source_address",
    "source_address_exclude",
    "source_port",
    "source_prefix",
    "stateless_reply",
    "translated",
    "ttl",
    "verbatim",
])

SUPPORTED_SUB_TOKENS = {
    "action": {"accept", "deny", "reject", "next", "reject-with-tcp-rst"},
    "icmp_type": {
        "alternate-address",
        "certification-path-advertisement",
        "certification-path-solicitation",
        "conversion-error",
        "destination-unreachable",
        "echo-reply",
        "echo-request",
        "mobile-redirect",
        "home-agent-address-discovery-reply",
        "home-agent-address-discovery-request",
        "icmp-node-information-query",
        "icmp-node-information-response",
        "information-request",
        "inverse-neighbor-discovery-advertisement",
        "inverse-neighbor-discovery-solicitation",
        "mask-reply",
        "mask-request",
        "information-reply",
        "mobile-prefix-advertisement",
        "mobile-prefix-solicitation",
        "multicast-listener-done",
        "multicast-listener-query",
        "multicast-listener-report",
        "multicast-router-advertisement",
        "multicast-router-solicitation",
        "multicast-router-termination",
        "neighbor-advertisement",
        "neighbor-solicit",
        "packet-too-big",
        "parameter-problem",
        "redirect",
        "redirect-message",
        "router-advertisement",
        "router-renumbering",
        "router-solicit",
        "router-solicitation",
        "source-quench",
        "time-exceeded",
        "timestamp-reply",
        "timestamp-request",
        "unreachable",
        "version-2-multicast-listener-report",
    },
    "option": {
        "established",
        "is-fragment",
        ".*",  # not actually a lex token!
        "tcp-established",
        "tcp-initial",
    },
}

# print an info message when a term is set to expire in that many weeks.
# normally passed from command line.
EXP_INFO = 2


class AristaTpTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testOptions(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP("10.0.0.0/8")]
    self.naming.GetServiceByProto.return_value = ["80"]

    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_2, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("destination port 1024-65535", output, output)
    # verify that tcp-established; doesn't get duplicated if both
    # 'established' and 'tcp-established' options are included in term
    self.assertEqual(output.count("established"), 1)

    self.naming.GetNetAddr.assert_called_once_with("SOME_HOST")
    self.naming.GetServiceByProto.assert_called_once_with("HTTP", "tcp")

  def testTermAndFilterName(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP("10.0.0.0/8")]
    self.naming.GetServiceByProto.return_value = ["25"]

    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("match good-term-1", output, output)
    self.assertIn("traffic-policy test-filter", output, output)

    self.naming.GetNetAddr.assert_called_once_with("SOME_HOST")
    self.naming.GetServiceByProto.assert_called_once_with("SMTP", "tcp")

  def testBadFilterType(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP("10.0.0.0/8")]
    self.naming.GetServiceByProto.return_value = ["25"]

    pol = policy.ParsePolicy(BAD_HEADER + GOOD_TERM_1, self.naming)
    self.assertRaises(
        aclgenerator.UnsupportedAFError,
        arista_tp.AristaTrafficPolicy,
        pol,
        EXP_INFO,
    )
    self.naming.GetNetAddr.assert_called_once_with("SOME_HOST")
    self.naming.GetServiceByProto.assert_called_once_with("SMTP", "tcp")

  def testDuplicateTermName(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP("10.0.0.0/8")]
    self.naming.GetServiceByProto.return_value = ["25"]

    pol = policy.ParsePolicy(GOOD_HEADER + DUPLICATE_TERMS, self.naming)
    self.assertRaises(
        aclgenerator.DuplicateTermError,
        arista_tp.AristaTrafficPolicy,
        pol,
        EXP_INFO,
    )
    self.naming.GetNetAddr.assert_called_once_with("SOME_HOST")
    self.naming.GetServiceByProto.assert_called_once_with("SMTP", "tcp")

  def testCounterCleanup(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + COUNTER_CLEANUP_TERM, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertIn("counter test-cleanup-check", output, output)
    self.assertIn("count test-cleanup-check", output, output)

  def testDefaultDeny(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + DEFAULT_TERM_1, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("match ipv4-default-all ipv4", output, output)
    self.assertIn("match ipv6-default-all ipv6", output, output)

  def testIcmpType(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_3, self.naming), EXP_INFO)
    output = str(atp)
    # verify proper translation from policy icmp-type text to
    # traffic-policy
    self.assertIn("icmp type ", output, output)
    self.assertIn("0,", output, output)
    self.assertIn("10,", output, output)
    self.assertIn("13,", output, output)
    self.assertIn("15,", output, output)
    self.assertIn("16", output, output)

  def testIcmpCode(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_35, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("code 3,4", output, output)

  def testInet6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP("2001::/33")]
    self.naming.GetServiceByProto.return_value = ["25"]

    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM_1_V6, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertTrue("protocol icmpv6" in output and "protocol tcp" in output,
                    output)

    self.naming.GetNetAddr.assert_called_once_with("SOME_HOST")
    self.naming.GetServiceByProto.assert_called_once_with("SMTP", "tcp")

  def testHopLimit(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM_V6_HOP_LIMIT,
                           self.naming),
        EXP_INFO,
    )
    output = str(atp)
    self.assertIn("ttl 25", output, output)

  def testProtocol(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_5, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("protocol icmp tcp", output, output)

  def testProtocolExceptTcp(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + PROTO_EXC_TCP, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("protocol 1-5,7-255", output, output)
    self.assertIn("protocol 0-5,7-255", output, output)

  def testProtocolExceptList(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + PROTO_EXC_LIST, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("protocol 1,3-7,9-26,28-255", output, output)

  def testPrefixList(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_8, self.naming), EXP_INFO)
    spfx_re = re.compile(r"source prefix field-set\W+foo_prefix_list\W+")
    dpfx_re = re.compile(
        r"destination prefix field-set\W+bar_prefix_list\W+baz_prefix_list\W+")
    output = str(atp)
    self.assertTrue(spfx_re.search(output), output)
    self.assertTrue(dpfx_re.search(output), output)

  def testVerbatimTerm(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_11, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("mary had a little lamb", output, output)
    # check if other platforms verbatim shows up in output
    self.assertNotIn("mary had a second lamb", output, output)
    self.assertNotIn("mary had a third lamb", output, output)

  def testTcpEstablished(self):
    self.naming.GetServiceByProto.return_value = ["53"]

    policy_text = GOOD_HEADER + ESTABLISHED_TERM_1
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("established", output, output)

    self.naming.GetServiceByProto.assert_called_once_with("DNS", "tcp")

  def testNonTcpWithTcpEstablished(self):
    self.naming.GetServiceByProto.return_value = ["53"]

    policy_text = GOOD_HEADER + BAD_TERM_1
    pol_obj = policy.ParsePolicy(policy_text, self.naming)
    atp = arista_tp.AristaTrafficPolicy(pol_obj, EXP_INFO)
    self.assertRaises(arista_tp.TcpEstablishedWithNonTcpError, str, atp)

    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call("DNS", "tcp"),
         mock.call("DNS", "udp")])

  def testNoVerboseMixed(self):
    addr_list = list()
    for octet in range(0, 256):
      net = nacaddr.IP("192.168." + str(octet) + ".64/27")
      addr_list.append(net)
    self.naming.GetNetAddr.return_value = addr_list
    self.naming.GetServiceByProto.return_value = ["25"]

    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(
            GOOD_NOVERBOSE_MIXED_HEADER + GOOD_TERM_1 + GOOD_TERM_COMMENT,
            self.naming), EXP_INFO)
    self.assertIn("192.168.0.64/27", str(atp))
    self.assertNotIn("COMMENT", str(atp))
    self.naming.GetNetAddr.assert_called_once_with("SOME_HOST")
    self.naming.GetServiceByProto.assert_called_once_with("SMTP", "tcp")

  def testNoVerboseV4(self):
    addr_list = list()
    for octet in range(0, 256):
      net = nacaddr.IP("192.168." + str(octet) + ".64/27")
      addr_list.append(net)
    self.naming.GetNetAddr.return_value = addr_list
    self.naming.GetServiceByProto.return_value = ["25"]

    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(
            GOOD_NOVERBOSE_V4_HEADER + GOOD_TERM_1 + GOOD_TERM_COMMENT,
            self.naming), EXP_INFO)
    self.assertIn("192.168.0.64/27", str(atp))
    self.assertNotIn("COMMENT", str(atp))
    self.naming.GetNetAddr.assert_called_once_with("SOME_HOST")
    self.naming.GetServiceByProto.assert_called_once_with("SMTP", "tcp")

  def testNoVerboseV6(self):
    addr_list = list()
    for octet in range(0, 256):
      net = nacaddr.IPv6(
          "2001:db8:1010:" + str(octet) + "::64/64", strict=False)
      addr_list.append(net)
    self.naming.GetNetAddr.return_value = addr_list
    self.naming.GetServiceByProto.return_value = ["25"]

    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(
            GOOD_NOVERBOSE_V6_HEADER + GOOD_TERM_1 + GOOD_TERM_COMMENT,
            self.naming), EXP_INFO)
    self.assertIn("2001:db8:1010:90::/61", str(atp))
    self.assertNotIn("COMMENT", str(atp))
    self.naming.GetNetAddr.assert_called_once_with("SOME_HOST")
    self.naming.GetServiceByProto.assert_called_once_with("SMTP", "tcp")

  def testTermTypeIndexKeys(self):
    # ensure an _INET entry for each _TERM_TYPE entry
    self.assertCountEqual(
        arista_tp.Term._TERM_TYPE.keys(),
        arista_tp.Term.AF_MAP.keys())

  def testCommentReflow(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + LONG_COMMENT_TERM_ANET, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertIn("!! 0 this is v", output, output)
    self.assertIn("!! very", output, output)
    self.assertIn("!! 1 very very", output, output)
    self.assertIn("!! 2 very very", output, output)
    self.assertIn("!! 3 very very", output, output)
    self.assertIn("!! 4 very very", output, output)

  @mock.patch.object(arista_tp.logging, "warning")
  def testArbitraryOptions(self, mock_warn):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + OPTION_TERM_1, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("fragment", output, output)
    # since this is a mixed term, check to make sure that the fragment term
    # isn't rendered for inet6
    mock_warn.assert_any_call(
        "WARNING: term %s in mixed policy %s uses fragment "
        "the ipv6 version of the term will not be rendered.",
        "ipv6-option-term", "test-filter")

  @mock.patch.object(arista_tp.logging, "warning")
  def testLoggingOptionFail(self, mock_warn):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + LOGGING_ACCEPT, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("match logging-term-1", output)
    self.assertNotIn(" log\n", output)  # check for bare 'log' word
    mock_warn.assert_any_call(
        "WARNING: term %s uses logging option but is not a deny "
        "action. logging will not be added.",
        "logging-term-1",
    )

  def testLoggingOption(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + LOGGING_DENY, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn(" log\n", output)

  @mock.patch.object(arista_tp.logging, "debug")
  def testIcmpv6InetMismatch(self, mock_debug):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + BAD_ICMPTYPE_TERM_1, self.naming),
        EXP_INFO)
    str(atp)

    mock_debug.assert_called_once_with(
        "Term icmptype-mismatch will not be rendered, "
        "as it has icmpv6 match specified but "
        "the ACL is of inet address family.")

  @mock.patch.object(arista_tp.logging, "debug")
  def testIcmpInet6Mismatch(self, mock_debug):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER_INET6 + BAD_ICMPTYPE_TERM_2,
                           self.naming),
        EXP_INFO,
    )
    str(atp)

    mock_debug.assert_called_once_with(
        "Term icmptype-mismatch will not be rendered, "
        "as it has icmp match specified but "
        "the ACL is of inet6 address family.")

  # icmptype-mismatch test for mixed filter type
  @mock.patch.object(arista_tp.logging, "debug")
  def testIcmpMismatchMixedInet(self, mock_debug):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + BAD_ICMPTYPE_TERM_1, self.naming),
        EXP_INFO,
    )
    str(atp)

    mock_debug.assert_called_once_with(
        "Term icmptype-mismatch will not be rendered, "
        "as it has icmpv6 match specified but "
        "the ACL is of inet address family.")

  @mock.patch.object(arista_tp.logging, "debug")
  def testIcmpMismatchMixedInet6(self, mock_debug):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + BAD_ICMPTYPE_TERM_2, self.naming),
        EXP_INFO,
    )
    str(atp)

    mock_debug.assert_called_once_with(
        "Term ipv6-icmptype-mismatch will not be rendered, "
        "as it has icmp match specified but "
        "the ACL is of inet6 address family.")

  @mock.patch.object(arista_tp.logging, "warning")
  def testExpiredTerm(self, mock_warn):
    _ = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + EXPIRED_TERM, self.naming), EXP_INFO)

    mock_warn.assert_any_call(
        "WARNING: term %s in policy %s is expired and will "
        "not be rendered.",
        "is_expired",
        "test-filter",
    )
    mock_warn.assert_any_call(
        "WARNING: term %s in policy %s is expired and will "
        "not be rendered.",
        "ipv6-is_expired",
        "test-filter",
    )

  @mock.patch.object(arista_tp.logging, "info")
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    _ = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(
            GOOD_HEADER + EXPIRING_TERM % exp_date.strftime("%Y-%m-%d"),
            self.naming),
        EXP_INFO,
    )
    mock_info.assert_any_call(
        "INFO: term %s in policy %s expires in "
        "less than two weeks.", "is_expiring", "test-filter")

  def testOwnerTerm(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_OWNER, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertIn("!! owner: foo@google.com", output, output)

  # confirm that we don't generate a term for non-default
  @mock.patch.object(arista_tp.logging, "warning")
  def testMissingMatchCriteria(self, mock_warn):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + MISSING_MATCH, self.naming), EXP_INFO)
    output = str(atp)
    self.assertNotIn("match", output, output)
    mock_warn.has_calls(
        "WARNING: term %s has no valid match criteria and "
        "will not be rendered.",
        "missing-match",
    )

  def testAddressExclude(self):
    big = nacaddr.IPv4("0.0.0.0/1")
    ip1 = nacaddr.IPv4("10.0.0.0/8")
    ip2 = nacaddr.IPv4("172.16.0.0/12")
    terms = (GOOD_TERM_18_SRC, GOOD_TERM_18_DST)
    self.naming.GetNetAddr.side_effect = [[big, ip1, ip2], [ip1]] * len(terms)

    mock_calls = []
    for term in terms:
      atp = arista_tp.AristaTrafficPolicy(
          policy.ParsePolicy(GOOD_HEADER + term, self.naming), EXP_INFO)
      output = str(atp)
      self.assertIn("except 10.0.0.0/8", output, output)
      # note that the additional spaces are in the following assert to insure
      # that it's not being rendered w/o the "except"
      self.assertNotIn("  10.0.0.0/8", output, output)
      self.assertIn("172.16.0.0/12", output, output)
      self.assertNotIn("except 172.16.0.0/12", output, output)
      mock_calls.append(mock.call("INTERNAL"))
      mock_calls.append(mock.call("SOME_HOST"))

    self.naming.GetNetAddr.assert_has_calls(mock_calls)

  def testMixedInet(self):
    self.naming.GetNetAddr.side_effect = [[
        nacaddr.IP("8.8.4.4"),
        nacaddr.IP("8.8.8.8"),
        nacaddr.IP("2001:4860:4860::8844"),
        nacaddr.IP("2001:4860:4860::8888")
    ],
                                          [
                                              nacaddr.IP("10.0.0.0/8"),
                                              nacaddr.IP("172.16.0.0/12"),
                                              nacaddr.IP("192.168.0.0/16")
                                          ]]

    pol = policy.ParsePolicy(GOOD_HEADER + MIXED_INET, self.naming)
    atp = arista_tp.AristaTrafficPolicy(pol, EXP_INFO)
    output = str(atp)
    self.assertIn("match MIXED_INET ipv4", output, output)
    self.assertIn("source prefix 8.8.4.4/32", output, output)
    self.assertIn("destination prefix 10.0.0.0/8", output, output)
    self.assertNotIn("match ipv6-MIXED_INET ipv6", output, output)
    self.assertNotIn("source prefix 2001:4860:4860::8844/128", output, output)

  def testInetMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [
            nacaddr.IP("10.0.0.0/8"),
            nacaddr.IP("172.16.0.0/12"),
            nacaddr.IP("192.168.0.0/16")
        ],
        [
            nacaddr.IP("8.8.4.4"),
            nacaddr.IP("8.8.8.8"),
            nacaddr.IP("2001:4860:4860::8844"),
            nacaddr.IP("2001:4860:4860::8888")
        ],
    ]

    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + INET_MIXED, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("match INET_MIXED ipv4", output, output)
    self.assertIn("source prefix 10.0.0.0/8", output, output)
    self.assertIn("destination prefix 8.8.4.4/32", output, output)
    self.assertNotIn("match ipv6-INET_MIXED ipv6", output, output)
    self.assertNotIn("destination prefix 2001:4860:4860::8844/128", output,
                     output)

  def testMixedInet6(self):
    self.naming.GetNetAddr.side_effect = [[
        nacaddr.IP("8.8.4.4"),
        nacaddr.IP("8.8.8.8"),
        nacaddr.IP("2001:4860:4860::8844"),
        nacaddr.IP("2001:4860:4860::8888")
    ], [nacaddr.IP("2001:4860:4860::8844")]]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + MIXED_INET6, self.naming), EXP_INFO)
    output = str(atp)
    # note that the term name will contain the 'ipv6-' prefix
    self.assertIn("match ipv6-MIXED_INET6 ipv6", output, output)
    self.assertIn("source prefix 2001:4860:4860::8844/128", output, output)
    self.assertIn("destination prefix 2001:4860:4860::8844/128", output, output)
    # check to make sure that the IPv4 elements are not rendered
    self.assertNotIn("match MIXED_INET6 ipv4", output, output)
    self.assertNotIn("source prefix 8.8.8.8", output, output)

  def testInet6Mixed(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP("2001:4860:4860::8844")],
        [
            nacaddr.IP("8.8.4.4"),
            nacaddr.IP("8.8.8.8"),
            nacaddr.IP("2001:4860:4860::8844"),
            nacaddr.IP("2001:4860:4860::8888")
        ]
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + INET6_MIXED, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("match ipv6-INET6_MIXED ipv6", output, output)
    self.assertIn("source prefix 2001:4860:4860::8844/128", output, output)
    self.assertIn("destination prefix 2001:4860:4860::8844/128", output, output)
    self.assertNotIn("match INET6_MIXED ipv4", output, output)
    self.assertNotIn("destination prefix 8.8.8.8", output, output)

  def testMixedMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [
            nacaddr.IP("8.8.4.4"),
            nacaddr.IP("8.8.8.8"),
            nacaddr.IP("2001:4860:4860::8844"),
            nacaddr.IP("2001:4860:4860::8888")
        ],
        [
            nacaddr.IP("4.4.2.2"),
            nacaddr.IP("4.4.4.4"),
            nacaddr.IP("2001:4860:1337::8844"),
            nacaddr.IP("2001:4860:1337::8888")
        ]
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + MIXED_MIXED, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("match MIXED_MIXED ipv4", output, output)
    self.assertIn("source prefix 8.8.4.4/32", output, output)
    self.assertIn("destination prefix 4.4.2.2", output, output)

    self.assertIn("match ipv6-MIXED_MIXED ipv6", output, output)
    self.assertIn("source prefix 2001:4860:4860::8844/128", output, output)
    self.assertIn("destination prefix 2001:4860:1337::8844/128", output, output)

  def testMixedAny(self):
    self.naming.GetNetAddr.side_effect = [[
        nacaddr.IP("8.8.4.4"),
        nacaddr.IP("8.8.8.8"),
        nacaddr.IP("2001:4860:4860::8844"),
        nacaddr.IP("2001:4860:4860::8888")
    ]]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + MIXED_ANY, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("match MIXED_ANY ipv4", output, output)
    self.assertIn("source prefix 8.8.4.4/32", output, output)

    self.assertIn("match ipv6-MIXED_ANY ipv6", output, output)
    self.assertIn("source prefix 2001:4860:4860::8844/128", output, output)

  def testAnyMixed(self):
    self.naming.GetNetAddr.side_effect = [[
        nacaddr.IP("8.8.4.4"),
        nacaddr.IP("8.8.8.8"),
        nacaddr.IP("2001:4860:4860::8844"),
        nacaddr.IP("2001:4860:4860::8888")
    ]]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + ANY_MIXED, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("match ANY_MIXED ipv4", output, output)
    self.assertIn("destination prefix 8.8.4.4/32", output, output)

    self.assertIn("match ipv6-ANY_MIXED ipv6", output, output)
    self.assertIn("destination prefix 2001:4860:4860::8844/128", output, output)

  def testInetInet(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP("8.8.4.4"), nacaddr.IP("8.8.8.8")],
        [nacaddr.IP("4.4.2.2"), nacaddr.IP("4.4.4.4")],
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + INET_INET, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("match INET_INET ipv4", output, output)
    self.assertIn("source prefix 8.8.4.4/32", output, output)
    self.assertIn("destination prefix 4.4.2.2/32", output, output)

  def testInet6Inet6(self):
    self.naming.GetNetAddr.side_effect = [[
        nacaddr.IP("2001:4860:4860::8844"),
        nacaddr.IP("2001:4860:4860::8888")
    ], [nacaddr.IP("2001:4860:1337::8844"),
        nacaddr.IP("2001:4860:1337::8888")]]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + INET6_INET6, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("match ipv6-INET6_INET6 ipv6", output, output)
    self.assertIn("source prefix 2001:4860:4860::8844/128", output, output)
    self.assertIn("destination prefix 2001:4860:1337::8844/128", output, output)

  def testInetInet6(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP("8.8.4.4"), nacaddr.IP("8.8.8.8")],
        [
            nacaddr.IP("2001:4860:4860::8844"),
            nacaddr.IP("2001:4860:4860::8888")
        ],
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + INET_INET6, self.naming), EXP_INFO)
    output = str(atp)
    # we should not generate this term
    # TODO(sulrich): we should, however, throw a warning
    self.assertNotIn("match INET_INET6 ipv4", output, output)
    self.assertNotIn("match ipv6-INET_INET6 ipv6", output, output)

  def testInet6Inet(self):
    self.naming.GetNetAddr.side_effect = [
        [
            nacaddr.IP("2001:4860:4860::8844"),
            nacaddr.IP("2001:4860:4860::8888")
        ],
        [nacaddr.IP("8.8.4.4"), nacaddr.IP("8.8.8.8")],
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + INET6_INET, self.naming), EXP_INFO)
    output = str(atp)
    self.assertNotIn("match INET6_INET ipv4", output, output)
    self.assertNotIn("match ipv6-INET6_INET ipv6", output, output)

  def testSrcFsInet(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP("8.8.4.0/24"),
         nacaddr.IP("8.8.8.0/24")],
        [nacaddr.IP("8.8.4.4"), nacaddr.IP("8.8.8.8")],
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + SRC_FIELD_SET_INET, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertIn("field-set ipv4 prefix src-FS_INET", output, output)
    self.assertIn("source prefix field-set src-FS_INET", output, output)

  def testSrcFsInet6(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP("2001:4860:4860::/64"),
         nacaddr.IP("2001:4860:4861::/64")],
        [
            nacaddr.IP("2001:4860:4860::8844"),
            nacaddr.IP("2001:4860:4861::8888")
        ],
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + SRC_FIELD_SET_INET6, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertIn("field-set ipv6 prefix src-ipv6-FS_INET6", output, output)
    self.assertIn("source prefix field-set src-ipv6-FS_INET6", output, output)

  def testSrcFsMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [
            nacaddr.IP("8.8.4.0/24"),
            nacaddr.IP("8.8.8.0/24"),
            nacaddr.IP("2001:4860:4860::/64"),
            nacaddr.IP("2001:4860:4860::/64"),
            nacaddr.IP("2001:4860:4861::/64")
        ],
        [
            nacaddr.IP("2001:4860:4860::8844"),
            nacaddr.IP("2001:4860:4861::8888"),
            nacaddr.IP("8.8.4.4"),
            nacaddr.IP("8.8.8.8"),
        ],
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + SRC_FIELD_SET_MIXED, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertIn("field-set ipv4 prefix src-FS_MIXED", output, output)
    self.assertIn("field-set ipv6 prefix src-ipv6-FS_MIXED", output, output)
    self.assertIn("source prefix field-set src-FS_MIXED", output, output)
    self.assertIn("source prefix field-set src-ipv6-FS_MIXED", output, output)

  def testDstFsInet(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP("8.8.4.0/24"),
         nacaddr.IP("8.8.8.0/24")],
        [nacaddr.IP("8.8.4.4"), nacaddr.IP("8.8.8.8")],
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + DST_FIELD_SET_INET, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertIn("field-set ipv4 prefix dst-FS_INET", output, output)
    self.assertIn("destination prefix field-set dst-FS_INET", output, output)

  def testDstFsInet6(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP("2001:4860:4860::/64"),
         nacaddr.IP("2001:4860:4861::/64")],
        [
            nacaddr.IP("2001:4860:4860::8844"),
            nacaddr.IP("2001:4860:4861::8888")
        ],
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + DST_FIELD_SET_INET6, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertIn("field-set ipv6 prefix dst-ipv6-FS_INET6", output, output)
    self.assertIn("destination prefix field-set dst-ipv6-FS_INET6", output,
                  output)

  def testDstFsMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [
            nacaddr.IP("8.8.4.0/24"),
            nacaddr.IP("8.8.8.0/24"),
            nacaddr.IP("2001:4860:4860::/64"),
            nacaddr.IP("2001:4860:4860::/64"),
            nacaddr.IP("2001:4860:4861::/64")
        ],
        [
            nacaddr.IP("2001:4860:4860::8844"),
            nacaddr.IP("2001:4860:4861::8888"),
            nacaddr.IP("8.8.4.4"),
            nacaddr.IP("8.8.8.8"),
        ],
    ]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + DST_FIELD_SET_MIXED, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertIn("field-set ipv4 prefix dst-FS_MIXED", output, output)
    self.assertIn("field-set ipv6 prefix dst-ipv6-FS_MIXED", output, output)
    self.assertIn("destination prefix field-set dst-FS_MIXED", output, output)
    self.assertIn("destination prefix field-set dst-ipv6-FS_MIXED", output,
                  output)

  def testConfigHelper(self):
    match_indent = " " * 6
    config = arista_tp.Config()
    config.Append(match_indent, "test")
    config.Append(match_indent, "blah")
    config.Append(match_indent, "foo")
    config.Append(match_indent, "bar")
    config.Append(match_indent, "Mr. T Pities the fool!", verbatim=True)
    self.assertMultiLineEqual(
        str(config), "      test\n"
        "      blah\n"
        "      foo\n"
        "      bar\n"
        "Mr. T Pities the fool!")

  def testFragmentOffset(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + FRAGOFFSET_TERM, self.naming),
        EXP_INFO)
    output = str(atp)
    self.assertIn("fragment offset 1-7", output, output)

  def testTTL(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_21, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("ttl 10", output)

  def testBuildTokens(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IP("10.1.1.1/26", strict=False)
    ]

    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_28, self.naming), EXP_INFO)
    st, sst = atp._BuildTokens()
    # print(ppr.pprint(st))
    # print(ppr.pprint(SUPPORTED_TOKENS))
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_28, self.naming), EXP_INFO)
    st, sst = atp._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testHopOptProtocol(self):
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + HOPOPT_TERM, self.naming), EXP_INFO)
    output = str(atp)
    self.assertIn("protocol 0", output, output)

  def testFailIsFragmentInV6(self):
    self.naming.GetServiceByProto.return_value = ["22"]
    pol = policy.ParsePolicy(GOOD_HEADER_INET6 + OPTION_TERM_1, self.naming)

    self.assertRaises(
        arista_tp.AristaTpFragmentInV6Error,
        arista_tp.AristaTrafficPolicy,
        pol,
        EXP_INFO,
    )

  @mock.patch.object(arista_tp.logging, "warning")
  def testFailIsFragmentInMixed(self, mock_warn):
    self.naming.GetServiceByProto.return_value = ["22"]
    atp = arista_tp.AristaTrafficPolicy(
        policy.ParsePolicy(GOOD_HEADER + OPTION_TERM_1, self.naming), EXP_INFO)
    output = str(atp)
    self.assertNotIn("match ipv6-option-term ipv6", output, output)
    mock_warn.assert_any_call(
        "WARNING: term %s in mixed policy %s uses fragment "
        "the ipv6 version of the term will not be rendered.",
        "ipv6-option-term", "test-filter")


if __name__ == "__main__":
  absltest.main()


# tests/lib/aruba_test.py

# Copyright 2017 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for Aruba acl rendering module."""

import datetime
import logging
import textwrap
from absl.testing import absltest
from unittest import mock

from capirca.lib import aruba
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER_V4 = """
header {
  target:: aruba test-filter
}
"""

GOOD_HEADER_V6 = """
header {
  target:: aruba test-filter inet6
}
"""

GOOD_HEADER_NOVERBOSE = """
header {
  target:: aruba test-filter noverbose
}
"""

EXPIRED_TERM = """
term is-expired {
  expiration:: 2010-01-01
  action:: accept
}
"""

EXPIRING_TERM = """
term is-expiring {
  expiration:: %s
  action:: accept
}
"""

GOOD_TERM_SIMPLE = """
term good-term-simple {
  action:: accept
}
"""

GOOD_TERM_SHORT_COMMENT = """
term good-term-short-comment {
  comment:: "Some short comment."
  owner:: someowner
  action:: deny
}
"""

GOOD_TERM_LONG_COMMENT = """
term good-term-long-comment {
  comment:: "Two households, both alike in dignity,"
  comment:: "In fair Verona, where we lay our scene,"
  comment:: "From ancient grudge break to new mutiny, Where civil blood makes civil hands unclean."
  owner:: wshakespeare
  action:: accept
}
"""

GOOD_TERM_VERBATIM = """
term much-verbatim {
  verbatim:: aruba "aruba uses some odd ACL format"
  verbatim:: aruba "which is kinda like, weird"
  verbatim:: aruba ""
  verbatim:: cisco "But Cisco's format is Ok, tho."
  verbatim:: juniper "And Juniper's is the best!"
}
"""

GOOD_TERM_ALLOW_ANY_ANY = """
term good-term-allow-any-any {
  action:: accept
}
"""

GOOD_TERM_DENY_ANY_ANY = """
term good-term-deny-any-any {
  action:: deny
}
"""

GOOD_TERM_SINGLE_NETDESTINATION = """
term gt-one-netd {
  source-address:: SINGLE_HOST
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_TWO_NETDESTINATIONS = """
term gt-two-netd {
  source-address:: SINGLE_HOST
  destination-address:: SINGLE_HOST
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_TWO_NETWORK_NETDESTINATIONS = """
term gt-mix-netd {
  source-address:: SOME_NETWORK
  destination-address:: SOME_NETWORK
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_COMBINED_NETDESTINATIONS = """
term good-term-combined-netdestinations {
  source-address:: MIXED_HOSTS
  protocol:: tcp
  destination-port:: HTTP
  action:: deny
}
"""

GOOD_TERMS_COMBINED_SINGLE_CASE = """
term good-terms-combined-1 {
  source-address:: SOME_HOST
  destination-address:: SOME_HOST
  protocol:: udp
  destination-port:: TFTP
  action:: accept
}

term good-terms-combined-2 {
  action:: deny
}
"""

GOOD_TERM_SOURCE_IS_USER = """
term good-term-source-is-user {
  destination-address:: SOME_NETWORK
  protocol:: tcp
  destination-port:: DNS
  action:: accept
  option:: source-is-user
}
"""

GOOD_TERM_DESTINATION_IS_USER = """
term good-term-destination-is-user {
  source-address:: SOME_NETWORK
  protocol:: tcp
  destination-port:: DNS
  action:: accept
  option:: destination-is-user
}
"""

GOOD_TERM_NEGATE_1 = """
term good-term-negate {
  source-address:: SOME_NETWORK
  action:: deny
  option:: negate
}
"""

GOOD_TERM_NEGATE_2 = """
term good-term-negate {
  action:: accept
  option:: negate
}
"""

GOOD_TERM_PROTOCOL_MAP = """
term allow-icmp {
  protocol:: icmp
  action:: accept
}

term allow-gre {
  protocol:: gre
  action:: accept
}

term allow-esp {
  protocol:: esp
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_port',
    'expiration',
    'stateless_reply',
    'name',
    'option',
    'protocol',
    'source_address',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {
        'accept',
        'deny',
    },
    'option': {
        'source-is-user',
        'destination-is-user',
        'negate',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class ArubaTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testBuildTokens(self):
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_SIMPLE,
                                         self.naming), EXP_INFO)
    st, sst = aru._BuildTokens()
    self.assertEqual(SUPPORTED_TOKENS, st)
    self.assertEqual(SUPPORTED_SUB_TOKENS, sst)

  @mock.patch.object(logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 + EXPIRED_TERM,
                                   self.naming), EXP_INFO)
    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and will not '
        'be rendered.', 'is-expired', 'test-filter')

  @mock.patch.object(logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 + EXPIRING_TERM %
                                   exp_date.strftime('%Y-%m-%d'),
                                   self.naming), EXP_INFO)
    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is-expiring', 'test-filter')

  def testSimpleTerm(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      any any any permit
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_SIMPLE,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testShortComment(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      # Some short comment.
      # Owner: someowner
      any any any deny
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_SHORT_COMMENT,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testNoVerbose(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      any any any deny
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_NOVERBOSE +
                                         GOOD_TERM_SHORT_COMMENT,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testLongWrappedComment(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      # Two households, both alike in dignity,
      # In fair Verona, where we lay our scene,
      # From ancient grudge break to new mutiny, Where civil blood makes civil
      # hands unclean.
      # Owner: wshakespeare
      any any any permit
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_LONG_COMMENT,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testVerbatim(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      aruba uses some odd ACL format
      which is kinda like, weird
      any any any permit
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_VERBATIM +
                                         GOOD_TERM_ALLOW_ANY_ANY,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testMultipleCallsSingleOwnerLine(self):
    expected_result = textwrap.dedent("""\
        # $Id:$
        # $Date:$
        # $Revision:$
        ip access-list session test-filter
          # Two households, both alike in dignity,
          # In fair Verona, where we lay our scene,
          # From ancient grudge break to new mutiny, Where civil blood makes civil
          # hands unclean.
          # Owner: wshakespeare
          any any any permit
        !
        """)
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_LONG_COMMENT,
                                         self.naming), EXP_INFO)
    self.assertEqual(expected_result, str(aru))
    self.assertEqual(expected_result, str(aru))

  def testTermAllowAnyAnyIPv4(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      any any any permit
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_ALLOW_ANY_ANY,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testTermAllowAnyAnyIPv6(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      ipv6 any any any permit
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V6 +
                                         GOOD_TERM_ALLOW_ANY_ANY,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testTermDenyAnyAnyIPv4(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      any any any deny
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_DENY_ANY_ANY,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testTermDenyAnyAnyIPv6(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      ipv6 any any any deny
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V6 +
                                         GOOD_TERM_DENY_ANY_ANY,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testMultipleCallsSingleNetdestinationsBlock(self):
    expected_result = textwrap.dedent("""\
        # $Id:$
        # $Date:$
        # $Revision:$
        netdestination gt-one-netd_src
          host 10.1.1.1
        !

        ip access-list session test-filter
          alias gt-one-netd_src any 1 permit
        !
        """)
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_SINGLE_NETDESTINATION,
                                         self.naming), EXP_INFO)
    self.assertEqual(expected_result, str(aru))
    self.assertEqual(expected_result, str(aru))

  def testSingleNetdestinationIPv4(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination gt-one-netd_src
      host 10.1.1.1
    !

    ip access-list session test-filter
      alias gt-one-netd_src any 1 permit
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_SINGLE_NETDESTINATION,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testSingleNetdestinationIPv6(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination6 gt-one-netd_src
      host 2001::
    !

    ip access-list session test-filter
      ipv6 alias gt-one-netd_src any 1 permit
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001::/128')]
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V6 +
                                         GOOD_TERM_SINGLE_NETDESTINATION,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testTwoNetdestinationsIPv4(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination gt-two-netd_src
      host 10.1.1.1
    !

    netdestination gt-two-netd_dst
      host 10.1.1.1
    !

    ip access-list session test-filter
      alias gt-two-netd_src alias gt-two-netd_dst 1 permit
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_TWO_NETDESTINATIONS,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testTwoNetdestinationsIPv6(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination6 gt-two-netd_src
      host 2001::
    !

    netdestination6 gt-two-netd_dst
      host 2001::
    !

    ip access-list session test-filter
      ipv6 alias gt-two-netd_src alias gt-two-netd_dst 1 permit
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001::/128')]
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V6 +
                                         GOOD_TERM_TWO_NETDESTINATIONS,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testTwoNetworkNetdestinationsIPv4(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination gt-mix-netd_src
      network 10.0.0.0 255.0.0.0
    !

    netdestination gt-mix-netd_dst
      network 10.0.0.0 255.0.0.0
    !

    ip access-list session test-filter
      alias gt-mix-netd_src alias gt-mix-netd_dst 1 permit
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_TWO_NETWORK_NETDESTINATIONS,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testTwoNetworkNetdestinationsIPv6(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination6 gt-mix-netd_src
      network 2001::/64
    !

    netdestination6 gt-mix-netd_dst
      network 2001::/64
    !

    ip access-list session test-filter
      ipv6 alias gt-mix-netd_src alias gt-mix-netd_dst 1 permit
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001::/64')]
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V6 +
                                         GOOD_TERM_TWO_NETWORK_NETDESTINATIONS,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testCombinedNetdestinationsIPv4(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination good-term-combined-netdestinations_src
      host 10.0.0.1
      network 100.0.0.0 255.0.0.0
    !

    ip access-list session test-filter
      alias good-term-combined-netdestinations_src any tcp 80 deny
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('100.0.0.0/8'),
                                           nacaddr.IP('10.0.0.1/32')]
    self.naming.GetServiceByProto.return_value = ['80']
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_COMBINED_NETDESTINATIONS,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testCombinedNetdestinationsIPv6(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination6 good-term-combined-netdestinations_src
      host 2001::
      network 2002::/64
    !

    ip access-list session test-filter
      ipv6 alias good-term-combined-netdestinations_src any tcp 80 deny
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2002::/64'),
                                           nacaddr.IP('2001::/128')]
    self.naming.GetServiceByProto.return_value = ['80']
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V6 +
                                         GOOD_TERM_COMBINED_NETDESTINATIONS,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testMultipleTermsIPv4(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination good-terms-combined-1_src
      host 10.0.0.1
      network 100.0.0.0 255.0.0.0
    !

    netdestination good-terms-combined-1_dst
      host 10.0.0.1
      network 100.0.0.0 255.0.0.0
    !

    ip access-list session test-filter
      alias good-terms-combined-1_src alias good-terms-combined-1_dst udp 69 permit
      any any any deny
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('100.0.0.0/8'),
                                           nacaddr.IP('10.0.0.1/32')]
    self.naming.GetServiceByProto.return_value = ['69']
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERMS_COMBINED_SINGLE_CASE,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testMultipleTermsIPv6(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination6 good-terms-combined-1_src
      host 2001::
      network 2002::/64
    !

    netdestination6 good-terms-combined-1_dst
      host 2001::
      network 2002::/64
    !

    ip access-list session test-filter
      ipv6 alias good-terms-combined-1_src alias good-terms-combined-1_dst udp 69 permit
      ipv6 any any any deny
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2002::/64'),
                                           nacaddr.IP('2001::/128')]
    self.naming.GetServiceByProto.return_value = ['69']
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V6 +
                                         GOOD_TERMS_COMBINED_SINGLE_CASE,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testSourceIsUser(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination good-term-source-is-user_dst
      network 100.0.0.0 255.0.0.0
    !

    ip access-list session test-filter
      user alias good-term-source-is-user_dst tcp 53 permit
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('100.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['53']
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_SOURCE_IS_USER,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testDestinationIsUser(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination good-term-destination-is-user_src
      network 100.0.0.0 255.0.0.0
    !

    ip access-list session test-filter
      alias good-term-destination-is-user_src user tcp 53 permit
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('100.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['53']
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_DESTINATION_IS_USER,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testProtocolIsContiguousRange(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination good-term-destination-is-user_src
      network 100.0.0.0 255.0.0.0
    !

    ip access-list session test-filter
      alias good-term-destination-is-user_src user tcp 53 55 permit
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('100.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['53-55', '54']
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_DESTINATION_IS_USER,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testProtocolIsDiscontiguousRange(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination good-term-destination-is-user_src
      network 100.0.0.0 255.0.0.0
    !

    ip access-list session test-filter
      alias good-term-destination-is-user_src user tcp 1 permit
      alias good-term-destination-is-user_src user tcp 10 20 permit
      alias good-term-destination-is-user_src user tcp 53 55 permit
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('100.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['53-55', '54', '10-20', '1']
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_DESTINATION_IS_USER,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testNegateWithNetwork(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    netdestination good-term-negate_src
      network 100.0.0.0 255.0.0.0
    !

    ip access-list session test-filter
      no alias good-term-negate_src any any deny
    !
    """
    self.naming.GetNetAddr.return_value = [nacaddr.IP('100.0.0.0/8')]
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_NEGATE_1,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testNegateAny(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      no any any any permit
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_NEGATE_2,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))

  def testProtocolMap(self):
    expected_result = """\
    # $Id:$
    # $Date:$
    # $Revision:$
    ip access-list session test-filter
      any any 1 permit
      any any 47 permit
      any any 50 permit
    !
    """
    aru = aruba.Aruba(policy.ParsePolicy(GOOD_HEADER_V4 +
                                         GOOD_TERM_PROTOCOL_MAP,
                                         self.naming), EXP_INFO)
    self.assertEqual(textwrap.dedent(expected_result), str(aru))


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


# tests/lib/brocade_test.py

# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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 brocade acl rendering module."""

import re
from absl.testing import absltest
from unittest import mock

from capirca.lib import brocade
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: brocade test-filter
}
"""
GOOD_TERM = """
term good-term {
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: tcp
  policer:: batman
  option:: tcp-established
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'address',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'dscp_match',
    'expiration',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'owner',
    'platform',
    'platform_exclude',
    'protocol',
    'restrict_address_family',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next',
               'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'tcp-established',
               'is-fragment',
               'fragments'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class BrocadeTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testTcpEstablished(self):
    acl = brocade.Brocade(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    self.assertTrue(re.search('permit tcp any any established\n',
                              str(acl)), str(acl))

  def testNoTermRemark(self):
    acl = brocade.Brocade(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    self.assertNotIn('remark good-term-3', str(acl))

  def testBuildTokens(self):
    pol1 = brocade.Brocade(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM,
                                              self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = brocade.Brocade(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1,
                                              self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


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


# tests/lib/cgrep_test.py

# Copyright 2011 Google Inc. All Rights Reserved.
#
# 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.
#

"""Unit tests for cgrep.

   Order doesn't matter for the purposes of these tests, so many
   actual and expected results are sorted/sets to prevent issues relating to the
   order in which items are returned.
"""

import argparse
from absl.testing import absltest

from capirca.lib import nacaddr
from capirca.lib import naming
from tools import cgrep


_NETWORK = """
#
# Sample naming defintions for network objects
#
RFC1918 = 10.0.0.0/8      # non-public
          172.16.0.0/12   # non-public
          192.168.0.0/16  # non-public

INTERNAL = RFC1918

LOOPBACK = 127.0.0.0/8  # loopback
           ::1/128       # ipv6 loopback

RFC_3330 = 169.254.0.0/16  # special use IPv4 addresses - netdeploy

RFC_6598 = 100.64.0.0/10   # Shared Address Space

LINKLOCAL = FE80::/10  # IPv6 link-local

SITELOCAL = FEC0::/10    # Ipv6 Site-local

MULTICAST = 224.0.0.0/4  # IP multicast
            FF00::/8     # IPv6 multicast

CLASS-E   = 240.0.0.0/4

RESERVED = 0.0.0.0/8           # reserved
           RFC1918
           LOOPBACK
           RFC_3330
           RFC_6598
           MULTICAST
           CLASS-E
           0000::/8            # reserved by IETF
           0100::/8            # reserved by IETF
           0200::/7            # reserved by IETF
           0400::/6            # reserved by IETF
           0800::/5            # reserved by IETF
           1000::/4            # reserved by IETF
           4000::/3            # reserved by IETF
           6000::/3            # reserved by IETF
           8000::/3            # reserved by IETF
           A000::/3            # reserved by IETF
           C000::/3            # reserved by IETF
           E000::/4            # reserved by IETF
           F000::/5            # reserved by IETF
           F800::/6            # reserved by IETF
           FC00::/7            # unique local unicast
           FE00::/9            # reserved by IETF
           LINKLOCAL           # link local unicast
           SITELOCAL           # IPv6 site-local

ANY = 0.0.0.0/0

# http://www.team-cymru.org/Services/Bogons/bogon-bn-agg.txt
# 22-Apr-2011
BOGON = 0.0.0.0/8
        192.0.0.0/24
        192.0.2.0/24
        198.18.0.0/15
        198.51.100.0/24
        203.0.113.0/24
        MULTICAST
        CLASS-E
        3FFE::/16      # 6bone
        5F00::/8       # 6bone
        2001:DB8::/32  # IPv6 documentation prefix

GOOGLE_PUBLIC_DNS_ANYCAST = 8.8.4.4/32               # IPv4 Anycast
                            8.8.8.8/32               # IPv4 Anycast
                            2001:4860:4860::8844/128 # IPv6 Anycast
                            2001:4860:4860::8888/128 # IPv6 Anycast
GOOGLE_DNS = GOOGLE_PUBLIC_DNS_ANYCAST


# The following are sample entires intended for us in the included
# sample policy file.  These should be removed.

WEB_SERVERS = 200.1.1.1/32  # Example web server 1
              200.1.1.2/32  # Example web server 2

MAIL_SERVERS = 200.1.1.4/32 # Example mail server 1
               200.1.1.5/32 # Example mail server 2

PUBLIC_NAT = 200.1.1.3/32   # Example company NAT address

NTP_SERVERS = 10.0.0.1/32   # Example NTP server
              10.0.0.2/32   # Example NTP server

TACACS_SERVERS = 10.1.0.1/32  # Example tacacs server
                 10.1.0.2/32  # Example tacacs server

INTERNAL_SERVER = 1.0.0.1/32

PUBLIC_SERVER = 100.0.0.1/32

INTERNAL_SERVERS = INTERNAL_SERVER

PUBLIC_SERVERS = PUBLIC_SERVER

SERVERS = INTERNAL_SERVERS
          PUBLIC_SERVERS

"""

_SERVICE = """
#
# Sample naming service definitions
#
WHOIS = 43/udp
SSH = 22/tcp
TELNET = 23/tcp
SMTP = 25/tcp
MAIL_SERVICES = SMTP
                ESMTP
                SMTP_SSL
                POP_SSL
TIME = 37/tcp 37/udp
TACACS = 49/tcp
DNS = 53/tcp 53/udp
BOOTPS = 67/udp   # BOOTP server
BOOTPC = 68/udp   # BOOTP client
DHCP = BOOTPS
       BOOTPC
TFTP = 69/tcp 69/udp
HTTP = 80/tcp
WEB_SERVICES = HTTP HTTPS
POP3 = 110/tcp
RPC = 111/udp
IDENT = 113/tcp 113/udp
NNTP = 119/tcp
NTP = 123/tcp 123/udp
MS_RPC_EPMAP = 135/udp 135/tcp
MS_137 = 137/udp
MS_138 = 138/udp
MS_139 = 139/tcp
IMAP = 143/tcp
SNMP = 161/udp
SNMP_TRAP = 162/udp
BGP = 179/tcp
IMAP3 = 220/tcp
LDAP = 389/tcp
LDAP_SERVICE = LDAP
               LDAPS
HTTPS = 443/tcp
MS_445 = 445/tcp
SMTP_SSL = 465/tcp
IKE = 500/udp
SYSLOG = 514/udp
RTSP = 554/tcp
ESMTP = 587/tcp
LDAPS = 636/tcp
IMAPS = 993/tcp
POP_SSL = 995/tcp
HIGH_PORTS = 1024-65535/tcp 1024-65535/udp
MSSQL = 1433/tcp
MSSQL_MONITOR = 1434/tcp
RADIUS = 1812/tcp 1812/udp
HSRP = 1985/udp
NFSD = 2049/tcp 2049/udp
NETFLOW = 2056/udp
SQUID_PROXY = 3128/tcp
MYSQL = 3306/tcp
RDP = 3389/tcp
IPSEC = 4500/udp
POSTGRESQL = 5432/tcp
TRACEROUTE = 33434-33534/udp
"""


class Namespace:

  def __init__(self, **kwargs):
    for arg in kwargs:
      setattr(self, arg, kwargs[arg])


class CgrepTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.db = naming.Naming(None)
    self.db.ParseServiceList(_SERVICE.split('\n'))
    self.db.ParseNetworkList(_NETWORK.split('\n'))

  #
  # test ip->token resolution (-i)
  #
  # 1.1.1.1 should only be in 'ANY'
  def test_one_ip(self):
    expected_results = [('ANY', ['0.0.0.0/0'])]
    ip = '1.1.1.1'
    results = cgrep.get_ip_parents(ip, self.db)
    self.assertEqual(results, expected_results)

  # 2001:db8::1 should only be in 'BOGON'
  def test_one_ipv6(self):
    expected_results = [('BOGON', ['2001:db8::/32'])]
    ip = '2001:db8::1'
    results = cgrep.get_ip_parents(ip, self.db)
    self.assertEqual(results, expected_results)

  # 1.1.1.1 should not be in CLASS-E
  def test_one_ip_fail(self):
    expected_results = [('CLASS-E', ['240.0.0.0/4'])]
    ip = nacaddr.IP('1.1.1.1/32')
    results = cgrep.get_ip_parents(ip, self.db)
    self.assertNotEqual(results, expected_results)

  # 2001:db8::1 should not be in LINKLOCAL
  def test_one_ipv6_fail(self):
    expected_results = [('LINKLOCAL', ['FE80::/10'])]
    ip = '2001:db8::1'
    results = cgrep.get_ip_parents(ip, self.db)
    self.assertNotEqual(results, expected_results)

  # 8.8.8.8 is in GOOGLE_PUBLIC_DNS_ANYCAST which is inside GOOGLE_DNS
  def test_one_ip_nested(self):
    expected_results = sorted((('GOOGLE_DNS', ['8.8.8.8/32']),
                               ('GOOGLE_DNS -> GOOGLE_PUBLIC_DNS_ANYCAST',
                                ['8.8.8.8/32']),
                               ('ANY', ['0.0.0.0/0'])))
    ip = '8.8.8.8'
    results = sorted(cgrep.get_ip_parents(ip, self.db))
    self.assertEqual(results, expected_results)

  # 2001:4860:4860::8844/128 is in GOOGLE_PUBLIC_DNS_ANYCAST which is
  # inside GOOGLE_DNS
  def test_one_ipv6_nested(self):
    expected_results = sorted((('GOOGLE_DNS', ['2001:4860:4860::8844/128']),
                               ('GOOGLE_DNS -> GOOGLE_PUBLIC_DNS_ANYCAST',
                                ['2001:4860:4860::8844/128'])))
    ip = '2001:4860:4860::8844/128'
    results = sorted(cgrep.get_ip_parents(ip, self.db))
    self.assertEqual(results, expected_results)

  # 1.0.0.1 is inside INTERNAL_SERVER, which is inside INTERNAL_SERVERS, which
  # is inside SERVERS
  def test_one_ip_multi_nested(self):
    expected_results = sorted((('INTERNAL_SERVERS -> INTERNAL_SERVER',
                                ['1.0.0.1/32']),
                               ('SERVERS -> INTERNAL_SERVER', ['1.0.0.1/32']),
                               ('SERVERS -> INTERNAL_SERVERS', ['1.0.0.1/32']),
                               ('SERVERS', ['1.0.0.1/32']),
                               ('ANY', ['0.0.0.0/0'])))
    ip = '1.0.0.1'
    results = sorted(cgrep.get_ip_parents(ip, self.db))
    self.assertEqual(results, expected_results)

  #
  # test 'ip in token' (-i -t)
  #
  # 8.8.8.8 is inside GOOGLE_DNS
  def test_ip_in_token(self):
    expected_results = r'8.8.8.8 is in GOOGLE_DNS'
    options = Namespace()
    options.ip = ('8.8.8.8',)
    options.token = ('GOOGLE_DNS')
    results = cgrep.compare_ip_token(options, self.db)
    self.assertEqual(results, expected_results)

  # 2001:4860:4860::8844 is inside GOOGLE_DNS
  def test_ipv6_in_token(self):
    expected_results = r'2001:4860:4860::8844 is in GOOGLE_DNS'
    options = Namespace()
    options.ip = ('2001:4860:4860::8844',)
    options.token = ('GOOGLE_DNS')
    results = cgrep.compare_ip_token(options, self.db)
    self.assertEqual(results, expected_results)

  # 69.171.239.12 is not in GOOGLE_DNS
  def test_ip_in_token_fail(self):
    expected_results = r'69.171.239.12 is _not_ in GOOGLE_DNS'
    options = Namespace()
    options.ip = ('69.171.239.12',)
    options.token = ('GOOGLE_DNS')
    results = cgrep.compare_ip_token(options, self.db)
    self.assertEqual(results, expected_results)

  # 2a03:2880:fffe:c:face:b00c:0:35 is not in GOOGLE_DNS
  def test_ipv6_in_token_fail(self):
    expected_results = r'2a03:2880:fffe:c:face:b00c:0:35 is _not_ in GOOGLE_DNS'
    options = Namespace()
    options.ip = ('2a03:2880:fffe:c:face:b00c:0:35',)
    options.token = ('GOOGLE_DNS')
    results = cgrep.compare_ip_token(options, self.db)
    self.assertEqual(results, expected_results)

  #
  # test network token compare (-c)
  #
  # these two tokens are identical and should contain the same nets
  def test_compare_same_token(self):
    expected_results = (
        (
            r'PUBLIC_NAT',
            r'PUBLIC_NAT',
            [
                nacaddr.IPv4('200.1.1.3/32')
            ],
        ),
        [
            r'200.1.1.3/32'
        ]
    )
    options = Namespace()
    options.cmp = ('PUBLIC_NAT', 'PUBLIC_NAT')
    results = cgrep.compare_tokens(options, self.db)
    self.assertEqual(results, expected_results)

  #
  # test network token encapsulations
  #
  def test_ip_contained(self):
    expected_results = True
    results = cgrep.check_encapsulated('network', 'RFC1918', 'RESERVED',
                                       self.db)
    self.assertEqual(results, expected_results)

  def test_ip_not_contained(self):
    expected_results = False
    results = cgrep.check_encapsulated('network', 'RESERVED', 'RFC1918',
                                       self.db)
    self.assertEqual(results, expected_results)

  def test_ipv6_contained(self):
    expected_results = True
    results = cgrep.check_encapsulated('network', 'LINKLOCAL', 'RESERVED',
                                       self.db)
    self.assertEqual(results, expected_results)

  def test_ipv6_not_contained(self):
    expected_results = False
    results = cgrep.check_encapsulated('network', 'RESERVED', 'LINKLOCAL',
                                       self.db)
    self.assertEqual(results, expected_results)

  #
  # test ip->object comparisons (-g)
  #
  # 8.8.8.8 is not present in object RESERVED and
  # 127.0.0.1 is not present in object GOOGLE_DNS and
  # the two IPs both exist in 'ANY'
  def test_group_diff(self):
    expected_results = sorted((
        ['ANY'],
        ['GOOGLE_DNS', 'GOOGLE_DNS -> GOOGLE_PUBLIC_DNS_ANYCAST'],
        ['RESERVED', 'RESERVED -> LOOPBACK']
    ))
    options = Namespace()
    options.gmp = ['8.8.8.8', '127.0.0.1']
    results = sorted(cgrep.group_diff(options, self.db))
    self.assertCountEqual(results, expected_results)

  # test to make sure two IPs share the same groups
  def test_group_diff_identical(self):
    expected_results = sorted((
        ['ANY',
         'INTERNAL',
         'INTERNAL -> RFC1918',
         'RESERVED',
         'RESERVED -> RFC1918'],
        [],
        []
    ))
    options = Namespace()
    options.gmp = ['172.16.0.1', '192.168.0.1']
    results = sorted(cgrep.group_diff(options, self.db))
    self.assertCountEqual(results, expected_results)

  #
  # test token->ip(s) resolution (-o)
  #
  # resolve GOOGLE_DNS to the 4 given IPs
  def test_token_to_ips(self):
    expected_results = [
        (
            r'GOOGLE_DNS',
            [
                nacaddr.IPv4('8.8.4.4/32'),
                nacaddr.IPv4('8.8.8.8/32'),
                nacaddr.IPv6('2001:4860:4860::8844/128'),
                nacaddr.IPv6('2001:4860:4860::8888/128')
            ]
        )
    ]
    options = Namespace()
    options.obj = ('GOOGLE_DNS',)

    results = cgrep.get_nets(options.obj, self.db)
    self.assertEqual(results[0][0], expected_results[0][0])
    self.assertEqual(set(results[0][1]), set(expected_results[0][1]))

  # GOOGLE_DNS does not resole to the given IP
  def test_token_to_ip_fail(self):
    expected_results = [
        (
            r'GOOGLE_DNS',
            [
                nacaddr.IPv4('69.171.239.12/32'),
                nacaddr.IPv6('2a03:2880:fffe:c:face:b00c:0:35/128')
            ]
        )
    ]
    options = Namespace()
    options.obj = ('GOOGLE_DNS',)
    results = cgrep.get_nets(options.obj, self.db)
    # the network object name should match, but not the IPs contained within
    self.assertEqual(results[0][0], expected_results[0][0])
    self.assertNotEqual(set(results[0][1]), set(expected_results[0][1]))

  #
  # test service->port resolution (-s)
  #
  # "SSH" is just '22/tcp'
  def test_svc_to_port(self):
    expected_results = [
        (
            r'SSH',
            [
                '22/tcp'
            ]
        )
    ]
    options = Namespace()
    options.service = ('SSH',)
    results = cgrep.get_ports(options.service, self.db)
    self.assertEqual(results, expected_results)

  # "SSH" does not contain '23/tcp'
  def test_svc_to_port_fail(self):
    expected_results = [
        (
            r'SSH',
            [
                '23/tcp'
            ]
        )
    ]
    options = Namespace()
    options.svc = ('SSH',)
    results = cgrep.get_ports(options.svc, self.db)
    self.assertNotEqual(results, expected_results)

  #
  # test port->service object resolution (-p)
  #
  # '22/tcp' belongs to SSH
  def test_get_port_parents(self):
    expected_results = (r'22', r'tcp', ['SSH'])
    options = Namespace()
    options.port = ('22', 'tcp')
    results = cgrep.get_services(options, self.db)
    self.assertEqual(results, expected_results)

  # 22/tcp does not belong to TELNET
  def test_get_port_parents_fail(self):
    expected_results = (r'22', r'tcp', ['TELNET'])
    options = Namespace()
    options.port = ('22', 'tcp')
    results = cgrep.get_services(options, self.db)
    self.assertNotEqual(results, expected_results)

  # 33434/tcp should only be in HIGH_PORTS (not also TRACEROUTE)
  def test_get_port_parents_range_tcp(self):
    expected_results = (r'33434', r'tcp', ['HIGH_PORTS'])
    options = Namespace()
    options.port = ('33434', 'tcp')
    results = cgrep.get_services(options, self.db)
    self.assertEqual(results, expected_results)

  # 33434/udp should be in HIGH_PORTS and TRACEROUTE
  def test_get_port_parents_range_udp(self):
    expected_results = (r'33434', r'udp', ['HIGH_PORTS', 'TRACEROUTE'])
    options = Namespace()
    options.port = ('33434', 'udp')
    results = cgrep.get_services(options, self.db)
    self.assertEqual(results, expected_results)

  #
  # test IP validity
  #
  def test_invalid_ip(self):
    self.assertRaises(argparse.ArgumentTypeError,
                      cgrep.is_valid_ip, '10.0.0.256')

  def test_invalid_ipv6(self):
    self.assertRaises(argparse.ArgumentTypeError,
                      cgrep.is_valid_ip, '2001:db8::z')

  def test_valid_ips(self):
    arg = '8.8.8.8'
    results = cgrep.is_valid_ip(arg)
    self.assertEqual(results, arg)

  def test_valid_ips_v6(self):
    arg = '2001:4860:4860::8844'
    results = cgrep.is_valid_ip(arg)
    self.assertEqual(results, arg)


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


# tests/lib/cisco_test.py

# Copyright 2008 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for cisco acl rendering module."""

import datetime
import re
from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import cisco
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy


GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: cisco test-filter
}
"""
GOOD_STANDARD_HEADER_1 = """
header {
  comment:: "this is a standard acl"
  target:: cisco 99 standard
}
"""
GOOD_STANDARD_HEADER_2 = """
header {
  comment:: "this is a standard acl"
  target:: cisco FOO standard
}
"""
GOOD_STANDARD_NUMBERED_HEADER = """
header {
  comment:: "numbered standard"
  target:: cisco 50 standard
}
"""
GOOD_OBJGRP_HEADER = """
header {
  comment:: "obj group header test"
  target:: cisco objgroupheader object-group
}
"""
GOOD_INET6_HEADER = """
header {
  comment:: "inet6 header test"
  target:: cisco inet6_acl inet6
}
"""
GOOD_MIXED_HEADER = """
header {
  comment:: "mixed inet/inet6 header test"
  target:: cisco mixed_acl mixed
}
"""
GOOD_DSMO_HEADER = """
header {
  comment:: "this is a dsmo test acl"
  target:: cisco dsmo_acl extended enable_dsmo
}
"""
GOOD_EXTENDED_NUMBERED_HEADER = """
header {
  comment:: "numbered extended"
  target:: cisco 150 extended
}
"""
GOOD_NOVERBOSE_HEADER = """
header {
  comment:: "should not see me"
  target:: cisco test-filter noverbose
}
"""

GOOD_NOVERBOSE_STANDARD_HEADER = """
header {
  comment:: "should not see me"
  target:: cisco 99 standard noverbose
}
"""
GOOD_NOVERBOSE_OBJGRP_HEADER = """
header {
  comment:: "should not see me"
  target:: cisco objgroupheader object-group noverbose
}
"""
GOOD_NOVERBOSE_INET6_HEADER = """
header {
  comment:: "should not see me"
  target:: cisco inet6_acl inet6 noverbose
}
"""
BAD_STANDARD_HEADER_1 = """
header {
  comment:: "this is a standard acl"
  target:: cisco 2001 standard
}
"""
BAD_STANDARD_HEADER_2 = """
header {
  comment:: "this is a standard acl"
  target:: cisco 101 standard
}
"""
BAD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: juniper test-filter
}
"""
BAD_HEADER_2 = """
header {
  target:: cisco 1300
}
"""
LONG_VERSION_HEADER = """
header {
  comment:: "This long header should be split even on a looooooooooooooooooooooooooonnnnnnnnnnnnnnnnnngggggggggg string. https://www.google.com/maps/place/1600+Amphitheatre+Parkway,+Mountain+View,+CA/@37.507491,-122.2540443,15z/data=!4m5!3m4!1s0x808fb99f8c51e885:0x169ef02a512c5b28!8m2!3d37.4220579!4d-122.0840897"
  target:: cisco test-filter
}
"""
GOOD_STANDARD_TERM_1 = """
term standard-term-1 {
  address:: SOME_HOST
  action:: accept
}
"""
GOOD_STANDARD_TERM_2 = """
term standard-term-2 {
  address:: SOME_HOST
  action:: accept
}
"""
BAD_STANDARD_TERM_1 = """
term bad-standard-term-1 {
  destination-address:: SOME_HOST
  protocol:: tcp
  action:: accept
}
"""
UNSUPPORTED_TERM_1 = """
term protocol_except_term {
  protocol-except:: tcp udp icmp
  action:: reject
}
"""
UNSUPPORTED_TERM_2 = """
term protocol_except_term {
  source-prefix:: configured-neighbors-only
  action:: reject
}
"""
EXPIRED_TERM = """
term is_expired {
  expiration:: 2001-01-01
  action:: accept
}
"""
EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}
"""
GOOD_TERM_2 = """
term good-term-2 {
  protocol:: tcp
  destination-address:: SOME_HOST
  source-port:: HTTP
  option:: established
  action:: accept
}
"""
GOOD_TERM_3 = """
term good-term-3 {
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""
GOOD_TERM_4 = """
term good-term-4 {
  protocol:: tcp
  logging:: true
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  verbatim:: cisco "mary had a little lamb"
  verbatim:: iptables "mary had second lamb"
  verbatim:: juniper "mary had third lamb"
}
"""
GOOD_TERM_6 = """
term good-term-6 {
  destination-address:: ANY
  action:: accept
}
"""
GOOD_TERM_7 = """
term good-term {
  protocol:: vrrp
  action:: accept
}
"""
GOOD_TERM_8 = """
term good-term {
  protocol:: tcp
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_9 = """
term good-term-9 {
  protocol:: tcp udp
  option:: established
  action:: accept
}
"""
GOOD_TERM_10 = """
term good-term-10 {
  protocol:: icmp
  icmp-type:: echo-reply unreachable time-exceeded
  action:: accept
}
"""
GOOD_TERM_11 = """
term good-term-11 {
  protocol:: icmpv6
  icmp-type:: echo-reply destination-unreachable time-exceeded
  action:: accept
}
"""
GOOD_TERM_12 = """
term good-term-12 {
  action:: accept
}
"""

GOOD_TERM_13 = """
term good-term-13 {
  owner:: foo@google.com
  action:: accept
}
"""
GOOD_TERM_14 = """
term good-term-14 {
  protocol:: tcp
  destination-address:: SOME_HOST
  destination-port:: CONSECUTIVE_PORTS
  action:: accept
}
"""
GOOD_TERM_15 = """
term good-term-15 {
  protocol:: hopopt
  action:: accept
}
"""
GOOD_TERM_16 = """
term good-term-16 {
  protocol:: tcp
  action:: accept
  dscp-match:: 42
}
"""
GOOD_TERM_17 = """
term good-term-17 {
  protocol:: tcp udp
  policer:: batman
  option:: established
  action:: accept
}
"""
GOOD_TERM_18 = """
term good-term-18 {
  source-address:: SOME_HOST
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_19 = """
term good_term_19 {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 3 4
  action:: accept
}
"""
GOOD_TERM_20 = """
term good_term_20 {
  source-address:: SOME_HOST
  destination-address:: SOME_HOST
  option:: fragments
  action:: accept
}
"""
GOOD_TERM_21 = """
term good_term_21 {
  source-address:: cs4-valid_network_name
  destination-address:: cs4-valid_network_name
  action:: accept
}
"""
GOOD_TERM_22 = """
term good_term_22 {
  source-address:: SOME_HOST
  destination-address:: SOME_HOST
  option:: is-fragment
  action:: accept
}
"""
GOOD_TERM_23 = """
term good_term_23 {
  protocol:: tcp
  destination-address:: SOME_HOST
  restrict-address-family:: inet
  action:: accept
}
"""
GOOD_TERM_24 = """
term good_term_24 {
  protocol:: ipip
  destination-address:: SOME_HOST
  action:: accept
}
"""
LONG_COMMENT_TERM = """
term long-comment-term {
  comment:: "%s "
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'address',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'dscp_match',
    'expiration',
    'icmp_type',
    'icmp_code',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'owner',
    'platform',
    'platform_exclude',
    'protocol',
    'restrict_address_family',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next',
               'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'tcp-established',
               'is-fragment',
               'fragments'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class CiscoTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testIPVersion(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('0.0.0.0/0'),
                                           nacaddr.IP('::/0')]

    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_6, self.naming)
    acl = cisco.Cisco(pol, EXP_INFO)
    # check if we've got a v6 address in there.
    self.assertNotIn('::', str(acl), str(acl))

    self.naming.GetNetAddr.assert_called_once_with('ANY')

  def testOptions(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['80']

    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_2,
                                         self.naming), EXP_INFO)
    # this is a hacky sort of way to test that 'established' maps to HIGH_PORTS
    # in the destination port section.
    range_test = 'permit tcp any eq 80 10.0.0.0 0.255.255.255 range 1024 65535'
    self.assertIn(range_test, str(acl), '[%s]' % str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('HTTP', 'tcp')

  def testExpandingConsequtivePorts(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['80', '81']

    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_14,
                                         self.naming), EXP_INFO)
    first_string = 'permit tcp any 10.0.0.0 0.255.255.255 eq 80'
    second_string = 'permit tcp any 10.0.0.0 0.255.255.255 eq 81'
    self.assertIn(first_string, str(acl), '[%s]' % str(acl))
    self.assertIn(second_string, str(acl), '[%s]' % str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with(
        'CONSECUTIVE_PORTS', 'tcp')

  def testDSCP(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_16,
                                         self.naming), EXP_INFO)
    self.assertTrue(re.search('permit tcp any any dscp 42', str(acl)),
                    str(acl))

  def testTermAndFilterName(self):
    acl = cisco.Cisco(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_1 + GOOD_TERM_6, self.naming), EXP_INFO)
    self.assertIn('ip access-list extended test-filter', str(acl), str(acl))
    self.assertIn(' remark good-term-1', str(acl), str(acl))
    self.assertIn(' permit ip any any', str(acl), str(acl))

  def testRemark(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]

    # Extended ACLs should have extended remark style.
    acl = cisco.Cisco(policy.ParsePolicy(
        GOOD_EXTENDED_NUMBERED_HEADER + GOOD_TERM_1, self.naming), EXP_INFO)
    self.assertIn('ip access-list extended 150', str(acl), str(acl))
    self.assertIn(' remark numbered extended', str(acl), str(acl))
    self.assertNotIn('150 remark', str(acl), str(acl))
    # Standard ACLs should have standard remark style.
    acl = cisco.Cisco(policy.ParsePolicy(
        GOOD_STANDARD_NUMBERED_HEADER + GOOD_STANDARD_TERM_1, self.naming),
                      EXP_INFO)
    self.assertIn('access-list 50 remark numbered standard', str(acl),
                  str(acl))
    self.assertIn('access-list 50 remark standard-term-1', str(acl),
                  str(acl))
    self.assertIn('access-list 50 remark %sId:%s' % ('$', '$'), str(acl),
                  str(acl))
    self.assertNotIn('access-list 50 remark %sRevision:%s' % ('$', '$'),
                     str(acl), str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testTcpEstablished(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_3,
                                         self.naming), EXP_INFO)
    self.assertTrue(re.search('permit tcp any any established\n',
                              str(acl)), str(acl))

  def testLogging(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_4,
                                         self.naming), EXP_INFO)
    self.assertTrue(re.search('permit tcp any any log\n',
                              str(acl)), str(acl))

  def testVerbatimTerm(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_5,
                                         self.naming), EXP_INFO)
    self.assertIn('mary had a little lamb', str(acl), str(acl))
    # check if other platforms verbatim shows up in ouput
    self.assertNotIn('mary had a second lamb', str(acl), str(acl))
    self.assertNotIn('mary had a third lamb', str(acl), str(acl))

  def testDuplicateTermNames(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/24')]
    pol = policy.ParsePolicy(GOOD_STANDARD_HEADER_1 + GOOD_STANDARD_TERM_1 +
                             GOOD_STANDARD_TERM_1, self.naming)
    self.assertRaises(cisco.CiscoDuplicateTermError, cisco.Cisco, pol, EXP_INFO)

  def testBadStandardTerm(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]

    pol = policy.ParsePolicy(GOOD_STANDARD_HEADER_1 + BAD_STANDARD_TERM_1,
                             self.naming)
    self.assertRaises(cisco.StandardAclTermError, cisco.Cisco, pol, EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testStandardTermHost(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]

    pol = policy.ParsePolicy(GOOD_STANDARD_HEADER_1 + GOOD_STANDARD_TERM_1,
                             self.naming)
    acl = cisco.Cisco(pol, EXP_INFO)
    expected = 'access-list 99 permit 10.1.1.1'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testStandardTermNet(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]

    pol = policy.ParsePolicy(GOOD_STANDARD_HEADER_1 + GOOD_STANDARD_TERM_2,
                             self.naming)
    acl = cisco.Cisco(pol, EXP_INFO)
    expected = 'access-list 99 permit 10.0.0.0 0.255.255.255'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testNamedStandard(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]

    pol = policy.ParsePolicy(GOOD_STANDARD_HEADER_2 + GOOD_STANDARD_TERM_2,
                             self.naming)
    acl = cisco.Cisco(pol, EXP_INFO)
    expected = 'ip access-list standard FOO'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit 10.0.0.0 0.255.255.255\n'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testNoIPv6InOutput(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2620:0:1000::/40')]

    pol = policy.ParsePolicy(GOOD_STANDARD_HEADER_1 + GOOD_STANDARD_TERM_2,
                             self.naming)
    acl = cisco.Cisco(pol, EXP_INFO)
    self.assertNotIn('::', str(acl), '[%s]' % str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testStandardFilterName(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]

    pol = policy.ParsePolicy(BAD_STANDARD_HEADER_1 + GOOD_STANDARD_TERM_2,
                             self.naming)
    self.assertRaises(cisco.UnsupportedCiscoAccessListError,
                      cisco.Cisco, pol, EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testStandardFilterRange(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]

    pol = policy.ParsePolicy(BAD_STANDARD_HEADER_2 + GOOD_STANDARD_TERM_2,
                             self.naming)
    self.assertRaises(cisco.UnsupportedCiscoAccessListError,
                      cisco.Cisco, pol, EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testObjectGroup(self):
    ip_grp = ['object-group network ipv4 SOME_HOST']
    ip_grp.append(' 10.0.0.0/8')
    ip_grp.append('exit')
    port_grp1 = ['object-group port 80-80']
    port_grp1.append(' eq 80')
    port_grp1.append('exit')
    port_grp2 = ['object-group port 1024-65535']
    port_grp2.append(' range 1024 65535')
    port_grp2.append('exit')

    self.naming.GetNetAddr.return_value = [
        nacaddr.IP('10.0.0.0/8', token='SOME_HOST')]
    self.naming.GetServiceByProto.return_value = ['80']

    pol = policy.ParsePolicy(
        GOOD_OBJGRP_HEADER + GOOD_TERM_2 + GOOD_TERM_18, self.naming)
    acl = cisco.Cisco(pol, EXP_INFO)

    self.assertIn('\n'.join(ip_grp), str(acl), '%s %s' % (
        '\n'.join(ip_grp), str(acl)))
    self.assertIn('\n'.join(port_grp1), str(acl), '%s %s' % (
        '\n'.join(port_grp1), str(acl)))
    self.assertIn('\n'.join(port_grp2), str(acl), '%s %s' % (
        '\n'.join(port_grp2), str(acl)))

    # Object-group terms should use the object groups created.
    self.assertIn(
        ' permit tcp any port-group 80-80 net-group SOME_HOST port-group'
        ' 1024-65535', str(acl), str(acl))
    self.assertIn(
        ' permit ip net-group SOME_HOST net-group SOME_HOST', str(acl),
        str(acl))

    # There should be no addrgroups that look like IP addresses.
    for addrgroup in re.findall(r'net-group ([a-f0-9.:/]+)', str(acl)):
      self.assertRaises(ValueError, nacaddr.IP(addrgroup))

    self.naming.GetNetAddr.assert_has_calls([mock.call('SOME_HOST'),
                                             mock.call('SOME_HOST')])
    self.naming.GetServiceByProto.assert_called_once_with('HTTP', 'tcp')

  def testInet6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8'),
                                           nacaddr.IP('2001:4860:8000::/33')]

    acl = cisco.Cisco(policy.ParsePolicy(GOOD_INET6_HEADER + GOOD_TERM_8,
                                         self.naming), EXP_INFO)
    inet6_test1 = 'no ipv6 access-list inet6_acl'
    inet6_test2 = 'ipv6 access-list inet6_acl'
    inet6_test3 = 'permit tcp any 2001:4860:8000::/33'
    self.assertIn(inet6_test1, str(acl), '[%s]' % str(acl))
    self.assertIn(inet6_test2, str(acl), '[%s]' % str(acl))
    self.assertTrue(re.search(inet6_test3, str(acl)), str(acl))
    self.assertNotIn('10.0.0.0', str(acl), str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testMixed(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8'),
                                           nacaddr.IP('2001:4860:8000::/33')]

    acl = cisco.Cisco(policy.ParsePolicy(GOOD_MIXED_HEADER + GOOD_TERM_8,
                                         self.naming), EXP_INFO)
    inet6_test1 = 'no ip access-list extended mixed_acl'
    inet6_test2 = 'ip access-list extended mixed_acl'
    inet6_test3 = 'permit tcp any 10.0.0.0 0.255.255.255'
    inet6_test4 = 'no ipv6 access-list ipv6-mixed_acl'
    inet6_test5 = 'ipv6 access-list ipv6-mixed_acl'
    inet6_test6 = 'permit tcp any 2001:4860:8000::/33'
    aclout = str(acl)
    self.assertIn(inet6_test1, aclout, '[%s]' % aclout)
    self.assertIn(inet6_test2, aclout, '[%s]' % aclout)
    self.assertTrue(re.search(inet6_test3, aclout), aclout)
    self.assertIn(inet6_test4, aclout, '[%s]' % aclout)
    self.assertIn(inet6_test5, aclout, '[%s]' % aclout)
    self.assertTrue(re.search(inet6_test6, aclout), aclout)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testRestrictAddressFamilyType(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IPv4('127.0.0.1'), nacaddr.IPv6('::1/128')]

    acl = cisco.Cisco(policy.ParsePolicy(GOOD_MIXED_HEADER + GOOD_TERM_23,
                                         self.naming), EXP_INFO)
    output = str(acl)
    self.assertIn('127.0.0.1', output, output)
    self.assertNotIn('::1/128', output, output)
    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testDsmo(self):
    addr_list = list()
    for octet in range(0, 256):
      net = nacaddr.IP('192.168.' + str(octet) + '.64/27')
      addr_list.append(net)
    self.naming.GetNetAddr.return_value = addr_list

    acl = cisco.Cisco(policy.ParsePolicy(GOOD_DSMO_HEADER + GOOD_TERM_8,
                                         self.naming), EXP_INFO)
    self.assertIn('permit tcp any 192.168.0.64 0.0.255.31', str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testUdpEstablished(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_9,
                                         self.naming), EXP_INFO)
    self.assertFalse(re.search('permit 17 any any established',
                               str(acl)), str(acl))

  def testIcmpTypes(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_10,
                                         self.naming), EXP_INFO)
    # echo-reply = 0
    self.assertTrue(re.search('permit icmp any any 0',
                              str(acl)), str(acl))
    # unreachable = 3
    self.assertTrue(re.search('permit icmp any any 3',
                              str(acl)), str(acl))
    # time-exceeded = 11
    self.assertTrue(re.search('permit icmp any any 11',
                              str(acl)), str(acl))

  def testIcmpCode(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_19,
                                         self.naming), EXP_INFO)
    output = str(acl)
    self.assertIn(' permit icmp any any 3 3', output, output)
    self.assertIn(' permit icmp any any 3 4', output, output)

  def testIpv6IcmpTypes(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_INET6_HEADER + GOOD_TERM_11,
                                         self.naming), EXP_INFO)
    # echo-reply = icmp-type code 129
    self.assertTrue(re.search('permit 58 any any 129',
                              str(acl)), str(acl))
    # destination-unreachable = icmp-type code 1
    self.assertTrue(re.search('permit 58 any any 1',
                              str(acl)), str(acl))
    # time-exceeded = icmp-type code 3
    self.assertTrue(re.search('permit 58 any any 3',
                              str(acl)), str(acl))

  @mock.patch.object(cisco.logging, 'debug')
  def testIcmpv6InetMismatch(self, mock_debug):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_11,
                                         self.naming), EXP_INFO)
    # output happens in __str_
    str(acl)

    mock_debug.assert_called_once_with(
        'Term good-term-11 will not be rendered,'
        ' as it has icmpv6 match specified but '
        'the ACL is of inet address family.')

  @mock.patch.object(cisco.logging, 'debug')
  def testIcmpInet6Mismatch(self, mock_debug):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_INET6_HEADER + GOOD_TERM_1,
                                         self.naming), EXP_INFO)
    # output happens in __str_
    str(acl)

    mock_debug.assert_called_once_with(
        'Term good-term-1 will not be rendered,'
        ' as it has icmp match specified but '
        'the ACL is of inet6 address family.')

  def testUnsupportedKeywordsError(self):
    pol1 = policy.ParsePolicy(GOOD_HEADER + UNSUPPORTED_TERM_1, self.naming)
    pol2 = policy.ParsePolicy(GOOD_HEADER + UNSUPPORTED_TERM_1, self.naming)
    # protocol-except
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      cisco.Cisco, pol1, EXP_INFO)
    # source-prefix
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      cisco.Cisco, pol2, EXP_INFO)

  def testDefaultInet6Protocol(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_INET6_HEADER + GOOD_TERM_12,
                                         self.naming), EXP_INFO)
    self.assertTrue(re.search('permit ipv6 any any', str(acl)), str(acl))

  @mock.patch.object(cisco.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    _ = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + EXPIRED_TERM,
                                       self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and will not '
        'be rendered.', 'is_expired', 'test-filter')

  @mock.patch.object(cisco.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    _ = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + EXPIRING_TERM %
                                       exp_date.strftime('%Y-%m-%d'),
                                       self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring', 'test-filter')

  def testTermHopByHop(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_15,
                                         self.naming), EXP_INFO)
    self.assertIn('permit hbh any any', str(acl), str(acl))

  def testOwnerTerm(self):
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER +
                                         GOOD_TERM_13, self.naming), EXP_INFO)
    self.assertTrue(re.search(' remark Owner: foo@google.com',
                              str(acl)), str(acl))

  def testBuildTokens(self):
    pol1 = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_5,
                                          self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_17,
                                          self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testProtoInts(self):
    pol = policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_7 + GOOD_TERM_9,
        self.naming)
    acl = cisco.Cisco(pol, EXP_INFO)
    self.assertIn('permit 112 any any', str(acl), str(acl))
    self.assertIn('permit tcp any any range 1024 65535 '
                  'established', str(acl), str(acl))
    self.assertIn('permit udp any any range 1024 65535', str(acl),
                  str(acl))

  def testFragments01(self):
    """Test policy term using 'fragments' (ref Github issue #187)."""
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/24')]
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_20,
                                         self.naming), EXP_INFO)
    expected = 'permit ip 10.0.0.0 0.0.0.255 10.0.0.0 0.0.0.255 fragments'
    self.assertIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_has_calls([mock.call('SOME_HOST'),
                                             mock.call('SOME_HOST')])

  def testFragments02(self):
    """Test policy term using 'is-fragment' (ref Github issue #187)."""
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/24')]
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_22,
                                         self.naming), EXP_INFO)
    expected = 'permit ip 10.0.0.0 0.0.0.255 10.0.0.0 0.0.0.255 fragments'
    self.assertIn(expected, str(acl))

  def testTermDSCPMarker(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/24')]
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_21,
                                         self.naming), EXP_INFO)
    expected = 'permit ip 10.0.0.0 0.0.0.255 10.0.0.0 0.0.0.255'
    self.assertIn(expected, str(acl))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('cs4-valid_network_name'),
         mock.call('cs4-valid_network_name')])

  def testNoVerbose(self):
    for i in [GOOD_NOVERBOSE_HEADER, GOOD_NOVERBOSE_STANDARD_HEADER,
              GOOD_NOVERBOSE_OBJGRP_HEADER, GOOD_NOVERBOSE_INET6_HEADER]:
      self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/24')]
      acl = cisco.Cisco(policy.ParsePolicy(i+GOOD_STANDARD_TERM_1, self.naming),
                        EXP_INFO)
      self.assertNotIn('remark', str(acl), str(acl))

  def testLongHeader(self):
    pol = policy.ParsePolicy(
        LONG_VERSION_HEADER + GOOD_TERM_7,
        self.naming)
    acl = cisco.Cisco(pol, EXP_INFO)
    self.assertIn('remark This long header should be split even on a', str(acl))
    self.assertIn(('remark looooooooooooooooooooooooooonnnnnnnnnnnnnnnnnn'
                   'gggggggggg string.'), str(acl))
    self.assertIn(('remark https://www.google.com/maps/place/1600+Amphitheatr'
                   'e+Parkway,+Mountain+'), str(acl))
    self.assertIn(('remark View,+CA/@37.507491,-122.2540443,15z/data=!4m5!3m4!'
                   '1s0x808fb99f8c51e88'), str(acl))
    self.assertIn(('remark 5:0x169ef02a512c5b28!8m2!3d37.4220579!4d-122.084'
                   '0897'), str(acl))

  def testIPIP(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/24')]
    acl = cisco.Cisco(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_24,
                                         self.naming), EXP_INFO)
    self.assertIn('permit 4 ', str(acl))

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


# tests/lib/ciscoasa_test.py

# Copyright 2008 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for ciscoasa acl rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import ciscoasa
from capirca.lib import naming
from capirca.lib import policy


GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: ciscoasa test-filter
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  verbatim:: ciscoasa "mary had a little lamb"
  verbatim:: iptables "mary had second lamb"
  verbatim:: juniper "mary had third lamb"
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  verbatim:: ciscoasa "mary had a little lamb"
  policer:: batman
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'owner',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next',
               'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request', 'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request', 'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established', 'tcp-established'}}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class CiscoASATest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testBuildTokens(self):
    pol1 = ciscoasa.CiscoASA(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1,
                                                self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = ciscoasa.CiscoASA(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_2,
                                                self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


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


# tests/lib/cisconx_test.py

# Copyright 2021 Google Inc. All Rights Reserved.
#
# 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 cisconx acl rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import cisconx
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER = """
header {
  comment:: "this is a test extended acl"
  target:: cisconx test-filter extended
}
"""

GOOD_HEADER_2 = """
header {
  comment:: "this is a test acl"
  target:: cisconx test-filter
}
"""

GOOD_HEADER_IPV6 = """
header {
  comment:: "this is a test inet6 acl"
  target:: cisconx test-filter inet6
}
"""

GOOD_TERM = """
term good-term {
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  protocol:: tcp
  option:: tcp-established
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  source-address:: SOME_HOST
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_3 = """
term good-term-3 {
  source-address:: SOME_HOST2
  destination-port:: GOPENFLOW
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_4 = """
term good-term-4 {
  comment:: "Accept SNMP from internal sources."
  address:: SOME_HOST
  action:: accept
}
"""

GOOD_TERM_5 = """
term good-term-5 {
  comment:: "Accept ESP from internal sources."
  address:: SOME_HOST
  protocol:: esp
  action:: accept
}
"""

GOOD_TERM_6 = """
term good-term-6 {
  comment:: "Accept AH from internal sources."
  address:: SOME_HOST
  protocol:: ah
  action:: accept
}
"""

GOOD_TERM_7 = """
term good-term-6 {
  comment:: "Accept AH from internal sources."
  address:: SOME_HOST
  protocol:: ah esp tcp
  action:: accept
}
"""
GOOD_TERM_10 = """
term good-term-10 {
  protocol:: icmp
  icmp-type:: echo-reply unreachable time-exceeded
  action:: accept
}
"""
GOOD_TERM_11 = """
term good-term-11 {
  protocol:: icmpv6
  icmp-type:: echo-reply destination-unreachable time-exceeded
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'address',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'dscp_match',
    'expiration',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'owner',
    'platform',
    'platform_exclude',
    'protocol',
    'restrict_address_family',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {
        'established',
        'tcp-established',
        'is-fragment',
        'fragments'
    }
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class CiscoNXTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testRemark(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]

    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_4, self.naming)
    acl = cisconx.CiscoNX(pol, EXP_INFO)
    expected = 'remark this is a test extended acl'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = 'remark good-term-4'
    self.assertIn(expected, str(acl), str(acl))
    expected = 'test-filter remark'
    self.assertNotIn(expected, str(acl), str(acl))
    self.assertNotIn(' remark %sId:%s' % ('$', '$'), str(acl), str(acl))
    self.assertIn(' remark "%sRevision:%s"' % ('$', '$'), str(acl), str(acl))
    self.assertNotIn(' remark $', str(acl), str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testExtendedNXosSyntax(self):
    # Extended access-lists should not use the "extended" argument to ip
    # access-list.
    acl = cisconx.CiscoNX(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    self.assertIn('ip access-list test-filter', str(acl))

  def testBuildTokens(self):
    pol1 = cisconx.CiscoNX(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = cisconx.CiscoNX(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testStandardTermHost(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.0/24')]
    self.naming.GetServiceByProto.return_value = ['22', '6537']

    pol = policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_2 + GOOD_TERM_3,
                             self.naming)
    acl = cisconx.CiscoNX(pol, EXP_INFO)
    expected = 'ip access-list test-filter'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit tcp 10.1.1.0 0.0.0.255 any eq 22'
    self.assertIn(expected, str(acl), str(acl))
    expected = ' permit tcp 10.1.1.0 0.0.0.255 any eq 6537'
    self.assertIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('SOME_HOST'),
         mock.call('SOME_HOST2')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('SSH', 'tcp'),
         mock.call('GOPENFLOW', 'tcp')])

  def testStandardTermHostV6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2620:1::/64')]
    self.naming.GetServiceByProto.return_value = ['22']

    pol = policy.ParsePolicy(GOOD_HEADER_IPV6 + GOOD_TERM_2, self.naming)
    acl = cisconx.CiscoNX(pol, EXP_INFO)
    expected = 'ipv6 access-list test-filter'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit tcp 2620:1::/64 any eq 22'
    self.assertIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_has_calls([mock.call('SOME_HOST')])
    self.naming.GetServiceByProto.assert_has_calls([mock.call('SSH', 'tcp')])

  def testIcmpTypes(self):
    acl = cisconx.CiscoNX(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_10, self.naming), EXP_INFO)
    # echo-reply = 0
    expected = 'permit icmp any any 0'
    self.assertIn(expected, str(acl), str(acl))
    # unreachable = 3
    expected = 'permit icmp any any 3'
    self.assertIn(expected, str(acl), str(acl))
    # time-exceeded = 11
    expected = 'permit icmp any any 11'
    self.assertIn(expected, str(acl), str(acl))

  def testIpv6IcmpTypes(self):
    acl = cisconx.CiscoNX(
        policy.ParsePolicy(GOOD_HEADER_IPV6 + GOOD_TERM_11, self.naming),
        EXP_INFO)
    # echo-reply = icmp-type code 129
    expected = 'permit icmp any any 129'
    self.assertIn(expected, str(acl), str(acl))
    # destination-unreachable = icmp-type code 1
    expected = 'permit icmp any any 1'
    self.assertIn(expected, str(acl), str(acl))
    # time-exceeded = icmp-type code 3
    expected = 'permit icmp any any 3'
    self.assertIn(expected, str(acl), str(acl))


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


# tests/lib/ciscoxr_test.py

# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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.
"""Unittest for Cisco XR acl rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import ciscoxr
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy


GOOD_HEADER_1 = """
header {
  comment:: "this is a test acl"
  target:: ciscoxr test-filter
}
"""

GOOD_HEADER_2 = """
header {
  comment:: "this is a test ipv6 acl"
  target:: ciscoxr ipv6-test-filter inet6
}
"""

OBJECT_GROUP_HEADER = """
header {
  target:: ciscoxr foo object-group
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  source-address:: SOME_HOST
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  protocol:: tcp
  destination-address:: SOME_HOST2
  source-port:: HTTP
  action:: accept
}
"""

GOOD_TERM_3 = """
term good-term-3 {
  protocol:: tcp
  destination-address:: SOME_HOST2
  source-port:: HTTP
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_4 = """
term good-term-4 {
  source-address:: SOME_HOST2
  action:: accept
}
"""

GOOD_TERM_5 = """
term good-term-5 {
  next-ip:: TEST_NEXT
}
"""

GOOD_TERM_6 = """
term good-term-6 {
  action:: accept
  next-ip:: TEST_NEXT
}
"""

VERBATIM_TERM = """
term verb_term {
  verbatim:: ciscoxr " permit tcp any"
}
"""
EXPIRED_TERM = """
term is_expired {
  expiration:: 2001-01-01
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'address',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'dscp_match',
    'expiration',
    'icmp_code',
    'icmp_type',
    'next_ip',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'owner',
    'platform',
    'platform_exclude',
    'protocol',
    'restrict_address_family',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next',
               'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'tcp-established',
               'is-fragment',
               'fragments'}
}

EXP_INFO = 2


class CiscoXRTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testRemark(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]

    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                             self.naming)
    acl = ciscoxr.CiscoXR(pol, EXP_INFO)
    expected = 'remark this is a test acl'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = 'remark good-term-1'
    self.assertIn(expected, str(acl), str(acl))
    expected = 'test-filter remark'
    self.assertNotIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testStandardTermHost(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]

    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1 + GOOD_TERM_4,
                             self.naming)
    acl = ciscoxr.CiscoXR(pol, EXP_INFO)
    expected = 'ipv4 access-list test-filter'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit icmp host 10.1.1.1 any'
    self.assertIn(expected, str(acl), str(acl))
    expected = ' permit ipv4 host 10.1.1.1 any'
    self.assertIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_has_calls([mock.call('SOME_HOST'),
                                             mock.call('SOME_HOST2')])

  def testStandardTermHostIPv6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001::3/128')]
    self.naming.GetServiceByProto.return_value = ['80']

    pol = policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_2 + GOOD_TERM_4,
                             self.naming)
    acl = ciscoxr.CiscoXR(pol, EXP_INFO)
    expected = 'ipv6 access-list ipv6-test-filter'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit tcp any eq 80 host 2001::3'
    self.assertIn(expected, str(acl), str(acl))
    expected = ' permit ipv6 host 2001::3 any'
    self.assertIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_has_calls([mock.call('SOME_HOST2'),
                                             mock.call('SOME_HOST2')])
    self.naming.GetServiceByProto.assert_called_once_with('HTTP', 'tcp')

  def testAclBasedForwardingIPv4(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]

    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_5, self.naming)
    acl = ciscoxr.CiscoXR(pol, EXP_INFO)
    expected = 'ipv4 access-list test-filter'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit ipv4 any any nexthop1 ipv4 10.1.1.1'
    self.assertIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_has_calls([mock.call('TEST_NEXT')])

  def testAclBasedForwardingIPv6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001::3/128')]

    pol = policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_5, self.naming)
    acl = ciscoxr.CiscoXR(pol, EXP_INFO)
    expected = 'ipv6 access-list ipv6-test-filter'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit ipv6 any any nexthop1 ipv6 2001::3'
    self.assertIn(expected, str(acl), str(acl))

    self.naming.GetNetAddr.assert_has_calls([mock.call('TEST_NEXT')])

  def testAclBasedForwardingMultipleIP(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IP('10.1.1.0/32'),
        nacaddr.IP('10.1.1.1/32')
    ]

    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_5, self.naming)
    acl = ciscoxr.CiscoXR(pol, EXP_INFO)
    self.assertRaises(ciscoxr.cisco.CiscoNextIpError, str, acl)

    self.naming.GetNetAddr.assert_has_calls([mock.call('TEST_NEXT')])

  def testAclBasedForwardingNetworkIP(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.0/31')]

    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_5, self.naming)
    acl = ciscoxr.CiscoXR(pol, EXP_INFO)
    self.assertRaises(ciscoxr.cisco.CiscoNextIpError, str, acl)

    self.naming.GetNetAddr.assert_has_calls([mock.call('TEST_NEXT')])

  def testAclBasedForwardingNotIP(self):
    self.naming.GetNetAddr.return_value = ['not_ip_address']

    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_5, self.naming)
    acl = ciscoxr.CiscoXR(pol, EXP_INFO)
    self.assertRaises(ciscoxr.cisco.CiscoNextIpError, str, acl)

    self.naming.GetNetAddr.assert_has_calls([mock.call('TEST_NEXT')])

  def testAclBasedForwardingActionAcceptNextIpIgnored(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]

    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_6, self.naming)
    acl = ciscoxr.CiscoXR(pol, EXP_INFO)
    expected = 'ipv4 access-list test-filter'
    self.assertIn(expected, str(acl), '[%s]' % str(acl))
    expected = ' permit ipv4 any any'
    self.assertIn(expected, str(acl), str(acl))
    expected = 'nexthop1'
    self.assertNotIn(expected, str(acl), str(acl))

  def testBuildTokens(self):
    pol1 = ciscoxr.CiscoXR(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                              self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001::3/128')]
    self.naming.GetServiceByProto.return_value = ['80']

    pol1 = ciscoxr.CiscoXR(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_3,
                                              self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testVerbatimObjectGroup(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]
    pol = policy.ParsePolicy(OBJECT_GROUP_HEADER + VERBATIM_TERM, self.naming)
    acl = ciscoxr.CiscoXR(pol, EXP_INFO)
    self.assertIn('permit tcp any', str(acl))


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


# tests/lib/cloudarmor_test.py

"""Tests for google3.third_party.py.capirca.lib.cloudarmor."""

import json
import random
from absl.testing import absltest
from unittest import mock

from capirca.lib import cloudarmor
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'priority',
    'source_address'
}

EXP_INFO = 2

GOOD_HEADER = """
header {
  comment:: "Test ACL for CloudArmor (IPv4)"
  target:: cloudarmor inet
}
"""

GOOD_HEADER_IPV6_ONLY = """
header {
  comment:: "Test ACL for CloudArmor (IPv6 only)"
  target:: cloudarmor inet6
}
"""

GOOD_HEADER_NOVERBOSE = """
header {
  comment:: "Test ACL for CloudArmor (IPv4)"
  target:: cloudarmor inet noverbose
}
"""

GOOD_HEADER_MIXED = """
header {
  comment:: "Test ACL for CloudArmor (IPv4 + IPv6)"
  target:: cloudarmor mixed
}
"""

GOOD_HEADER_NO_AF = """
header {
  comment:: "Test ACL for CloudArmor (Default AF = IPv4)"
  target:: cloudarmor
}
"""

BAD_HEADER_INVALID_AF = """
header {
  comment:: "Test ACL for CloudArmor (IPv4 + IPv6)"
  target:: cloudarmor inet8
}
"""

GOOD_TERM_ALLOW = """
term good-term-allow {
  comment:: "Sample CloudArmor Allow Rule"
  source-address:: GOOGLE_PUBLIC_DNS_ANYCAST
  action:: accept
}
"""
GOOD_TERM_DENY = """
term good-term-deny {
  comment:: "Sample Deny Rule"
  source-address:: INTERNAL
  action:: deny
}
"""

GOOD_TERM_NO_COMMENT = """
term good-term-nocomment {
  source-address:: GOOGLE_PUBLIC_DNS_ANYCAST
  action:: accept
}
"""

GOOD_TERM_DEFAULT_DENY = """
term good-term-defaultdeny {
  comment:: "Default Deny term"
  action:: deny
}

"""

GOOD_TERM_LARGE_COMMENT = """
term good-term-allow {
  comment:: "This is an unnecessarily long term comment that's going to be truncated"
  source-address:: GOOGLE_PUBLIC_DNS_ANYCAST
  action:: accept
}
"""

BAD_TERM_NO_ACTION = """
term bad-term-no-action {
  comment:: "Sample rule with missing 'action' attribute"
  source-address:: GOOGLE_PUBLIC_DNS_ANYCAST
}
"""


EXPECTED_IPV4_NOSPLIT_JSON = """
[
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule",
    "match": {
      "config": {
        "srcIpRanges": [
          "10.2.3.4/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule",
    "match": {
      "config": {
        "srcIpRanges": [
          "10.2.3.4/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 2
  }
]
"""

EXPECTED_IPV6_NOSPLIT_JSON = """
[
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule",
    "match": {
      "config": {
        "srcIpRanges": [
          "2001:4860:8000::5/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule",
    "match": {
      "config": {
        "srcIpRanges": [
          "2001:4860:8000::5/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 2
  }
]

"""

EXPECTED_MIXED_NOSPLIT_JSON = """
[
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule",
    "match": {
      "config": {
        "srcIpRanges": [
          "10.2.3.4/32",
          "2001:4860:8000::5/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule",
    "match": {
      "config": {
        "srcIpRanges": [
          "10.2.3.4/32",
          "2001:4860:8000::5/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 2
  }
]

"""

EXPECTED_IPV4_SPLIT_JSON = """
[
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule [1/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "5.2.3.2/32",
          "10.2.3.4/32",
          "23.2.3.3/32",
          "54.2.3.4/32",
          "76.2.3.5/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  },
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule [2/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "132.2.3.6/32",
          "197.2.3.7/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 2
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule [1/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "5.2.3.2/32",
          "10.2.3.4/32",
          "23.2.3.3/32",
          "54.2.3.4/32",
          "76.2.3.5/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 3
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule [2/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "132.2.3.6/32",
          "197.2.3.7/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 4
  }
]

"""

EXPECTED_IPV6_SPLIT_JSON = """
[
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule [1/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "2001:4860:8000::5/128",
          "24da:3ed8:32a0::7/128",
          "3051:abd2:5400::9/128",
          "577e:5400:3051::6/128",
          "6f5d:abd2:1403::1/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  },
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule [2/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "aee2:37ba:3cc0::3/128",
          "af22:32d2:3f00::2/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 2
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule [1/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "2001:4860:8000::5/128",
          "24da:3ed8:32a0::7/128",
          "3051:abd2:5400::9/128",
          "577e:5400:3051::6/128",
          "6f5d:abd2:1403::1/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 3
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule [2/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "aee2:37ba:3cc0::3/128",
          "af22:32d2:3f00::2/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 4
  }
]

"""

EXPECTED_MIXED_SPLIT_JSON = """
[
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule [1/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "5.2.3.2/32",
          "10.2.3.4/32",
          "23.2.3.3/32",
          "54.2.3.4/32",
          "76.2.3.5/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  },
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule [2/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "132.2.3.6/32",
          "197.2.3.7/32",
          "2001:4860:8000::5/128",
          "24da:3ed8:32a0::7/128",
          "3051:abd2:5400::9/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 2
  },
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule [3/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "577e:5400:3051::6/128",
          "6f5d:abd2:1403::1/128",
          "aee2:37ba:3cc0::3/128",
          "af22:32d2:3f00::2/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 3
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule [1/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "5.2.3.2/32",
          "10.2.3.4/32",
          "23.2.3.3/32",
          "54.2.3.4/32",
          "76.2.3.5/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 4
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule [2/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "132.2.3.6/32",
          "197.2.3.7/32",
          "2001:4860:8000::5/128",
          "24da:3ed8:32a0::7/128",
          "3051:abd2:5400::9/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 5
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule [3/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "577e:5400:3051::6/128",
          "6f5d:abd2:1403::1/128",
          "aee2:37ba:3cc0::3/128",
          "af22:32d2:3f00::2/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 6
  }
]

"""

EXPECTED_NOCOMMENT_SPLIT_JSON = """
[
  {
    "action": "allow",
    "description": " [1/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "5.2.3.2/32",
          "10.2.3.4/32",
          "23.2.3.3/32",
          "54.2.3.4/32",
          "76.2.3.5/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  },
  {
    "action": "allow",
    "description": " [2/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "132.2.3.6/32",
          "197.2.3.7/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 2
  }
]

"""

EXPECTED_NOCOMMENT_NOSPLIT_JSON = """
[
  {
    "action": "allow",
    "match": {
      "config": {
        "srcIpRanges": [
          "10.2.3.4/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  }
]
"""

EXPECTED_LARGECOMMENT_NOSPLIT_JSON = """
[
  {
    "action": "allow",
    "description": "This is an unnecessarily long term comment that's going to be tr",
    "match": {
      "config": {
        "srcIpRanges": [
          "10.2.3.4/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  }
]
"""

EXPECTED_LARGECOMMENT_SPLIT_JSON = """
[
  {
    "action": "allow",
    "description": "This is an unnecessarily long term comment that's going to [1/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "5.2.3.2/32",
          "10.2.3.4/32",
          "23.2.3.3/32",
          "54.2.3.4/32",
          "76.2.3.5/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  },
  {
    "action": "allow",
    "description": "This is an unnecessarily long term comment that's going to [2/2]",
    "match": {
      "config": {
        "srcIpRanges": [
          "132.2.3.6/32",
          "197.2.3.7/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 2
  }
]
"""

EXPECTED_DEFAULT_DENY_JSON = """
[
  {
    "action": "deny(404)",
    "description": "Default Deny term",
    "match": {
      "config": {
        "srcIpRanges": [
          "*"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  }
]
"""

EXPECTED_DEFAULT_DENY_SPLIT_JSON = """
[
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule [1/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "5.2.3.2/32",
          "10.2.3.4/32",
          "23.2.3.3/32",
          "54.2.3.4/32",
          "76.2.3.5/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 1
  },
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule [2/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "132.2.3.6/32",
          "197.2.3.7/32",
          "2001:4860:8000::5/128",
          "24da:3ed8:32a0::7/128",
          "3051:abd2:5400::9/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 2
  },
  {
    "action": "allow",
    "description": "Sample CloudArmor Allow Rule [3/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "577e:5400:3051::6/128",
          "6f5d:abd2:1403::1/128",
          "aee2:37ba:3cc0::3/128",
          "af22:32d2:3f00::2/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 3
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule [1/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "5.2.3.2/32",
          "10.2.3.4/32",
          "23.2.3.3/32",
          "54.2.3.4/32",
          "76.2.3.5/32"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 4
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule [2/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "132.2.3.6/32",
          "197.2.3.7/32",
          "2001:4860:8000::5/128",
          "24da:3ed8:32a0::7/128",
          "3051:abd2:5400::9/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 5
  },
  {
    "action": "deny(404)",
    "description": "Sample Deny Rule [3/3]",
    "match": {
      "config": {
        "srcIpRanges": [
          "577e:5400:3051::6/128",
          "6f5d:abd2:1403::1/128",
          "aee2:37ba:3cc0::3/128",
          "af22:32d2:3f00::2/128"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 6
  },
  {
    "action": "deny(404)",
    "description": "Default Deny term",
    "match": {
      "config": {
        "srcIpRanges": [
          "*"
        ]
      },
      "versionedExpr": "SRC_IPS_V1"
    },
    "preview": false,
    "priority": 7
  }
]
"""

TEST_IPS_NOSPLIT = [nacaddr.IP('10.2.3.4/32'),
                    nacaddr.IP('2001:4860:8000::5/128')]

TEST_IPS_SPLIT = [nacaddr.IP('10.2.3.4/32'),
                  nacaddr.IP('5.2.3.2/32'),
                  nacaddr.IP('23.2.3.3/32'),
                  nacaddr.IP('54.2.3.4/32'),
                  nacaddr.IP('76.2.3.5/32'),
                  nacaddr.IP('132.2.3.6/32'),
                  nacaddr.IP('197.2.3.7/32'),
                  nacaddr.IP('2001:4860:8000::5/128'),
                  nacaddr.IP('3051:abd2:5400::9/128'),
                  nacaddr.IP('aee2:37ba:3cc0::3/128'),
                  nacaddr.IP('6f5d:abd2:1403::1/128'),
                  nacaddr.IP('577e:5400:3051::6/128'),
                  nacaddr.IP('af22:32d2:3f00::2/128'),
                  nacaddr.IP('24da:3ed8:32a0::7/128')]


class CloudArmorTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def _StripAclHeaders(self, acl):
    return '\n'.join([line for line in str(acl).split('\n')
                      if not line.lstrip().startswith('#')])

  def testGenericIPv4Term(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_NOSPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_ALLOW + GOOD_TERM_DENY,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_IPV4_NOSPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testGenericIPv6Term(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_NOSPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(
            GOOD_HEADER_IPV6_ONLY + GOOD_TERM_ALLOW + GOOD_TERM_DENY,
            self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_IPV6_NOSPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testGenericMixedTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_NOSPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_ALLOW + GOOD_TERM_DENY,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_MIXED_NOSPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testDefaultAddressFamily(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_NOSPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER_NO_AF + GOOD_TERM_ALLOW + GOOD_TERM_DENY,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_IPV4_NOSPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testIPv4TermSplitting(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_SPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_ALLOW + GOOD_TERM_DENY,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_IPV4_SPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testIPv6TermSplitting(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_SPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(
            GOOD_HEADER_IPV6_ONLY + GOOD_TERM_ALLOW + GOOD_TERM_DENY,
            self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_IPV6_SPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testMixedTermSplitting(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_SPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_ALLOW + GOOD_TERM_DENY,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_MIXED_SPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testInvalidAddressFamilyCheck(self):

    self.naming.GetNetAddr.return_value = TEST_IPS_NOSPLIT

    self.assertRaisesRegex(
        cloudarmor.UnsupportedFilterTypeError,
        "'inet8' is not a valid filter type",
        cloudarmor.CloudArmor,
        policy.ParsePolicy(
            BAD_HEADER_INVALID_AF + GOOD_TERM_ALLOW, self.naming),
        EXP_INFO)

  def testMaxRuleLimitEnforcement(self):
    test_1001_ips_list = []

    for _ in range(1001):
      random_ip_octets = []
      for _ in range(4):
        random_ip_octets.append(str(int(random.randint(1, 255))))
      rand_ip = '.'.join(random_ip_octets)
      test_1001_ips_list.append(nacaddr.IP(rand_ip + '/32'))

    self.naming.GetNetAddr.return_value = test_1001_ips_list

    self.assertRaisesRegex(
        cloudarmor.ExceededMaxTermsError,
        'Exceeded maximum number of rules in a single policy | MAX = 200',
        cloudarmor.CloudArmor,
        policy.ParsePolicy(
            GOOD_HEADER + GOOD_TERM_ALLOW, self.naming),
        EXP_INFO)

  def testNoCommentWithSplit(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_SPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_NO_COMMENT, self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_NOCOMMENT_SPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testNoCommentWithoutSplit(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_NOSPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_NO_COMMENT, self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_NOCOMMENT_NOSPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testLargeCommentWithSplit(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_SPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_LARGE_COMMENT, self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_LARGECOMMENT_SPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testLargeCommentWithoutSplit(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_NOSPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_LARGE_COMMENT, self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_LARGECOMMENT_NOSPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testNoVerbose(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_NOSPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER_NOVERBOSE + GOOD_TERM_LARGE_COMMENT,
                           self.naming), EXP_INFO)
    self.assertNotIn('description', str(acl))

  def testDefaultDenyStandalone(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_NOSPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_DEFAULT_DENY,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_DEFAULT_DENY_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testDefaultDenyWithSplit(self):
    self.naming.GetNetAddr.return_value = TEST_IPS_SPLIT

    acl = cloudarmor.CloudArmor(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_ALLOW
                           + GOOD_TERM_DENY + GOOD_TERM_DEFAULT_DENY,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_DEFAULT_DENY_SPLIT_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

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


# tests/lib/gce_test.py

# Copyright 2015 Google Inc. All Rights Reserved.
#
# 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.
"""Unittest for GCE firewall rendering module."""

import json
from unittest import mock
from absl.testing import absltest

from absl.testing import parameterized
from capirca.lib import aclgenerator
from capirca.lib import gce
from capirca.lib import gcp
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER = """
header {
  comment:: "The general policy comment."
  target:: gce global/networks/default
}
"""

GOOD_HEADER_INGRESS = """
header {
  comment:: "The general policy comment."
  target:: gce INGRESS
}
"""

GOOD_HEADER_EGRESS = """
header {
  comment:: "The general policy comment."
  target:: gce EGRESS
}
"""

GOOD_HEADER_NO_NETWORK = """
header {
  comment:: "The general policy comment."
  target:: gce
}
"""

GOOD_HEADER_MAX_ATTRIBUTE_COUNT = """
header {
  comment:: "The general policy comment."
  target:: gce INGRESS global/networks/default 2
}
"""

GOOD_HEADER_INET = """
header {
  comment:: "The general policy comment."
  target:: gce INGRESS inet
}
"""

GOOD_HEADER_EGRESS_INET = """
header {
  comment:: "The general policy comment."
  target:: gce INGRESS inet
}
"""

GOOD_HEADER_INET6 = """
header {
  comment:: "The general policy comment."
  target:: gce INGRESS inet6
}
"""

GOOD_HEADER_EGRESS_INET6 = """
header {
  comment:: "The general policy comment."
  target:: gce EGRESS inet6
}
"""

GOOD_HEADER_MIXED = """
header {
  comment:: "The general policy comment."
  target:: gce INGRESS mixed
}
"""

GOOD_HEADER_EGRESS_MIXED = """
header {
  comment:: "The general policy comment."
  target:: gce EGRESS mixed
}
"""

GOOD_TERM = """
term good-term-1 {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_3 = """
term good-term-1 {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  priority:: 1
  action:: accept
}
"""

GOOD_TERM_EXCLUDE = """
term good-term-1 {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  source-exclude:: GUEST_WIRELESS_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_4 = """
term good-term-1 {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  comment:: "ICMP from IP."
  source-address:: CORP_EXTERNAL
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_EGRESS = """
term good-term-1 {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_EGRESS_SOURCETAG = """
term good-term-1 {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  source-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_INGRESS_SOURCETAG = """
term good-term-1 {
  comment:: "Allow all GCE network internal traffic."
  source-tag:: internal-servers
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_INGRESS_ADDRESS_SOURCETAG = """
term good-term-1 {
  comment:: "Allow all GCE network internal traffic."
  source-tag:: internal-servers
  source-address:: CORP_EXTERNAL
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_PLATFORM_EXCLUDE_TERM = """
term good-platform-exclude-term {
  comment:: "DNS access from corp."
  destination-tag:: dns-servers
  protocol:: udp tcp
  action:: accept
  platform-exclude:: gce
}
"""

GOOD_PLATFORM_TERM = """
term good-platform-term {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
  platform:: gce
}
"""

GOOD_TERM_JSON = """
[
  {
    "name": "default-good-term-1",
    "sourceRanges": [
      "10.2.3.4/32"
    ],
    "allowed": [
      {
        "ports": [
          "53"
        ],
        "IPProtocol": "udp"
      },
      {
        "ports": [
          "53"
        ],
        "IPProtocol": "tcp"
      }
    ],
    "description": "DNS access from corp.",
    "targetTags": [
      "dns-servers"
    ],
    "direction": "INGRESS",
    "network": "global/networks/default"
  }
]
"""

GOOD_TERM_NO_NETWORK_JSON = """
[
  {
    "name": "good-term-1",
    "sourceRanges": [
      "10.2.3.4/32"
    ],
    "allowed": [
      {
        "ports": [
          "53"
        ],
        "IPProtocol": "udp"
      },
      {
        "ports": [
          "53"
        ],
        "IPProtocol": "tcp"
      }
    ],
    "description": "DNS access from corp.",
    "direction": "INGRESS",
    "targetTags": [
      "dns-servers"
    ]
  }
]
"""

GOOD_TERM_EXPIRED = """
term good-term-expired {
  comment:: "Management access from corp."
  expiration:: 2001-01-01
  source-address:: CORP_EXTERNAL
  destination-tag:: ssh-servers
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_LOGGING = """
term good-term-logging {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
  logging:: true
}
"""

GOOD_TERM_CUSTOM_NAME = """
term %s {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_OWNERS = """
term good-term-owners {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  owner:: test-owner
  action:: accept
}
"""

GOOD_TERM_ICMP = """
term good-term-ping {
  comment:: "Good term."
  source-address:: CORP_EXTERNAL
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_ICMPV6 = """
term good-term-pingv6 {
  comment:: "Good term."
  source-address:: CORP_EXTERNAL
  protocol:: icmpv6
  action:: accept
}
"""

GOOD_TERM_IGMP = """
term good-term-igmp {
  comment:: "Good term."
  source-address:: CORP_EXTERNAL
  protocol:: igmp
  action:: accept
}
"""

GOOD_TERM_NO_PROTOCOL = """
term good-term-no-protocol {
  comment:: "Good term."
  source-address:: CORP_EXTERNAL
  action:: accept
}
"""

GOOD_TERM_INGRESS_SOURCE_SERVICE_ACCOUNT = """
term good-term-source-service-account {
  comment:: "Test with a service account."
  source-address:: CORP_EXTERNAL
  source-service-accounts:: acct@blah.com
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_INGRESS_TARGET_SERVICE_ACCOUNT = """
term good-term-target-service-account {
  comment:: "Test with a service account."
  source-address:: CORP_EXTERNAL
  target-service-accounts:: acct@blah.com
  protocol:: udp tcp
  action:: accept
}
"""

BAD_TERM_NO_SOURCE = """
term bad-term-no-source {
  comment:: "Management access from corp."
  destination-tag:: ssh-servers
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_SOURCE_EXCLUDE_ONLY = """
term bad-term-source-ex-only {
  comment:: "Management access from corp."
  destination-port:: SSH
  source-tag:: ssh-bastion
  source-exclude:: GUEST_WIRELESS_EXTERNAL
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_SOURCE_PORT = """
term bad-term-source-port {
  comment:: "Management access from corp."
  source-address:: CORP_EXTERNAL
  source-port:: SSH
  destination-tag:: ssh-servers
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_NAME_TOO_LONG = """
term good-term-whith-a-name-which-is-way-way-too-long-for-gce-to-accept {
  comment:: "Management access from corp."
  source-address:: CORP_EXTERNAL
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_UNSUPPORTED_PORT = """
term good-term-unsupported-port {
  comment:: "Management access from corp."
  source-address:: CORP_EXTERNAL
  destination-port:: SSH
  protocol:: tcp icmp
  action:: accept
}
"""

BAD_TERM_UNSUPPORTED_OPTION = """
term bad-term-unsupported-option {
  comment:: "Management access from corp."
  source-address:: CORP_EXTERNAL
  destination-port:: SSH
  protocol:: tcp
  action:: accept
  option:: tcp-initial
}
"""

BAD_TERM_EGRESS = """
term bad-term-dest-tag {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

BAD_TERM_EGRESS_SOURCE_ADDRESS = """
term bad-term-source-address {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  source-address:: CORP_EXTERNAL
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

BAD_TERM_EGRESS_SOURCE_DEST_TAG = """
term bad-term-source-dest-tag {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  source-tag:: ssh-bastion
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

BAD_TERM_PORTS_COUNT = """
term bad-term-ports-count {
  comment:: "This term has way too many ports."
  source-address:: CORP_EXTERNAL
  source-tag:: ssh-bastion
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

SAMPLE_TAG = 'ssh-bastions '

BAD_TERM_SOURCE_TAGS_COUNT = """
term bad-term-source-tags-count {{
  comment:: "This term has way too many source tags."
  protocol:: tcp
  action:: accept
  source-tag:: {many_source_tags}
}}""".format(many_source_tags=SAMPLE_TAG *
             (gce.Term._TERM_SOURCE_TAGS_LIMIT + 1))

BAD_TERM_TARGET_TAGS_COUNT = """
term bad-term-target-tags-count {{
  comment:: "This term has way too many target tags."
  source-address:: CORP_EXTERNAL
  protocol:: tcp
  action:: accept
  destination-tag:: {many_target_tags}
}}""".format(many_target_tags=SAMPLE_TAG *
             (gce.Term._TERM_TARGET_TAGS_LIMIT + 1))

BAD_TERM_SERVICE_ACCOUNTS_COUNT = """
term bad-term-service-accounts-count {{
  comment:: "This term has way too many source service accounts."
  protocol:: tcp
  action:: accept
  source-tag:: ssh-bastion
  source-service-accounts:: {many_service_accounts}
}}""".format(many_service_accounts='acct1@blah.com ' *
             (gce.Term._TERM_SERVICE_ACCOUNTS_LIMIT + 1))

BAD_TERM_TARGET_TAGS_AND_SERVICE_ACCOUNTS = """
term bad-term-tags-and-service-accounts {
  comment:: "This term has both a tag and a target service account."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  protocol:: tcp
  action:: accept
  target-service-accounts:: acct1@blah.com
}
"""

BAD_TERM_TARGET_TAGS_AND_SERVICE_ACCOUNTS2 = """
term bad-term-tags-and-service-accounts {
  comment:: "This term has both a tag and a source service account."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  protocol:: tcp
  action:: accept
  source-service-accounts:: acct1@blah.com
}
"""

GOOD_TERM_EXCLUDE_RANGE = """
[
  {
    "name": "default-good-term-1",
    "sourceRanges": [
      "10.128.0.0/10",
      "10.192.0.0/11",
      "10.224.0.0/12",
      "10.241.0.0/16",
      "10.242.0.0/15",
      "10.244.0.0/14",
      "10.248.0.0/13"
    ],
    "allowed": [
      {
        "ports": [
          "53"
        ],
        "IPProtocol": "udp"
      },
      {
        "ports": [
          "53"
        ],
        "IPProtocol": "tcp"
      }
    ],
    "description": "DNS access from corp.",
    "direction": "INGRESS",
    "targetTags": [
      "dns-servers"
    ],
    "network": "global/networks/default"
  }
]
"""

DEFAULT_DENY = """
term default-deny {
  comment:: "default_deny."
  action:: deny
}
"""

GOOD_TERM_DENY = """
term good-term-1 {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  protocol:: udp tcp
  action:: deny
}
"""

GOOD_TERM_DENY_EXPECTED = """[
  {
    "denied": [
      {
        "IPProtocol": "udp"
      },
      {
        "IPProtocol": "tcp"
      }
    ],
    "description": "DNS access from corp.",
    "name": "default-good-term-1",
    "network": "global/networks/default",
    "sourceRanges": [
      "10.2.3.4/32"
    ],
    "direction": "INGRESS",
    "targetTags": [
      "dns-servers"
    ]
  }
]
"""

VALID_TERM_NAMES = [
    'icmp', 'gcp-to-gcp', 'accept-ssh-from-google', 'ndc-rampart', 'lab-syslog',
    'windows-windows', 'shell-wmn-inbound', 'shell-internal-smtp',
    'accept-internal-traffic', 'deepfield-lab-management',
    'deepfield-lab-reverse-proxy', 'cr-proxy-replication',
    'ciena-one-control-tcp', 'fms-prod-to-fms-prod', 'ast', 'default-deny',
    'google-web', 'zo6hmxkfibardh6tgbiy7ua6'
]

SUPPORTED_TOKENS = frozenset({
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'destination_tag',
    'expiration',
    'stateless_reply',
    'name',
    'option',
    'owner',
    'priority',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'source_service_accounts',
    'source_tag',
    'target_service_accounts',
    'translated',
    'platform',
    'platform_exclude',
})

SUPPORTED_SUB_TOKENS = {'action': {'accept', 'deny'}}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2

TEST_IPS = [
    nacaddr.IP('10.2.3.4/32'),
    nacaddr.IP('2001:4860:8000::5/128'),
    nacaddr.IP('::ffff:a02:301/128'),  # IPV4-mapped
    nacaddr.IP('2002::/16'),  # 6to4
    nacaddr.IP('::0000:a02:301/128'),  # IPv4-compatible
]

TEST_INCLUDE_IPS = [nacaddr.IP('10.2.3.4/32'), nacaddr.IP('10.4.3.2/32')]

TEST_EXCLUDE_IPS = [nacaddr.IP('10.4.3.2/32')]

TEST_INCLUDE_RANGE = [nacaddr.IP('10.128.0.0/9')]

TEST_EXCLUDE_RANGE = [nacaddr.IP('10.240.0.0/16')]

ANY_IPS = [nacaddr.IP('0.0.0.0/0'), nacaddr.IP('::/0')]

TEST_IPV4_ONLY = [nacaddr.IP('10.2.3.4/32')]

TEST_IPV6_ONLY = [nacaddr.IP('2001:4860:8000::5/128')]

_TERM_SOURCE_TAGS_LIMIT = 30
_TERM_TARGET_TAGS_LIMIT = 70
_TERM_PORTS_LIMIT = 256


class GCETest(parameterized.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def _StripAclHeaders(self, acl):
    return '\n'.join([
        line for line in str(acl).split('\n')
        if not line.lstrip().startswith('#')
    ])

  def testGenericTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    expected = json.loads(GOOD_TERM_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testTermWithPriority(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_3, self.naming), EXP_INFO)
    self.assertIn('"priority": "1",', str(acl), str(acl))

  def testTermWithLogging(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_LOGGING, self.naming),
        EXP_INFO)
    rendered_acl = json.loads(str(acl))[0]
    self.assertIn('logConfig', rendered_acl)
    self.assertEqual(rendered_acl['logConfig'], {'enable': True})

  def testGenericTermWithoutNetwork(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_NO_NETWORK + GOOD_TERM, self.naming),
        EXP_INFO)
    expected = json.loads(GOOD_TERM_NO_NETWORK_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testGenericTermWithExclude(self):
    self.naming.GetNetAddr.side_effect = [TEST_INCLUDE_IPS, TEST_EXCLUDE_IPS]
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_EXCLUDE, self.naming),
        EXP_INFO)
    expected = json.loads(GOOD_TERM_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('CORP_EXTERNAL'),
         mock.call('GUEST_WIRELESS_EXTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testGenericTermWithExcludeRange(self):
    self.naming.GetNetAddr.side_effect = [
        TEST_INCLUDE_RANGE, TEST_EXCLUDE_RANGE
    ]
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_EXCLUDE, self.naming),
        EXP_INFO)
    expected = json.loads(GOOD_TERM_EXCLUDE_RANGE)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('CORP_EXTERNAL'),
         mock.call('GUEST_WIRELESS_EXTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testSkipExpiredTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_EXPIRED, self.naming),
        EXP_INFO)
    self.assertEqual(self._StripAclHeaders(str(acl)), '[]\n\n')

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testSkipStatelessReply(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    # Add stateless_reply to terms, there is no current way to include it in the
    # term definition.
    ret = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming)
    _, terms = ret.filters[0]
    for term in terms:
      term.stateless_reply = True

    acl = gce.GCE(ret, EXP_INFO)
    self.assertEqual(self._StripAclHeaders(str(acl)), '[]\n\n')

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testSourceNetworkSplit(self):
    lots_of_ips = []
    for i in range(20):
      for j in range(20):
        lots_of_ips.append(nacaddr.IP('10.%d.%d.1/32' % (i, j)))
    self.naming.GetNetAddr.return_value = lots_of_ips
    self.naming.GetServiceByProto.return_value = ['53']

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    self.assertIn('default-good-term-1-1', str(acl))
    self.assertIn('default-good-term-1-2', str(acl))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testRaisesWithoutSource(self):
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaisesRegex(
        gce.GceFirewallError,
        'Ingress rule missing required field oneof "sourceRanges" or "sourceTags.',
        gce.GCE,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_NO_SOURCE,
                           self.naming), EXP_INFO)

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testRaisesWithOnlySourceExclusion(self):
    self.naming.GetNetAddr.return_value = TEST_EXCLUDE_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaisesRegex(
        gce.GceFirewallError,
        ('GCE firewall does not support address exclusions without a source '
         'address list.'), gce.GCE,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_SOURCE_EXCLUDE_ONLY,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('GUEST_WIRELESS_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testRaisesNoSourceAfterExclude(self):
    self.naming.GetNetAddr.side_effect = [TEST_INCLUDE_IPS, TEST_INCLUDE_IPS]
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    self.assertRaisesRegex(
        gce.GceFirewallError,
        ('GCE firewall rule no longer contains any source addresses after '
         'the prefixes in source_address_exclude were removed.'), gce.GCE,
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_EXCLUDE,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('CORP_EXTERNAL'),
         mock.call('GUEST_WIRELESS_EXTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testRaisesWithSourcePort(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaisesRegex(
        gce.GceFirewallError,
        'GCE firewall does not support source port restrictions.', gce.GCE,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_SOURCE_PORT,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testRaisesWithLongTermName(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaises(
        aclgenerator.TermNameTooLongError, gce.GCE,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_NAME_TOO_LONG, self.naming),
        EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testRaisesWithUnsupportedOption(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaisesRegex(
        gce.GceFirewallError, 'GCE firewall does not support term options.',
        gce.GCE,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_UNSUPPORTED_OPTION,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testBuildTokens(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    pol1 = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    pol1 = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_2, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testDenyAction(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_DENY, self.naming), EXP_INFO)
    expected = json.loads(GOOD_TERM_DENY_EXPECTED)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testIngress(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INGRESS + GOOD_TERM, self.naming),
        EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))

  def testEgress(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM_EGRESS, self.naming),
        EXP_INFO)
    self.assertIn('EGRESS', str(acl))
    self.assertIn('good-term-1-e', str(acl))
    self.assertNotIn('INGRESS', str(acl))

  def testRaisesWithEgressDestinationTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    self.assertRaisesRegex(
        gce.GceFirewallError, 'GCE Egress rule cannot have destination tag.',
        gce.GCE,
        policy.ParsePolicy(GOOD_HEADER_EGRESS + BAD_TERM_EGRESS,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testRaisesWithEgressSourceAddress(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    self.assertRaisesRegex(
        gce.GceFirewallError, 'Egress rules cannot include "sourceRanges".',
        gce.GCE,
        policy.ParsePolicy(GOOD_HEADER_EGRESS + BAD_TERM_EGRESS_SOURCE_ADDRESS,
                           self.naming), EXP_INFO)

    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testRaisesWithEgressSourceAndDestTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    self.assertRaisesRegex(
        gce.GceFirewallError, 'GCE Egress rule cannot have destination tag.',
        gce.GCE,
        policy.ParsePolicy(GOOD_HEADER_EGRESS + BAD_TERM_EGRESS_SOURCE_DEST_TAG,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testEgressTags(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM_EGRESS_SOURCETAG,
                           self.naming), EXP_INFO)

    self.assertIn('targetTags', str(acl))
    self.assertNotIn('sourceTags', str(acl))

  def testIngressTags(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INGRESS + GOOD_TERM_INGRESS_SOURCETAG,
                           self.naming), EXP_INFO)

    self.assertIn('sourceTags', str(acl))
    self.assertNotIn('targetTags', str(acl))

  def testSourceServiceAccounts(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_INGRESS + GOOD_TERM_INGRESS_SOURCE_SERVICE_ACCOUNT,
            self.naming), EXP_INFO)
    self.assertIn('sourceServiceAccounts', str(acl))
    self.assertNotIn('targetTags', str(acl))
    self.assertNotIn('sourceTags', str(acl))

  def testTargetServiceAccounts(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_INGRESS + GOOD_TERM_INGRESS_TARGET_SERVICE_ACCOUNT,
            self.naming), EXP_INFO)
    self.assertIn('targetServiceAccounts', str(acl))
    self.assertNotIn('targetTags', str(acl))
    self.assertNotIn('sourceTags', str(acl))

  def testDestinationRanges(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM_EGRESS, self.naming),
        EXP_INFO)
    self.assertIn('destinationRanges', str(acl), str(acl))
    self.assertNotIn('sourceRanges', str(acl), str(acl))
    self.assertIn('10.2.3.4/32', str(acl), str(acl))

  def testP4TagsNotPresent(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    self.assertNotIn('$Id:', str(acl))

  def testRaisesConflictingDirectionAddress(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaisesRegex(
        gce.GceFirewallError,
        'Ingress rule missing required field oneof "sourceRanges" or "sourceTags"',
        gce.GCE,
        policy.ParsePolicy(GOOD_HEADER_INGRESS + GOOD_TERM_4,
                           self.naming), EXP_INFO)
    self.assertRaisesRegex(
        gce.GceFirewallError, 'Egress rules cannot include "sourceRanges".',
        gce.GCE, policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM,
                                    self.naming), EXP_INFO)

  def testDefaultDenyEgressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM_EGRESS + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('"priority": 65534', str(acl))

  def testDefaultDenyIngressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_INGRESS + GOOD_TERM_INGRESS_SOURCETAG + DEFAULT_DENY,
            self.naming), EXP_INFO)
    self.assertIn('"priority": 65534', str(acl))

  def testValidTermNames(self):
    for name in VALID_TERM_NAMES:
      self.naming.GetNetAddr.return_value = TEST_IPS
      self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
      pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_CUSTOM_NAME % name,
                               self.naming)
      acl = gce.GCE(pol, EXP_INFO)
      self.assertIsNotNone(str(acl))

  def testInet(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))

  def testInet6(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM, self.naming),
        EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))

  def testInetWithV6AddressesOnly(self):
    self.naming.GetNetAddr.return_value = TEST_IPV6_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))

  def testInet6WithV4AddressesOnly(self):
    self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))

  def testFilterIPv4InIPv6FormatMixed(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('::ffff:a02:301/128', str(acl))
    self.assertNotIn('2002::/16', str(acl))
    self.assertNotIn('::0000:a02:301/128', str(acl))

  def testFilterIPv4InIPv6FormatInet6(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertNotIn('::ffff:a02:301/128', str(acl))
    self.assertNotIn('2002::/16', str(acl))
    self.assertNotIn('::0000:a02:301/128', str(acl))

  def testFilterIPv4InIPv6FormatInet(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('::ffff:a02:301/128', str(acl))
    self.assertNotIn('2002::/16', str(acl))
    self.assertNotIn('::0000:a02:301/128', str(acl))

  def testInetWithSourceTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('internal-servers', str(acl))

  def testInet6WithSourceTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET6 + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertIn('internal-servers', str(acl))

  def testInetWithSourceTagAndV6Addresses(self):
    self.naming.GetNetAddr.return_value = TEST_IPV6_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG +
            DEFAULT_DENY, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('internal-servers', str(acl))

  def testInet6WithSourceTagAndV4Addresses(self):
    self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET6 + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG +
            DEFAULT_DENY, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertNotIn('internal-servers', str(acl))

  def testInet6DefaultDenyEgressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_EGRESS_INET6 + GOOD_TERM_EGRESS + DEFAULT_DENY,
            self.naming), EXP_INFO)
    self.assertNotIn('INGRESS', str(acl))
    self.assertIn('EGRESS', str(acl))
    self.assertIn('"priority": 65534', str(acl))
    self.assertIn('::/0', str(acl))
    self.assertNotIn('0.0.0.0/0', str(acl))

  def testInet6DefaultDenyIngressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET6 + GOOD_TERM_INGRESS_SOURCETAG + DEFAULT_DENY,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('"priority": 65534', str(acl))
    self.assertIn('::/0', str(acl))
    self.assertNotIn('0.0.0.0/0', str(acl))

  def testIcmpInet(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM_ICMP, self.naming),
        EXP_INFO)
    self.assertIn('icmp', str(acl))
    self.assertNotIn('58', str(acl))

  def testIcmpv6Inet6(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM_ICMPV6, self.naming),
        EXP_INFO)
    self.assertIn('58', str(acl))
    self.assertNotIn('icmp', str(acl))

  def testIcmpInet6(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM_ICMP, self.naming),
        EXP_INFO)
    self.assertNotIn('icmp', str(acl))

  def testIcmpv6Inet(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM_ICMPV6, self.naming),
        EXP_INFO)
    self.assertNotIn('58', str(acl))

  def testIgmpInet(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM_IGMP, self.naming),
        EXP_INFO)
    self.assertIn('2', str(acl))

  def testIgmpInet6(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM_IGMP, self.naming),
        EXP_INFO)
    self.assertNotIn('2', str(acl))

  def testPortsCountExceededError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = list(
        str(i) for i in range(1024, 1024 + (gce.Term._TERM_PORTS_LIMIT) * 3, 2))
    self.assertRaisesRegex(
        gce.GceFirewallError,
        'GCE firewall rule exceeded number of ports per rule: ' +
        'bad-term-ports-count', gce.GCE,
        policy.ParsePolicy(GOOD_HEADER_INET + BAD_TERM_PORTS_COUNT,
                           self.naming), EXP_INFO)
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testSourceTagCountExceededError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        gce.GceFirewallError,
        'GCE firewall rule exceeded number of source tags per rule: ' +
        'bad-term-source-tags-count', gce.GCE,
        policy.ParsePolicy(GOOD_HEADER_INET + BAD_TERM_SOURCE_TAGS_COUNT,
                           self.naming), EXP_INFO)

  def testTargetTagCountExceededError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        gce.GceFirewallError,
        'GCE firewall rule exceeded number of target tags per rule: ' +
        'bad-term-target-tags-count', gce.GCE,
        policy.ParsePolicy(GOOD_HEADER_INET + BAD_TERM_TARGET_TAGS_COUNT,
                           self.naming), EXP_INFO)

  def testServiceAccountCountExceededError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        gce.GceFirewallError,
        'GCE firewall rule exceeded number of service accounts per rule: ' +
        'bad-term-service-accounts-count', gce.GCE,
        policy.ParsePolicy(GOOD_HEADER_INET + BAD_TERM_SERVICE_ACCOUNTS_COUNT,
                           self.naming), EXP_INFO)

  def testTargetTagsAndSourceServiceAccountsError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        gce.GceFirewallError,
        'sourceServiceAccounts cannot be used at the same time as targetTags or sourceTags',
        gce.GCE,
        policy.ParsePolicy(
            GOOD_HEADER_INET + BAD_TERM_TARGET_TAGS_AND_SERVICE_ACCOUNTS2,
            self.naming), EXP_INFO)

  def testTargetTagsAndTargetServiceAccountsError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        gce.GceFirewallError,
        'targetServiceAccounts cannot be used at the same time as targetTags or sourceTags',
        gce.GCE,
        policy.ParsePolicy(
            GOOD_HEADER_INET + BAD_TERM_TARGET_TAGS_AND_SERVICE_ACCOUNTS,
            self.naming), EXP_INFO)

  def testMixed(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM, self.naming),
        EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))

  def testInetIsDefault(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))

  def testMixedWithV6AddressesOnly(self):
    self.naming.GetNetAddr.return_value = TEST_IPV6_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))

  def testMixedWithV4AddressesOnly(self):
    self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))

  def testMixedIsSeparateRules(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertIn('good-term-1', str(acl))
    self.assertIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testMixedWithSourceTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('internal-servers', str(acl))
    self.assertIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testMixedWithSourceTagOnly(self):
    self.naming.GetNetAddr.return_value = []
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('internal-servers', str(acl))
    self.assertNotIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testMixedWithSourceTagAndV6Addresses(self):
    self.naming.GetNetAddr.return_value = TEST_IPV6_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG +
            DEFAULT_DENY, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('internal-servers', str(acl))
    self.assertIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testMixedWithSourceTagAndV4Addresses(self):
    self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG +
            DEFAULT_DENY, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertIn('internal-servers', str(acl))
    self.assertNotIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testMixedWithEgressSourceTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_EGRESS_MIXED + GOOD_TERM_EGRESS_SOURCETAG, self.naming),
        EXP_INFO)
    self.assertNotIn('INGRESS', str(acl))
    self.assertIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('dns-servers', str(acl))
    self.assertIn(gcp.GetIpv6TermName('good-term-1-e'), str(acl))

  def testMixedDefaultDenyEgressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_EGRESS_MIXED + GOOD_TERM_EGRESS + DEFAULT_DENY,
            self.naming), EXP_INFO)
    self.assertNotIn('INGRESS', str(acl))
    self.assertIn('EGRESS', str(acl))
    self.assertIn('"priority": 65534', str(acl))
    self.assertIn('default-deny-e', str(acl))
    self.assertIn(gcp.GetIpv6TermName('default-deny-e'), str(acl))
    self.assertIn('::/0', str(acl))
    self.assertIn('0.0.0.0/0', str(acl))

  def testMixedDefaultDenyIngressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_SOURCETAG + DEFAULT_DENY,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('"priority": 65534', str(acl))
    self.assertIn('default-deny', str(acl))
    self.assertIn(gcp.GetIpv6TermName('default-deny'), str(acl))
    self.assertIn('::/0', str(acl))
    self.assertIn('0.0.0.0/0', str(acl))

  def testIcmpMixed(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_ICMP, self.naming),
        EXP_INFO)
    self.assertIn('icmp', str(acl))
    self.assertNotIn('58', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testIcmpv6Mixed(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_ICMPV6, self.naming),
        EXP_INFO)
    self.assertIn('58', str(acl))
    self.assertNotIn('icmp', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn(gcp.GetIpv6TermName('good-term-pingv6'), str(acl))

  def testIgmpMixed(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_IGMP, self.naming),
        EXP_INFO)
    self.assertIn('2', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn(gcp.GetIpv6TermName('good-term-pingv6'), str(acl))

  def testNoProtocol(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_NO_PROTOCOL,
                           self.naming), EXP_INFO)
    self.assertIn('all', str(acl))

  def testPlatformExclude(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET + GOOD_PLATFORM_EXCLUDE_TERM + GOOD_TERM,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('good-term-1', str(acl))
    self.assertNotIn('good-platform-exclude-term', str(acl))

  def testPlatform(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_PLATFORM_TERM, self.naming),
        EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('good-platform-term', str(acl))

  def testTermOwners(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce.GCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_OWNERS, self.naming),
        EXP_INFO)
    rendered_acl = json.loads(str(acl))[0]
    self.assertEqual(rendered_acl['description'],
                     'DNS access from corp. Owner: test-owner')

  def testMaxAttributeExceeded(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    self.assertRaises(
        gce.ExceededAttributeCountError, gce.GCE,
        policy.ParsePolicy(
            GOOD_HEADER_MAX_ATTRIBUTE_COUNT + GOOD_TERM + DEFAULT_DENY,
            self.naming), EXP_INFO)

  def testMaxAttribute(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.2.3.4/32')]
    pol = policy.ParsePolicy(GOOD_HEADER_MAX_ATTRIBUTE_COUNT + GOOD_TERM_5,
                             self.naming)
    acl = gce.GCE(pol, EXP_INFO)
    self.assertIsNotNone(str(acl))

  @parameterized.named_parameters(('1 ip, 2 ports', {
      'sourceRanges': ['10.128.0.0/10'],
      'allowed': [{
          'ports': ['22'],
          'IPProtocol': 'tcp'
      }, {
          'ports': ['53'],
          'IPProtocol': 'udp'
      }],
  }, 5), ('1 ip, 2 ports, 1 target tag', {
      'sourceRanges': ['10.128.0.0/10'],
      'allowed': [{
          'ports': ['22'],
          'IPProtocol': 'tcp'
      }, {
          'ports': ['53'],
          'IPProtocol': 'udp'
      }],
      'targetTags': ['dns-servers'],
  }, 6), ('2 ips, 2 ports, 1 target tag', {
      'sourceRanges': ['10.128.0.0/10', '192.168.1.1/24'],
      'allowed': [{
          'ports': ['22'],
          'IPProtocol': 'tcp'
      }, {
          'ports': ['53'],
          'IPProtocol': 'udp'
      }],
      'targetTags': ['dns-servers'],
  }, 7), ('2 ips, 2 ports', {
      'sourceRanges': ['10.128.0.0/10', '192.168.1.1/24'],
      'allowed': [{
          'ports': ['22'],
          'IPProtocol': 'tcp'
      }, {
          'ports': ['53'],
          'IPProtocol': 'udp'
      }],
  }, 6), ('2 ips, 2 protocols', {
      'sourceRanges': ['10.128.0.0/10', '192.168.1.1/24'],
      'allowed': [{
          'IPProtocol': 'tcp'
      }, {
          'IPProtocol': 'udp'
      }],
  }, 4), ('1 ip, 2 protocols, 1 source tag', {
      'sourceRanges': ['10.128.0.0/10'],
      'allowed': [{
          'IPProtocol': 'tcp'
      }, {
          'IPProtocol': 'udp'
      }],
      'sourceTags': ['dns-servers'],
  }, 4), ('2 ips, 1 protocol', {
      'sourceRanges': ['10.128.0.0/10', '192.168.1.1/24'],
      'allowed': [{
          'IPProtocol': 'icmp'
      }],
  }, 3), ('1 ip, 2 protocols, 2 service accounts', {
      'sourceRanges': ['10.128.0.0/10'],
      'allowed': [{
          'IPProtocol': 'tcp'
      }, {
          'IPProtocol': 'udp'
      }],
      'sourceServiceAccounts': ['test@system.gserviceaccount.com'],
      'targetServiceAccounts': ['test@system.gserviceaccount.com'],
  }, 5))
  def testGetAttributeCount(self, dict_term, expected):
    self.assertEqual(gce.GetAttributeCount(dict_term), expected)

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


# tests/lib/gce_vpc_tf_test.py

# Copyright 2015 Google Inc. All Rights Reserved.
#
# 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.
"""Unittest for gce_vpc_tf firewall rendering module."""

import json
from unittest import mock
from absl.testing import absltest

from absl.testing import parameterized
from capirca.lib import aclgenerator
from capirca.lib import gce_vpc_tf
from capirca.lib import gcp
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER = """
header {
  comment:: "The general policy comment."
  target:: gce_vpc_tf global/networks/default
}
"""

GOOD_HEADER_INGRESS = """
header {
  comment:: "The general policy comment."
  target:: gce_vpc_tf INGRESS global/networks/default
}
"""

GOOD_HEADER_EGRESS = """
header {
  comment:: "The general policy comment."
  target:: gce_vpc_tf EGRESS global/networks/default
}
"""

GOOD_HEADER_NO_NETWORK = """
header {
  comment:: "The general policy comment."
  target:: gce_vpc_tf
}
"""

GOOD_HEADER_MAX_ATTRIBUTE_COUNT = """
header {
  comment:: "The general policy comment."
  target:: gce_vpc_tf INGRESS global/networks/default 2
}
"""

GOOD_HEADER_INET = """
header {
  comment:: "The general policy comment."
  target:: gce_vpc_tf INGRESS inet global/networks/default
}
"""

GOOD_HEADER_INET6 = """
header {
  comment:: "The general policy comment."
  target:: gce_vpc_tf INGRESS inet6 global/networks/default
}
"""

GOOD_HEADER_EGRESS_INET6 = """
header {
  comment:: "The general policy comment."
  target:: gce_vpc_tf EGRESS inet6 global/networks/default
}
"""

GOOD_HEADER_MIXED = """
header {
  comment:: "The general policy comment."
  target:: gce_vpc_tf INGRESS mixed global/networks/default
}
"""

GOOD_HEADER_EGRESS_MIXED = """
header {
  comment:: "The general policy comment."
  target:: gce_vpc_tf EGRESS mixed global/networks/default
}
"""

GOOD_TERM = """
term good-term-1 {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_3 = """
term good-term-1 {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  priority:: 1
  action:: accept
}
"""

GOOD_TERM_EXCLUDE = """
term good-term-1 {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  source-exclude:: GUEST_WIRELESS_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_4 = """
term good-term-1 {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  comment:: "ICMP from IP."
  source-address:: CORP_EXTERNAL
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_EGRESS = """
term good-term-1 {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_EGRESS_SOURCETAG = """
term good-term-1 {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  source-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_INGRESS_SOURCETAG = """
term good-term-1 {
  comment:: "Allow all GCE network internal traffic."
  source-tag:: internal-servers
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_INGRESS_ADDRESS_SOURCETAG = """
term good-term-1 {
  comment:: "Allow all GCE network internal traffic."
  source-tag:: internal-servers
  source-address:: CORP_EXTERNAL
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_PLATFORM_EXCLUDE_TERM = """
term good-platform-exclude-term {
  comment:: "DNS access from corp."
  destination-tag:: dns-servers
  protocol:: udp tcp
  action:: accept
  platform-exclude:: gce_vpc_tf
}
"""

GOOD_PLATFORM_TERM = """
term good-platform-term {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
  platform:: gce_vpc_tf
}
"""

GOOD_TERM_JSON = """
{
  "resource": {
    "google_compute_firewall": [
      {
        "default-good-term-1": {
          "name": "default-good-term-1",
          "source_ranges": [
            "10.2.3.4/32"
          ],
          "allow": [
            {
              "ports": [
                "53"
              ],
              "protocol": "udp"
            },
            {
              "ports": [
                "53"
              ],
              "protocol": "tcp"
            }
          ],
          "description": "DNS access from corp.",
          "target_tags": [
            "dns-servers"
          ],
          "direction": "INGRESS",
          "network": "global/networks/default",
          "priority": 1
        }
      }
    ]
  }
}
"""

GOOD_TERM_EXPIRED = """
term good-term-expired {
  comment:: "Management access from corp."
  expiration:: 2001-01-01
  source-address:: CORP_EXTERNAL
  destination-tag:: ssh-servers
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_LOGGING = """
term good-term-logging {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
  logging:: true
}
"""

GOOD_TERM_CUSTOM_NAME = """
term %s {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_OWNERS = """
term good-term-owners {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  owner:: test-owner
  action:: accept
}
"""

GOOD_TERM_ICMP = """
term good-term-ping {
  comment:: "Good term."
  source-address:: CORP_EXTERNAL
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_ICMPV6 = """
term good-term-pingv6 {
  comment:: "Good term."
  source-address:: CORP_EXTERNAL
  protocol:: icmpv6
  action:: accept
}
"""

GOOD_TERM_IGMP = """
term good-term-igmp {
  comment:: "Good term."
  source-address:: CORP_EXTERNAL
  protocol:: igmp
  action:: accept
}
"""

GOOD_TERM_NO_PROTOCOL = """
term good-term-no-protocol {
  comment:: "Good term."
  source-address:: CORP_EXTERNAL
  action:: accept
}
"""

GOOD_TERM_INGRESS_TARGET_SERVICE_ACCOUNT = """
term good-term-target-service-account {
  comment:: "Test with a service account."
  source-address:: CORP_EXTERNAL
  target-service-accounts:: acct@blah.com
  protocol:: udp tcp
  action:: accept
}
"""

BAD_TERM_NO_SOURCE = """
term bad-term-no-source {
  comment:: "Management access from corp."
  destination-tag:: ssh-servers
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_SOURCE_EXCLUDE_ONLY = """
term bad-term-source-ex-only {
  comment:: "Management access from corp."
  destination-port:: SSH
  source-tag:: ssh-bastion
  source-exclude:: GUEST_WIRELESS_EXTERNAL
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_SOURCE_PORT = """
term bad-term-source-port {
  comment:: "Management access from corp."
  source-address:: CORP_EXTERNAL
  source-port:: SSH
  destination-tag:: ssh-servers
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_NAME_TOO_LONG = """
term good-term-whith-a-name-which-is-way-way-too-long-for-gce-to-accept {
  comment:: "Management access from corp."
  source-address:: CORP_EXTERNAL
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_UNSUPPORTED_PORT = """
term good-term-unsupported-port {
  comment:: "Management access from corp."
  source-address:: CORP_EXTERNAL
  destination-port:: SSH
  protocol:: tcp icmp
  action:: accept
}
"""

BAD_TERM_UNSUPPORTED_OPTION = """
term bad-term-unsupported-option {
  comment:: "Management access from corp."
  source-address:: CORP_EXTERNAL
  destination-port:: SSH
  protocol:: tcp
  action:: accept
  option:: tcp-initial
}
"""

BAD_TERM_EGRESS = """
term bad-term-dest-tag {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

BAD_TERM_EGRESS_SOURCE_ADDRESS = """
term bad-term-source-address {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  source-address:: CORP_EXTERNAL
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

BAD_TERM_EGRESS_SOURCE_DEST_TAG = """
term bad-term-source-dest-tag {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  source-tag:: ssh-bastion
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

BAD_TERM_PORTS_COUNT = """
term bad-term-ports-count {
  comment:: "This term has way too many ports."
  source-address:: CORP_EXTERNAL
  source-tag:: ssh-bastion
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

SAMPLE_TAG = 'ssh-bastions '

BAD_TERM_SOURCE_TAGS_COUNT = """
term bad-term-source-tags-count {{
  comment:: "This term has way too many source tags."
  protocol:: tcp
  action:: accept
  source-tag:: {many_source_tags}
}}""".format(many_source_tags=SAMPLE_TAG *
             (gce_vpc_tf.Term._TERM_SOURCE_TAGS_LIMIT + 1))

BAD_TERM_TARGET_TAGS_COUNT = """
term bad-term-target-tags-count {{
  comment:: "This term has way too many target tags."
  source-address:: CORP_EXTERNAL
  protocol:: tcp
  action:: accept
  destination-tag:: {many_target_tags}
}}""".format(many_target_tags=SAMPLE_TAG *
             (gce_vpc_tf.Term._TERM_TARGET_TAGS_LIMIT + 1))

BAD_TERM_SERVICE_ACCOUNTS_COUNT = """
term bad-term-service-accounts-count {{
  comment:: "This term has way too many source service accounts."
  protocol:: tcp
  action:: accept
  source-tag:: ssh-bastion
  source-service-accounts:: {many_service_accounts}
}}""".format(many_service_accounts='acct1@blah.com ' *
             (gce_vpc_tf.Term._TERM_SERVICE_ACCOUNTS_LIMIT + 1))

BAD_TERM_TARGET_TAGS_AND_SERVICE_ACCOUNTS = """
term bad-term-tags-and-service-accounts {
  comment:: "This term has both a tag and a service account."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  protocol:: tcp
  action:: accept
  target-service-accounts:: acct1@blah.com
}
"""

GOOD_TERM_EXCLUDE_RANGE = """
{
  "resource": {
    "google_compute_firewall": [
      {
        "default-good-term-1": {
          "name": "default-good-term-1",
          "source_ranges": [
            "10.128.0.0/10",
            "10.192.0.0/11",
            "10.224.0.0/12",
            "10.241.0.0/16",
            "10.242.0.0/15",
            "10.244.0.0/14",
            "10.248.0.0/13"
          ],
          "allow": [
            {
              "ports": [
                "53"
              ],
              "protocol": "udp"
            },
            {
              "ports": [
                "53"
              ],
              "protocol": "tcp"
            }
          ],
          "description": "DNS access from corp.",
          "direction": "INGRESS",
          "target_tags": [
            "dns-servers"
          ],
          "network": "global/networks/default",
          "priority": 1
        }
      }
    ]
  }
}
"""

DEFAULT_DENY = """
term default-deny {
  comment:: "default_deny."
  action:: deny
}
"""

GOOD_TERM_DENY = """
term good-term-1 {
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-tag:: dns-servers
  protocol:: udp tcp
  action:: deny
}
"""

GOOD_TERM_DENY_EXPECTED = """{
  "resource": {
    "google_compute_firewall": [
      {
        "default-good-term-1": {
          "deny": [
            {
              "protocol": "udp"
            },
            {
              "protocol": "tcp"
            }
          ],
          "description": "DNS access from corp.",
          "direction": "INGRESS",
          "name": "default-good-term-1",
          "network": "global/networks/default",
          "priority": 1,
          "source_ranges": [
            "10.2.3.4/32"
          ],
          "target_tags": [
            "dns-servers"
          ]
        }
      }
    ]
  }
}
"""

STATELESS_REPLY = """{
  "resource": {
    "google_compute_firewall": []
  }
}

"""

VALID_TERM_NAMES = [
    'icmp', 'gcp-to-gcp', 'accept-ssh-from-google', 'ndc-rampart', 'lab-syslog',
    'windows-windows', 'shell-wmn-inbound', 'shell-internal-smtp',
    'accept-internal-traffic', 'deepfield-lab-management',
    'deepfield-lab-reverse-proxy', 'cr-proxy-replication',
    'ciena-one-control-tcp', 'fms-prod-to-fms-prod', 'ast', 'default-deny',
    'google-web', 'zo6hmxkfibardh6tgbiy7ua6'
]

SUPPORTED_TOKENS = frozenset({
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'destination_tag',
    'expiration',
    'stateless_reply',
    'name',
    'option',
    'owner',
    'priority',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'source_service_accounts',
    'source_tag',
    'target_service_accounts',
    'translated',
    'platform',
    'platform_exclude',
})

SUPPORTED_SUB_TOKENS = {'action': {'accept', 'deny'}}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2

TEST_IPS = [
    nacaddr.IP('10.2.3.4/32'),
    nacaddr.IP('2001:4860:8000::5/128'),
    nacaddr.IP('::ffff:a02:301/128'),  # IPV4-mapped
    nacaddr.IP('2002::/16'),  # 6to4
    nacaddr.IP('::0000:a02:301/128'),  # IPv4-compatible
]

TEST_INCLUDE_IPS = [nacaddr.IP('10.2.3.4/32'), nacaddr.IP('10.4.3.2/32')]

TEST_EXCLUDE_IPS = [nacaddr.IP('10.4.3.2/32')]

TEST_INCLUDE_RANGE = [nacaddr.IP('10.128.0.0/9')]

TEST_EXCLUDE_RANGE = [nacaddr.IP('10.240.0.0/16')]

ANY_IPS = [nacaddr.IP('0.0.0.0/0'), nacaddr.IP('::/0')]

TEST_IPV4_ONLY = [nacaddr.IP('10.2.3.4/32')]

TEST_IPV6_ONLY = [nacaddr.IP('2001:4860:8000::5/128')]

_TERM_SOURCE_TAGS_LIMIT = 30
_TERM_TARGET_TAGS_LIMIT = 70
_TERM_PORTS_LIMIT = 256


class TerraformGCETest(parameterized.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def _StripAclHeaders(self, acl):
    return '\n'.join([
        line for line in str(acl).split('\n')
        if not line.lstrip().startswith('#')
    ])

  def testGenericTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    expected = json.loads(GOOD_TERM_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testTermWithPriority(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_3, self.naming), EXP_INFO)
    self.assertIn('"priority": 1', str(acl), str(acl))

  def testTermWithLogging(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_LOGGING, self.naming),
        EXP_INFO)
    rendered_acl = json.loads(
        str(acl)
    )['resource']['google_compute_firewall'][0]['default-good-term-logging']
    self.assertIn('log_config', rendered_acl)
    self.assertEqual(rendered_acl['log_config'],
                     {'metadata': 'INCLUDE_ALL_METADATA'})

  def testGenericTermWithoutNetwork(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'GCE filter does not specify a network.',
        gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER_NO_NETWORK + GOOD_TERM, self.naming),
        EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testGenericTermWithExclude(self):
    self.naming.GetNetAddr.side_effect = [TEST_INCLUDE_IPS, TEST_EXCLUDE_IPS]
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_EXCLUDE, self.naming),
        EXP_INFO)
    expected = json.loads(GOOD_TERM_JSON)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('CORP_EXTERNAL'),
         mock.call('GUEST_WIRELESS_EXTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testGenericTermWithExcludeRange(self):
    self.naming.GetNetAddr.side_effect = [
        TEST_INCLUDE_RANGE, TEST_EXCLUDE_RANGE
    ]
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_EXCLUDE, self.naming),
        EXP_INFO)
    expected = json.loads(GOOD_TERM_EXCLUDE_RANGE)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('CORP_EXTERNAL'),
         mock.call('GUEST_WIRELESS_EXTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testSkipExpiredTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_EXPIRED, self.naming),
        EXP_INFO)
    self.assertEqual(self._StripAclHeaders(str(acl)), STATELESS_REPLY)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testSkipStatelessReply(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    # Add stateless_reply to terms, there is no current way to include it in the
    # term definition.
    ret = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming)
    _, terms = ret.filters[0]
    for term in terms:
      term.stateless_reply = True

    acl = gce_vpc_tf.TerraformGCE(ret, EXP_INFO)
    self.assertEqual(self._StripAclHeaders(str(acl)), STATELESS_REPLY)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testSourceNetworkSplit(self):
    lots_of_ips = []
    for i in range(20):
      for j in range(20):
        lots_of_ips.append(nacaddr.IP('10.%d.%d.1/32' % (i, j)))
    self.naming.GetNetAddr.return_value = lots_of_ips
    self.naming.GetServiceByProto.return_value = ['53']

    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    self.assertIn('default-good-term-1-1', str(acl))
    self.assertIn('default-good-term-1-2', str(acl))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testRaisesWithoutSource(self):
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'Ingress rule missing required field oneof "source_ranges" or "source_tags.',
        gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_NO_SOURCE,
                           self.naming), EXP_INFO)

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testRaisesWithOnlySourceExclusion(self):
    self.naming.GetNetAddr.return_value = TEST_EXCLUDE_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        ('GCE firewall does not support address exclusions without a source '
         'address list.'), gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_SOURCE_EXCLUDE_ONLY,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('GUEST_WIRELESS_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testRaisesNoSourceAfterExclude(self):
    self.naming.GetNetAddr.side_effect = [TEST_INCLUDE_IPS, TEST_INCLUDE_IPS]
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        ('GCE firewall rule no longer contains any source addresses after '
         'the prefixes in source_address_exclude were removed.'),
        gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_EXCLUDE,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('CORP_EXTERNAL'),
         mock.call('GUEST_WIRELESS_EXTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testRaisesWithSourcePort(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'GCE firewall does not support source port restrictions.',
        gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_SOURCE_PORT,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testRaisesWithLongTermName(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaises(
        aclgenerator.TermNameTooLongError, gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_NAME_TOO_LONG, self.naming),
        EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testRaisesWithUnsupportedOption(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'GCE firewall does not support term options.', gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_UNSUPPORTED_OPTION,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testBuildTokens(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    pol1 = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    pol1 = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_2, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testDenyAction(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_DENY, self.naming), EXP_INFO)
    expected = json.loads(GOOD_TERM_DENY_EXPECTED)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testIngress(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INGRESS + GOOD_TERM, self.naming),
        EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))

  def testEgress(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM_EGRESS, self.naming),
        EXP_INFO)
    self.assertIn('EGRESS', str(acl))
    self.assertIn('good-term-1-e', str(acl))
    self.assertNotIn('INGRESS', str(acl))

  def testRaisesWithEgressDestinationTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'GCE Egress rule cannot have destination tag.', gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER_EGRESS + BAD_TERM_EGRESS,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testRaisesWithEgressSourceAddress(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'Egress rules cannot include "source_ranges".', gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER_EGRESS + BAD_TERM_EGRESS_SOURCE_ADDRESS,
                           self.naming), EXP_INFO)

    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testRaisesWithEgressSourceAndDestTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'GCE Egress rule cannot have destination tag.', gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER_EGRESS + BAD_TERM_EGRESS_SOURCE_DEST_TAG,
                           self.naming), EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testEgressTags(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM_EGRESS_SOURCETAG,
                           self.naming), EXP_INFO)

    self.assertIn('target_tags', str(acl))
    self.assertNotIn('source_tags', str(acl))

  def testIngressTags(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INGRESS + GOOD_TERM_INGRESS_SOURCETAG,
                           self.naming), EXP_INFO)

    self.assertIn('source_tags', str(acl))
    self.assertNotIn('target_tags', str(acl))

  def testTargetServiceAccounts(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_INGRESS + GOOD_TERM_INGRESS_TARGET_SERVICE_ACCOUNT,
            self.naming), EXP_INFO)
    self.assertIn('target_service_accounts', str(acl))
    self.assertNotIn('target_tags', str(acl))
    self.assertNotIn('source_tags', str(acl))

  def testDestinationRanges(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM_EGRESS, self.naming),
        EXP_INFO)
    self.assertIn('destination_ranges', str(acl), str(acl))
    self.assertNotIn('source_ranges', str(acl), str(acl))
    self.assertIn('10.2.3.4/32', str(acl), str(acl))

  def testP4TagsNotPresent(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    self.assertNotIn('$Id:', str(acl))

  def testRaisesConflictingDirectionAddress(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'Ingress rule missing required field oneof "source_ranges" or "source_tags"',
        gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER_INGRESS + GOOD_TERM_4,
                           self.naming), EXP_INFO)
    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'Egress rules cannot include "source_ranges".', gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM,
                           self.naming), EXP_INFO)

  def testDefaultDenyEgressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM_EGRESS + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('"priority": 2', str(acl))

  def testDefaultDenyIngressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_INGRESS + GOOD_TERM_INGRESS_SOURCETAG + DEFAULT_DENY,
            self.naming), EXP_INFO)
    self.assertIn('"priority": 2', str(acl))

  def testValidTermNames(self):
    for name in VALID_TERM_NAMES:
      self.naming.GetNetAddr.return_value = TEST_IPS
      self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
      pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_CUSTOM_NAME % name,
                               self.naming)
      acl = gce_vpc_tf.TerraformGCE(pol, EXP_INFO)
      self.assertIsNotNone(str(acl))

  def testInet(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))

  def testInet6(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM, self.naming),
        EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))

  def testInetWithV6AddressesOnly(self):
    self.naming.GetNetAddr.return_value = TEST_IPV6_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))

  def testInet6WithV4AddressesOnly(self):
    self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))

  def testFilterIPv4InIPv6FormatMixed(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('::ffff:a02:301/128', str(acl))
    self.assertNotIn('2002::/16', str(acl))
    self.assertNotIn('::0000:a02:301/128', str(acl))

  def testFilterIPv4InIPv6FormatInet6(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertNotIn('::ffff:a02:301/128', str(acl))
    self.assertNotIn('2002::/16', str(acl))
    self.assertNotIn('::0000:a02:301/128', str(acl))

  def testFilterIPv4InIPv6FormatInet(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('::ffff:a02:301/128', str(acl))
    self.assertNotIn('2002::/16', str(acl))
    self.assertNotIn('::0000:a02:301/128', str(acl))

  def testInetWithSourceTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('internal-servers', str(acl))

  def testInet6WithSourceTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET6 + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertIn('internal-servers', str(acl))

  def testInetWithSourceTagAndV6Addresses(self):
    self.naming.GetNetAddr.return_value = TEST_IPV6_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG +
            DEFAULT_DENY, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('internal-servers', str(acl))

  def testInet6WithSourceTagAndV4Addresses(self):
    self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET6 + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG +
            DEFAULT_DENY, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertNotIn('internal-servers', str(acl))

  def testInet6DefaultDenyEgressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_EGRESS_INET6 + GOOD_TERM_EGRESS + DEFAULT_DENY,
            self.naming), EXP_INFO)
    self.assertNotIn('INGRESS', str(acl))
    self.assertIn('EGRESS', str(acl))
    self.assertIn('"priority": 2', str(acl))
    self.assertIn('::/0', str(acl))
    self.assertNotIn('0.0.0.0/0', str(acl))

  def testInet6DefaultDenyIngressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET6 + GOOD_TERM_INGRESS_SOURCETAG + DEFAULT_DENY,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('"priority": 2', str(acl))
    self.assertIn('::/0', str(acl))
    self.assertNotIn('0.0.0.0/0', str(acl))

  def testIcmpInet(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM_ICMP, self.naming),
        EXP_INFO)
    self.assertIn('icmp', str(acl))
    self.assertNotIn('58', str(acl))

  def testIcmpv6Inet6(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM_ICMPV6, self.naming),
        EXP_INFO)
    self.assertIn('58', str(acl))
    self.assertNotIn('icmp', str(acl))

  def testIcmpInet6(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM_ICMP, self.naming),
        EXP_INFO)
    self.assertNotIn('icmp', str(acl))

  def testIcmpv6Inet(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM_ICMPV6, self.naming),
        EXP_INFO)
    self.assertNotIn('58', str(acl))

  def testIgmpInet(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_TERM_IGMP, self.naming),
        EXP_INFO)
    self.assertIn('2', str(acl))

  def testIgmpInet6(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET6 + GOOD_TERM_IGMP, self.naming),
        EXP_INFO)
    self.assertNotIn('2', str(acl))

  def testPortsCountExceededError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = list(
        str(i)
        for i in range(1024, 1024 + (gce_vpc_tf.Term._TERM_PORTS_LIMIT) * 3, 2))
    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'GCE firewall rule exceeded number of ports per rule: ' +
        'bad-term-ports-count', gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER_INET + BAD_TERM_PORTS_COUNT,
                           self.naming), EXP_INFO)
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testSourceTagCountExceededError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'GCE firewall rule exceeded number of source tags per rule: ' +
        'bad-term-source-tags-count', gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER_INET + BAD_TERM_SOURCE_TAGS_COUNT,
                           self.naming), EXP_INFO)

  def testTargetTagCountExceededError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'GCE firewall rule exceeded number of target tags per rule: ' +
        'bad-term-target-tags-count', gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER_INET + BAD_TERM_TARGET_TAGS_COUNT,
                           self.naming), EXP_INFO)

  def testServiceAccountCountExceededError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'GCE firewall rule exceeded number of service accounts per rule: ' +
        'bad-term-service-accounts-count', gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(GOOD_HEADER_INET + BAD_TERM_SERVICE_ACCOUNTS_COUNT,
                           self.naming), EXP_INFO)

  def testTargetTagsAndServiceAccountsError(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        gce_vpc_tf.TerraformFirewallError,
        'target_service_accounts cannot be used at the same time as target_tags or source_tags',
        gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(
            GOOD_HEADER_INET + BAD_TERM_TARGET_TAGS_AND_SERVICE_ACCOUNTS,
            self.naming), EXP_INFO)

  def testMixed(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM, self.naming),
        EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))

  def testInetIsDefault(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))

  def testMixedWithV6AddressesOnly(self):
    self.naming.GetNetAddr.return_value = TEST_IPV6_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))

  def testMixedWithV4AddressesOnly(self):
    self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))

  def testMixedIsSeparateRules(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM + DEFAULT_DENY,
                           self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertIn('good-term-1', str(acl))
    self.assertIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testMixedWithSourceTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('internal-servers', str(acl))
    self.assertIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testMixedWithSourceTagOnly(self):
    self.naming.GetNetAddr.return_value = []
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('internal-servers', str(acl))
    self.assertNotIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testMixedWithSourceTagAndV6Addresses(self):
    self.naming.GetNetAddr.return_value = TEST_IPV6_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG +
            DEFAULT_DENY, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('internal-servers', str(acl))
    self.assertIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testMixedWithSourceTagAndV4Addresses(self):
    self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_ADDRESS_SOURCETAG +
            DEFAULT_DENY, self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertIn('internal-servers', str(acl))
    self.assertNotIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testMixedWithEgressSourceTag(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_EGRESS_MIXED + GOOD_TERM_EGRESS_SOURCETAG, self.naming),
        EXP_INFO)
    self.assertNotIn('INGRESS', str(acl))
    self.assertIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('dns-servers', str(acl))
    self.assertIn(gcp.GetIpv6TermName('good-term-1-e'), str(acl))

  def testMixedDefaultDenyEgressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_EGRESS_MIXED + GOOD_TERM_EGRESS + DEFAULT_DENY,
            self.naming), EXP_INFO)
    self.assertNotIn('INGRESS', str(acl))
    self.assertIn('EGRESS', str(acl))
    self.assertIn('"priority": 2', str(acl))
    self.assertIn('default-deny-e', str(acl))
    self.assertIn(gcp.GetIpv6TermName('default-deny-e'), str(acl))
    self.assertIn('::/0', str(acl))
    self.assertIn('0.0.0.0/0', str(acl))

  def testMixedDefaultDenyIngressCreation(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_INGRESS_SOURCETAG + DEFAULT_DENY,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('"priority": 2', str(acl))
    self.assertIn('default-deny', str(acl))
    self.assertIn(gcp.GetIpv6TermName('default-deny'), str(acl))
    self.assertIn('::/0', str(acl))
    self.assertIn('0.0.0.0/0', str(acl))

  def testIcmpMixed(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_ICMP, self.naming),
        EXP_INFO)
    self.assertIn('icmp', str(acl))
    self.assertNotIn('58', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn(gcp.GetIpv6TermName('good-term-1'), str(acl))

  def testIcmpv6Mixed(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_ICMPV6, self.naming),
        EXP_INFO)
    self.assertIn('58', str(acl))
    self.assertNotIn('icmp', str(acl))
    self.assertNotIn('10.2.3.4/32', str(acl))
    self.assertIn('2001:4860:8000::5/128', str(acl))
    self.assertIn(gcp.GetIpv6TermName('good-term-pingv6'), str(acl))

  def testIgmpMixed(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_IGMP, self.naming),
        EXP_INFO)
    self.assertIn('2', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertNotIn(gcp.GetIpv6TermName('good-term-pingv6'), str(acl))

  def testNoProtocol(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_NO_PROTOCOL,
                           self.naming), EXP_INFO)
    self.assertIn('all', str(acl))

  def testPlatformExclude(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(
            GOOD_HEADER_INET + GOOD_PLATFORM_EXCLUDE_TERM + GOOD_TERM,
            self.naming), EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('good-term-1', str(acl))
    self.assertNotIn('good-platform-exclude-term', str(acl))

  def testPlatform(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER_INET + GOOD_PLATFORM_TERM, self.naming),
        EXP_INFO)
    self.assertIn('INGRESS', str(acl))
    self.assertNotIn('EGRESS', str(acl))
    self.assertIn('10.2.3.4/32', str(acl))
    self.assertNotIn('2001:4860:8000::5/128', str(acl))
    self.assertIn('good-platform-term', str(acl))

  def testTermOwners(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gce_vpc_tf.TerraformGCE(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_OWNERS, self.naming),
        EXP_INFO)
    rendered_acl = json.loads(
        str(acl)
    )['resource']['google_compute_firewall'][0]['default-good-term-owners']
    self.assertEqual(rendered_acl['description'],
                     'DNS access from corp. Owner: test-owner')

  def testMaxAttributeExceeded(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    self.assertRaises(
        gce_vpc_tf.ExceededAttributeCountError, gce_vpc_tf.TerraformGCE,
        policy.ParsePolicy(
            GOOD_HEADER_MAX_ATTRIBUTE_COUNT + GOOD_TERM + DEFAULT_DENY,
            self.naming), EXP_INFO)

  def testMaxAttribute(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.2.3.4/32')]
    pol = policy.ParsePolicy(GOOD_HEADER_MAX_ATTRIBUTE_COUNT + GOOD_TERM_5,
                             self.naming)
    acl = gce_vpc_tf.TerraformGCE(pol, EXP_INFO)
    self.assertIsNotNone(str(acl))

  @parameterized.named_parameters(('1 ip, 2 ports', {
      'source_ranges': ['10.128.0.0/10'],
      'allow': [{
          'ports': ['22'],
          'protocol': 'tcp'
      }, {
          'ports': ['53'],
          'protocol': 'udp'
      }],
  }, 5), ('1 ip, 2 ports, 1 target tag', {
      'source_ranges': ['10.128.0.0/10'],
      'allow': [{
          'ports': ['22'],
          'protocol': 'tcp'
      }, {
          'ports': ['53'],
          'protocol': 'udp'
      }],
      'target_tags': ['dns-servers'],
  }, 6), ('2 ips, 2 ports, 1 target tag', {
      'source_ranges': ['10.128.0.0/10', '192.168.1.1/24'],
      'allow': [{
          'ports': ['22'],
          'protocol': 'tcp'
      }, {
          'ports': ['53'],
          'protocol': 'udp'
      }],
      'target_tags': ['dns-servers'],
  }, 7), ('2 ips, 2 ports', {
      'source_ranges': ['10.128.0.0/10', '192.168.1.1/24'],
      'allow': [{
          'ports': ['22'],
          'protocol': 'tcp'
      }, {
          'ports': ['53'],
          'protocol': 'udp'
      }],
  }, 6), ('2 ips, 2 protocols', {
      'source_ranges': ['10.128.0.0/10', '192.168.1.1/24'],
      'allow': [{
          'protocol': 'tcp'
      }, {
          'protocol': 'udp'
      }],
  }, 4), ('1 ip, 2 protocols, 1 source tag', {
      'source_ranges': ['10.128.0.0/10'],
      'allow': [{
          'protocol': 'tcp'
      }, {
          'protocol': 'udp'
      }],
      'source_tags': ['dns-servers'],
  }, 4), ('2 ips, 1 protocol', {
      'source_ranges': ['10.128.0.0/10', '192.168.1.1/24'],
      'allow': [{
          'protocol': 'icmp'
      }],
  }, 3), ('1 ip, 2 protocols, 1 service account', {
      'source_ranges': ['10.128.0.0/10'],
      'allow': [{
          'protocol': 'tcp'
      }, {
          'protocol': 'udp'
      }],
      'target_service_accounts': ['test@system.gserviceaccount.com'],
  }, 4))
  def testGetAttributeCount(self, dict_term, expected):
    self.assertEqual(gce_vpc_tf.GetAttributeCount(dict_term), expected)

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


# tests/lib/gcp_hf_test.py

"""Tests for google3.third_party.py.capirca.lib.gcp_hf.py."""

import json
from unittest import mock
from absl.testing import absltest

from absl.testing import parameterized
from capirca.lib import gcp
from capirca.lib import gcp_hf
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

HEADER_NO_OPTIONS = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname
}
"""

HEADER_OPTION_MAX = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname 20
}
"""

HEADER_OPTION_EGRESS = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname EGRESS
}
"""

HEADER_OPTION_EGRESS_2 = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname2 EGRESS
}
"""

HEADER_OPTION_AF = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname inet
}
"""

HEADER_OPTION_HIGH_QUOTA = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname 500
}
"""

HEADER_OPTION_EGRESS_HIGH_QUOTA = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname EGRESS 500
}
"""

HEADER_OPTION_EGRESS_AND_MAX = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname EGRESS 20
}
"""

HEADER_OPTION_EGRESS_AND_AF = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname EGRESS inet
}
"""

HEADER_OPTION_EGRESS_INET6 = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname EGRESS inet6 ga
}
"""

HEADER_OPTION_MAX_AND_AF = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname 20 inet
}
"""

HEADER_VERY_LOW_DEFAULT_MAX = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname 1
}
"""

HEADER_OPTION_BETA = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname inet beta
}
"""

HEADER_OPTION_GA = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname ga
}
"""

HEADER_GA_NO_INET_OPTIONS = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname ga
}
"""

HEADER_OPTION_INET6 = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname inet6 ga
}
"""

HEADER_OPTION_MIXED = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname mixed ga
}
"""

HEADER_OPTION_EGRESS_MIXED = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname EGRESS mixed ga
}
"""

BAD_HEADER_NO_DISPLAYNAME = """
header {
  comment:: "Header without a policy name."
  target:: gcp_hf
}
"""

BAD_HEADER_LONG_DISPLAYNAME = """
header {
  comment:: "Using a display name with 64 characters."
  target:: gcp_hf this-is-a-very-very-long-policy-name-which-is-over-63-characters
}
"""

BAD_HEADER_INVALID_DISPLAYNAME_1 = """
header {
  comment:: "Using a display name with an upper case letter."
  target:: gcp_hf Displayname
}
"""

BAD_HEADER_INVALID_DISPLAYNAME_2 = """
header {
  comment:: "Using a display name with an underscore character."
  target:: gcp_hf display_name
}
"""

BAD_HEADER_INVALID_DISPLAYNAME_3 = """
header {
  comment:: "Using a display name that ends in a dash."
  target:: gcp_hf displayname-
}
"""

BAD_HEADER_UNKNOWN_OPTION = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname INGRESS randomOption
}
"""

BAD_HEADER_UNKNOWN_DIRECTION = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname BIGRESS
}
"""

BAD_HEADER_INVALID_MAX_COST = """
header {
  comment:: "The general policy comment."
  target:: gcp_hf displayname INGRESS 888888888
}
"""

BAD_HEADER_WRONG_PLATFORM = """
header {
  comment:: "The general policy comment."
  target:: wrong_platform
}
"""

TERM_ALLOW_ALL_INTERNAL = """
term allow-internal-traffic {
  comment:: "Generic description"
  protocol:: tcp icmp udp
  action:: next
}
"""

TERM_PLATFORM_ALLOW_ALL_INTERNAL = """
term allow-internal-traffic {
  comment:: "Generic description"
  protocol:: tcp icmp udp
  action:: next
  platform:: gcp_hf
}
"""

TERM_PLATFORM_EXCLUDE = """
term allow-internal-traffic {
  comment:: "Generic description"
  protocol:: tcp icmp udp
  action:: next
  platform-exclude:: gcp_hf
}
"""

TERM_ALLOW_MULTIPLE_PROTOCOL = """
term allow-internal-traffic {
  comment:: "Generic description"
  source-address:: PUBLIC_NAT
  protocol:: tcp icmp udp
  action:: next
}
"""

TERM_ALLOW_MULTIPLE_PROTOCOL_ICMPV6 = """
term allow-internal-traffic {
  comment:: "Generic description"
  source-address:: PUBLIC_NAT
  protocol:: tcp icmpv6 udp
  action:: next
}
"""

TERM_ALLOW_DNS = """
term allow-dns-traffic {
  comment:: "Generic description"
  source-address:: PUBLIC_NAT
  protocol:: tcp udp
  destination-port:: DNS
  action:: next
}
"""

TERM_ALLOW_PORT = """
term allow-traffic-to-port {
  comment:: "Generic description"
  source-address:: PUBLIC_NAT
  protocol:: tcp
  destination-port:: PORT
  action:: next
}
"""

TERM_ALLOW_EGRESS_PORT = """
term allow-traffic-to-port {
  comment:: "Generic description"
  destination-address:: PUBLIC_NAT
  protocol:: tcp
  destination-port:: PORT
  action:: next
}
"""

TERM_ALLOW_PORT_RANGE = """
term allow-port-range {
  comment:: "Generic description"
  source-address:: PUBLIC_NAT
  protocol:: tcp
  destination-port:: RANGE
  action:: next
}
"""

TERM_RESTRICT_EGRESS = """
term restrict_egress {
  comment:: "Generic description"
  destination-address:: PUBLIC_NAT
  protocol:: tcp icmp udp
  action:: next
}
"""

TERM_DENY_INGRESS = """
term default-deny-ingress {
  comment:: "Generic description"
  action:: deny
}
"""

TERM_DENY_EGRESS = """
term default-deny-egress {
  comment:: "Generic description"
  action:: deny
}
"""

TERM_WITH_TARGET_RESOURCES = """
term default-deny-ingress-on-target {
  comment:: "Generic description"
  source-address:: ANY
  action:: deny
  target-resources:: (project1, vpc1)
  target-resources:: (project2, vpc2)
}
"""

TERM_WITH_TARGET_RESOURCES_2 = """
term default-deny-ingress-on-target {
  comment:: "Generic description"
  source-address:: ANY
  action:: deny
  target-resources:: [(project1, vpc1),(project2,vpc2)]
}
"""

TERM_WITH_LOGGING = """
term term-with-logging {
  comment:: "Generic description"
  source-address:: ANY
  protocol:: tcp
  action:: accept
  logging:: true
}
"""

TERM_NO_COMMENT = """
term allow-internal-traffic {
  source-address:: INTERNAL
  protocol:: tcp icmp udp
  action:: next
}
"""

TERM_LONG_COMMENT = """
term allow-internal-traffic {
  comment:: "This is a very long description, it is longer than sixty-four chars"
  source-address:: INTERNAL
  protocol:: tcp icmp udp
  action:: next
}
"""

TERM_NUMBERED_PROTOCOL = """
  term term-numbered-protocol {
  comment:: "Generic description"
  source-address:: PUBLIC_NAT
  protocol:: igmp
  action:: next
}
"""

BAD_TERM_USING_SOURCE_TAG = """
  term bad-term-with-tag {
  comment:: "Generic description"
  source-address:: PUBLIC_NAT
  source-tag:: a-tag
  protocol:: tcp icmp udp
  action:: next
}
"""

BAD_TERM_USING_DEST_TAG = """
  term bad-term-with-tag {
  comment:: "Generic description"
  source-address:: PUBLIC_NAT
  destination-tag:: a-tag
  protocol:: tcp icmp udp
  action:: next
}
"""

BAD_TERM_SOURCE_PORT = """
term allow-traffic-from-port {
  comment:: "Generic description"
  destination-address:: INTERNAL
  protocol:: tcp
  source-port:: PORT
  action:: next
}
"""

BAD_TERM_TARGET_RESOURCES = """
term hf-too-many-target-resources {
  comment:: "Generic description"
  destination-address:: INTERNAL
  protocol:: tcp
  target-resources:: (proj1,vpc1)
  target-resources:: (proj2,vpc2)
  target-resources:: (proj3,vpc3)
  target-resources:: (proj4,vpc4)
  target-resources:: (proj5,vpc5)
  target-resources:: (proj6,vpc6)
  target-resources:: (proj7,vpc7)
  target-resources:: (proj8,vpc8)
  target-resources:: (proj9,vpc9)
  target-resources:: (proj10,vpc10)
  target-resources:: (proj11,vpc11)
  target-resources:: (proj12,vpc12)
  target-resources:: (proj13,vpc13)
  target-resources:: (proj14,vpc14)
  target-resources:: (proj15,vpc15)
  target-resources:: (proj16,vpc16)
  target-resources:: (proj17,vpc17)
  target-resources:: (proj18,vpc18)
  target-resources:: (proj19,vpc19)
  target-resources:: (proj20,vpc20)
  target-resources:: (proj21,vpc21)
  target-resources:: (proj22,vpc22)
  target-resources:: (proj23,vpc23)
  target-resources:: (proj24,vpc24)
  target-resources:: (proj25,vpc25)
  target-resources:: (proj26,vpc26)
  target-resources:: (proj27,vpc27)
  target-resources:: (proj28,vpc28)
  target-resources:: (proj29,vpc29)
  target-resources:: (proj30,vpc30)
  target-resources:: (proj31,vpc31)
  target-resources:: (proj32,vpc32)
  target-resources:: (proj33,vpc33)
  target-resources:: (proj34,vpc34)
  target-resources:: (proj35,vpc35)
  target-resources:: (proj36,vpc36)
  target-resources:: (proj37,vpc37)
  target-resources:: (proj38,vpc38)
  target-resources:: (proj39,vpc39)
  target-resources:: (proj40,vpc40)
  target-resources:: (proj41,vpc41)
  target-resources:: (proj42,vpc42)
  target-resources:: (proj43,vpc43)
  target-resources:: (proj44,vpc44)
  target-resources:: (proj45,vpc45)
  target-resources:: (proj46,vpc46)
  target-resources:: (proj47,vpc47)
  target-resources:: (proj48,vpc48)
  target-resources:: (proj49,vpc49)
  target-resources:: (proj50,vpc50)
  target-resources:: (proj51,vpc51)
  target-resources:: (proj52,vpc52)
  target-resources:: (proj53,vpc53)
  target-resources:: (proj54,vpc54)
  target-resources:: (proj55,vpc55)
  target-resources:: (proj56,vpc56)
  target-resources:: (proj57,vpc57)
  target-resources:: (proj58,vpc58)
  target-resources:: (proj59,vpc59)
  target-resources:: (proj60,vpc60)
  target-resources:: (proj61,vpc61)
  target-resources:: (proj62,vpc62)
  target-resources:: (proj63,vpc63)
  target-resources:: (proj64,vpc64)
  target-resources:: (proj65,vpc65)
  target-resources:: (proj66,vpc66)
  target-resources:: (proj67,vpc67)
  target-resources:: (proj68,vpc68)
  target-resources:: (proj69,vpc69)
  target-resources:: (proj70,vpc70)
  target-resources:: (proj71,vpc71)
  target-resources:: (proj72,vpc72)
  target-resources:: (proj73,vpc73)
  target-resources:: (proj74,vpc74)
  target-resources:: (proj75,vpc75)
  target-resources:: (proj76,vpc76)
  target-resources:: (proj77,vpc77)
  target-resources:: (proj78,vpc78)
  target-resources:: (proj79,vpc79)
  target-resources:: (proj80,vpc80)
  target-resources:: (proj81,vpc81)
  target-resources:: (proj82,vpc82)
  target-resources:: (proj83,vpc83)
  target-resources:: (proj84,vpc84)
  target-resources:: (proj85,vpc85)
  target-resources:: (proj86,vpc86)
  target-resources:: (proj87,vpc87)
  target-resources:: (proj88,vpc88)
  target-resources:: (proj89,vpc89)
  target-resources:: (proj90,vpc90)
  target-resources:: (proj91,vpc91)
  target-resources:: (proj92,vpc92)
  target-resources:: (proj93,vpc93)
  target-resources:: (proj94,vpc94)
  target-resources:: (proj95,vpc95)
  target-resources:: (proj96,vpc96)
  target-resources:: (proj97,vpc97)
  target-resources:: (proj98,vpc98)
  target-resources:: (proj99,vpc99)
  target-resources:: (proj100,vpc100)
  target-resources:: (proj101,vpc101)
  target-resources:: (proj102,vpc102)
  target-resources:: (proj103,vpc103)
  target-resources:: (proj104,vpc104)
  target-resources:: (proj105,vpc105)
  target-resources:: (proj106,vpc106)
  target-resources:: (proj107,vpc107)
  target-resources:: (proj108,vpc108)
  target-resources:: (proj109,vpc109)
  target-resources:: (proj110,vpc110)
  target-resources:: (proj111,vpc111)
  target-resources:: (proj112,vpc112)
  target-resources:: (proj113,vpc113)
  target-resources:: (proj114,vpc114)
  target-resources:: (proj115,vpc115)
  target-resources:: (proj116,vpc116)
  target-resources:: (proj117,vpc117)
  target-resources:: (proj118,vpc118)
  target-resources:: (proj119,vpc119)
  target-resources:: (proj120,vpc120)
  target-resources:: (proj121,vpc121)
  target-resources:: (proj122,vpc122)
  target-resources:: (proj123,vpc123)
  target-resources:: (proj124,vpc124)
  target-resources:: (proj125,vpc125)
  target-resources:: (proj126,vpc126)
  target-resources:: (proj127,vpc127)
  target-resources:: (proj128,vpc128)
  target-resources:: (proj129,vpc129)
  target-resources:: (proj130,vpc130)
  target-resources:: (proj131,vpc131)
  target-resources:: (proj132,vpc132)
  target-resources:: (proj133,vpc133)
  target-resources:: (proj134,vpc134)
  target-resources:: (proj135,vpc135)
  target-resources:: (proj136,vpc136)
  target-resources:: (proj137,vpc137)
  target-resources:: (proj138,vpc138)
  target-resources:: (proj139,vpc139)
  target-resources:: (proj140,vpc140)
  target-resources:: (proj141,vpc141)
  target-resources:: (proj142,vpc142)
  target-resources:: (proj143,vpc143)
  target-resources:: (proj144,vpc144)
  target-resources:: (proj145,vpc145)
  target-resources:: (proj146,vpc146)
  target-resources:: (proj147,vpc147)
  target-resources:: (proj148,vpc148)
  target-resources:: (proj149,vpc149)
  target-resources:: (proj150,vpc150)
  target-resources:: (proj151,vpc151)
  target-resources:: (proj152,vpc152)
  target-resources:: (proj153,vpc153)
  target-resources:: (proj154,vpc154)
  target-resources:: (proj155,vpc155)
  target-resources:: (proj156,vpc156)
  target-resources:: (proj157,vpc157)
  target-resources:: (proj158,vpc158)
  target-resources:: (proj159,vpc159)
  target-resources:: (proj160,vpc160)
  target-resources:: (proj161,vpc161)
  target-resources:: (proj162,vpc162)
  target-resources:: (proj163,vpc163)
  target-resources:: (proj164,vpc164)
  target-resources:: (proj165,vpc165)
  target-resources:: (proj166,vpc166)
  target-resources:: (proj167,vpc167)
  target-resources:: (proj168,vpc168)
  target-resources:: (proj169,vpc169)
  target-resources:: (proj170,vpc170)
  target-resources:: (proj171,vpc171)
  target-resources:: (proj172,vpc172)
  target-resources:: (proj173,vpc173)
  target-resources:: (proj174,vpc174)
  target-resources:: (proj175,vpc175)
  target-resources:: (proj176,vpc176)
  target-resources:: (proj177,vpc177)
  target-resources:: (proj178,vpc178)
  target-resources:: (proj179,vpc179)
  target-resources:: (proj180,vpc180)
  target-resources:: (proj181,vpc181)
  target-resources:: (proj182,vpc182)
  target-resources:: (proj183,vpc183)
  target-resources:: (proj184,vpc184)
  target-resources:: (proj185,vpc185)
  target-resources:: (proj186,vpc186)
  target-resources:: (proj187,vpc187)
  target-resources:: (proj188,vpc188)
  target-resources:: (proj189,vpc189)
  target-resources:: (proj190,vpc190)
  target-resources:: (proj191,vpc191)
  target-resources:: (proj192,vpc192)
  target-resources:: (proj193,vpc193)
  target-resources:: (proj194,vpc194)
  target-resources:: (proj195,vpc195)
  target-resources:: (proj196,vpc196)
  target-resources:: (proj197,vpc197)
  target-resources:: (proj198,vpc198)
  target-resources:: (proj199,vpc199)
  target-resources:: (proj200,vpc200)
  target-resources:: (proj201,vpc201)
  target-resources:: (proj202,vpc202)
  target-resources:: (proj203,vpc203)
  target-resources:: (proj204,vpc204)
  target-resources:: (proj205,vpc205)
  target-resources:: (proj206,vpc206)
  target-resources:: (proj207,vpc207)
  target-resources:: (proj208,vpc208)
  target-resources:: (proj209,vpc209)
  target-resources:: (proj210,vpc210)
  target-resources:: (proj211,vpc211)
  target-resources:: (proj212,vpc212)
  target-resources:: (proj213,vpc213)
  target-resources:: (proj214,vpc214)
  target-resources:: (proj215,vpc215)
  target-resources:: (proj216,vpc216)
  target-resources:: (proj217,vpc217)
  target-resources:: (proj218,vpc218)
  target-resources:: (proj219,vpc219)
  target-resources:: (proj220,vpc220)
  target-resources:: (proj221,vpc221)
  target-resources:: (proj222,vpc222)
  target-resources:: (proj223,vpc223)
  target-resources:: (proj224,vpc224)
  target-resources:: (proj225,vpc225)
  target-resources:: (proj226,vpc226)
  target-resources:: (proj227,vpc227)
  target-resources:: (proj228,vpc228)
  target-resources:: (proj229,vpc229)
  target-resources:: (proj230,vpc230)
  target-resources:: (proj231,vpc231)
  target-resources:: (proj232,vpc232)
  target-resources:: (proj233,vpc233)
  target-resources:: (proj234,vpc234)
  target-resources:: (proj235,vpc235)
  target-resources:: (proj236,vpc236)
  target-resources:: (proj237,vpc237)
  target-resources:: (proj238,vpc238)
  target-resources:: (proj239,vpc239)
  target-resources:: (proj240,vpc240)
  target-resources:: (proj241,vpc241)
  target-resources:: (proj242,vpc242)
  target-resources:: (proj243,vpc243)
  target-resources:: (proj244,vpc244)
  target-resources:: (proj245,vpc245)
  target-resources:: (proj246,vpc246)
  target-resources:: (proj247,vpc247)
  target-resources:: (proj248,vpc248)
  target-resources:: (proj249,vpc249)
  target-resources:: (proj250,vpc250)
  target-resources:: (proj251,vpc251)
  target-resources:: (proj252,vpc252)
  target-resources:: (proj253,vpc253)
  target-resources:: (proj254,vpc254)
  target-resources:: (proj255,vpc255)
  target-resources:: (proj256,vpc256)
  target-resources:: (proj257,vpc257)
  action:: next
}
"""

BAD_TERM_DESTINATION_PORTS = """
term hf-too-many-destination-ports {
  comment:: "Generic description"
  source-address:: INTERNAL
  destination-port:: TP2000
  destination-port:: TP2001
  destination-port:: TP2002
  destination-port:: TP2003
  destination-port:: TP2004
  destination-port:: TP2005
  destination-port:: TP2006
  destination-port:: TP2007
  destination-port:: TP2008
  destination-port:: TP2009
  destination-port:: TP2010
  destination-port:: TP2011
  destination-port:: TP2012
  destination-port:: TP2013
  destination-port:: TP2014
  destination-port:: TP2015
  destination-port:: TP2016
  destination-port:: TP2017
  destination-port:: TP2018
  destination-port:: TP2019
  destination-port:: TP2020
  destination-port:: TP2021
  destination-port:: TP2022
  destination-port:: TP2023
  destination-port:: TP2024
  destination-port:: TP2025
  destination-port:: TP2026
  destination-port:: TP2027
  destination-port:: TP2028
  destination-port:: TP2029
  destination-port:: TP2030
  destination-port:: TP2031
  destination-port:: TP2032
  destination-port:: TP2033
  destination-port:: TP2034
  destination-port:: TP2035
  destination-port:: TP2036
  destination-port:: TP2037
  destination-port:: TP2038
  destination-port:: TP2039
  destination-port:: TP2040
  destination-port:: TP2041
  destination-port:: TP2042
  destination-port:: TP2043
  destination-port:: TP2044
  destination-port:: TP2045
  destination-port:: TP2046
  destination-port:: TP2047
  destination-port:: TP2048
  destination-port:: TP2049
  destination-port:: TP2050
  destination-port:: TP2051
  destination-port:: TP2052
  destination-port:: TP2053
  destination-port:: TP2054
  destination-port:: TP2055
  destination-port:: TP2056
  destination-port:: TP2057
  destination-port:: TP2058
  destination-port:: TP2059
  destination-port:: TP2060
  destination-port:: TP2061
  destination-port:: TP2062
  destination-port:: TP2063
  destination-port:: TP2064
  destination-port:: TP2065
  destination-port:: TP2066
  destination-port:: TP2067
  destination-port:: TP2068
  destination-port:: TP2069
  destination-port:: TP2070
  destination-port:: TP2071
  destination-port:: TP2072
  destination-port:: TP2073
  destination-port:: TP2074
  destination-port:: TP2075
  destination-port:: TP2076
  destination-port:: TP2077
  destination-port:: TP2078
  destination-port:: TP2079
  destination-port:: TP2080
  destination-port:: TP2081
  destination-port:: TP2082
  destination-port:: TP2083
  destination-port:: TP2084
  destination-port:: TP2085
  destination-port:: TP2086
  destination-port:: TP2087
  destination-port:: TP2088
  destination-port:: TP2089
  destination-port:: TP2090
  destination-port:: TP2091
  destination-port:: TP2092
  destination-port:: TP2093
  destination-port:: TP2094
  destination-port:: TP2095
  destination-port:: TP2096
  destination-port:: TP2097
  destination-port:: TP2098
  destination-port:: TP2099
  destination-port:: TP2100
  destination-port:: TP2101
  destination-port:: TP2102
  destination-port:: TP2103
  destination-port:: TP2104
  destination-port:: TP2105
  destination-port:: TP2106
  destination-port:: TP2107
  destination-port:: TP2108
  destination-port:: TP2109
  destination-port:: TP2110
  destination-port:: TP2111
  destination-port:: TP2112
  destination-port:: TP2113
  destination-port:: TP2114
  destination-port:: TP2115
  destination-port:: TP2116
  destination-port:: TP2117
  destination-port:: TP2118
  destination-port:: TP2119
  destination-port:: TP2120
  destination-port:: TP2121
  destination-port:: TP2122
  destination-port:: TP2123
  destination-port:: TP2124
  destination-port:: TP2125
  destination-port:: TP2126
  destination-port:: TP2127
  destination-port:: TP2128
  destination-port:: TP2129
  destination-port:: TP2130
  destination-port:: TP2131
  destination-port:: TP2132
  destination-port:: TP2133
  destination-port:: TP2134
  destination-port:: TP2135
  destination-port:: TP2136
  destination-port:: TP2137
  destination-port:: TP2138
  destination-port:: TP2139
  destination-port:: TP2140
  destination-port:: TP2141
  destination-port:: TP2142
  destination-port:: TP2143
  destination-port:: TP2144
  destination-port:: TP2145
  destination-port:: TP2146
  destination-port:: TP2147
  destination-port:: TP2148
  destination-port:: TP2149
  destination-port:: TP2150
  destination-port:: TP2151
  destination-port:: TP2152
  destination-port:: TP2153
  destination-port:: TP2154
  destination-port:: TP2155
  destination-port:: TP2156
  destination-port:: TP2157
  destination-port:: TP2158
  destination-port:: TP2159
  destination-port:: TP2160
  destination-port:: TP2161
  destination-port:: TP2162
  destination-port:: TP2163
  destination-port:: TP2164
  destination-port:: TP2165
  destination-port:: TP2166
  destination-port:: TP2167
  destination-port:: TP2168
  destination-port:: TP2169
  destination-port:: TP2170
  destination-port:: TP2171
  destination-port:: TP2172
  destination-port:: TP2173
  destination-port:: TP2174
  destination-port:: TP2175
  destination-port:: TP2176
  destination-port:: TP2177
  destination-port:: TP2178
  destination-port:: TP2179
  destination-port:: TP2180
  destination-port:: TP2181
  destination-port:: TP2182
  destination-port:: TP2183
  destination-port:: TP2184
  destination-port:: TP2185
  destination-port:: TP2186
  destination-port:: TP2187
  destination-port:: TP2188
  destination-port:: TP2189
  destination-port:: TP2190
  destination-port:: TP2191
  destination-port:: TP2192
  destination-port:: TP2193
  destination-port:: TP2194
  destination-port:: TP2195
  destination-port:: TP2196
  destination-port:: TP2197
  destination-port:: TP2198
  destination-port:: TP2199
  destination-port:: TP2200
  destination-port:: TP2201
  destination-port:: TP2202
  destination-port:: TP2203
  destination-port:: TP2204
  destination-port:: TP2205
  destination-port:: TP2206
  destination-port:: TP2207
  destination-port:: TP2208
  destination-port:: TP2209
  destination-port:: TP2210
  destination-port:: TP2211
  destination-port:: TP2212
  destination-port:: TP2213
  destination-port:: TP2214
  destination-port:: TP2215
  destination-port:: TP2216
  destination-port:: TP2217
  destination-port:: TP2218
  destination-port:: TP2219
  destination-port:: TP2220
  destination-port:: TP2221
  destination-port:: TP2222
  destination-port:: TP2223
  destination-port:: TP2224
  destination-port:: TP2225
  destination-port:: TP2226
  destination-port:: TP2227
  destination-port:: TP2228
  destination-port:: TP2229
  destination-port:: TP2230
  destination-port:: TP2231
  destination-port:: TP2232
  destination-port:: TP2233
  destination-port:: TP2234
  destination-port:: TP2235
  destination-port:: TP2236
  destination-port:: TP2237
  destination-port:: TP2238
  destination-port:: TP2239
  destination-port:: TP2240
  destination-port:: TP2241
  destination-port:: TP2242
  destination-port:: TP2243
  destination-port:: TP2244
  destination-port:: TP2245
  destination-port:: TP2246
  destination-port:: TP2247
  destination-port:: TP2248
  destination-port:: TP2249
  destination-port:: TP2250
  destination-port:: TP2251
  destination-port:: TP2252
  destination-port:: TP2253
  destination-port:: TP2254
  destination-port:: TP2255
  destination-port:: TP2256
  destination-port:: TP2257
  destination-port:: TP2258
  destination-port:: TP2259
  protocol:: tcp
  action:: next
}
"""

BAD_TERM_IP_VERSION_MISMATCH = """
term icmpv6-in-inet-term {
  comment:: "Generic description"
  source-address:: INTERNAL
  protocol:: icmpv6
  action:: next
}
"""

BAD_TERM_ICMP_VERSION_MISMATCH = """
term icmp-in-inet6-term {
  comment:: "Generic description"
  source-address:: INTERNAL
  protocol:: icmp
  action:: next
}
"""

BAD_TERM_IGMP_VERSION_MISMATCH = """
term igmp-in-inet6-term {
  comment:: "Generic description"
  source-address:: INTERNAL
  protocol:: igmp
  action:: next
}
"""

BAD_TERM_OPTIONS = """
term term-with-options {
  comment:: "Generic description"
  destination-address:: INTERNAL
  option:: TCP_ESTABLISHED
  action:: next
}
"""

BAD_TERM_NON_VALID_PROJECT_ID = """
term default-deny-ingress-on-target {
  comment:: "Generic description"
  protocol:: tcp
  source-address:: ANY
  action:: deny
  target-resources:: (proj, vpc1)
}
"""

BAD_TERM_NON_VALID_VPC_NAME = """
term default-deny-ingress-on-target {
  comment:: "Generic description"
  protocol:: tcp
  source-address:: ANY
  action:: deny
  target-resources:: (project, Vpc)
}
"""

EXPECTED_ONE_RULE_INGRESS_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-internal-traffic: Generic description",
        "direction": "INGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp"
              },
              {
                "ipProtocol": "icmp"
              },
              {
                "ipProtocol": "udp"
              }
            ],
            "srcIpRanges": ["0.0.0.0/0"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_ONE_RULE_INGRESS_W_LOGGING_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "allow",
        "description": "term-with-logging: Generic description",
        "direction": "INGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp"
              }
            ],
            "srcIpRanges": ["10.0.0.0/8"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": true
      }
    ]
  }
]
"""

EXPECTED_ONE_RULE_EGRESS_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "restrict_egress: Generic description",
        "direction": "EGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp"
              },
              {
                "ipProtocol": "icmp"
              },
              {
                "ipProtocol": "udp"
              }
            ],
            "destIpRanges": ["10.0.0.0/8"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_MULTIPLE_RULE_INGRESS_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-internal-traffic: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp"
              },
              {
                "ipProtocol": "icmp"
              },
              {
                "ipProtocol": "udp"
              }
            ],
            "srcIpRanges": ["0.0.0.0/0"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1
      },
      {
        "action": "goto_next",
        "description": "allow-dns-traffic: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp",
                "ports": ["53"]
              },
              {
                "ipProtocol": "udp",
                "ports": ["53"]
              }
            ],
            "srcIpRanges": ["0.0.0.0/0"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 2
      }
    ]
  }
]
"""

EXPECTED_MULTIPLE_RULE_INGRESS_W_DENY = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "Generic description",
        "direction": "INGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp"
              },
              {
                "ipProtocol": "icmp"
              },
              {
                "ipProtocol": "udp"
              }
            ],
            "srcIpRanges": ["10.0.0.0/8"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      },
      {
        "action": "deny",
        "description": "Generic description",
        "direction": "INGRESS",
        "match": {
          "config": {
            "srcIpRanges": ["0.0.0.0/0"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 2,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_PORT_RANGE_INGRESS_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-port-range: Generic description",
        "direction": "INGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp",
                "ports": ["8000-9000"]
              }
            ],
            "srcIpRanges": ["10.0.0.0/8"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_DENY_INGRESS_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "deny",
        "description": "default-deny-ingress: Generic description",
        "direction": "INGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "all"
              }
            ],
            "srcIpRanges": ["0.0.0.0/0"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_IPV6_DENY_INGRESS_GA = """
[
  {
    "shortName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "deny",
        "description": "default-deny-ingress: Generic description",
        "direction": "INGRESS",
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "all"
            }
          ],
          "srcIpRanges": ["::/0"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_MIXED_DENY_INGRESS_GA = """
[
  {
    "shortName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "deny",
        "description": "default-deny-ingress: Generic description",
        "direction": "INGRESS",
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "all"
            }
          ],
          "srcIpRanges": ["0.0.0.0/0"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      },
      {
        "action": "deny",
        "description": "default-deny-ingress-v6: Generic description",
        "direction": "INGRESS",
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "all"
            }
          ],
          "srcIpRanges": ["::/0"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 2,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_DENY_INGRESS_ON_TARGET_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "deny",
        "description": "default-deny-ingress-on-target: Generic description",
        "direction": "INGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "all"
              }
            ],
            "srcIpRanges": ["0.0.0.0/0"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false,
        "targetResources": ["https://www.googleapis.com/compute/v1/projects/project1/global/networks/vpc1",
                            "https://www.googleapis.com/compute/v1/projects/project2/global/networks/vpc2"]
      }
    ]
  }
]
"""

EXPECTED_INGRESS_AND_EGRESS_W_DENY_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-internal-traffic: Generic description",
        "direction": "INGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp"
              },
              {
                "ipProtocol": "icmp"
              },
              {
                "ipProtocol": "udp"
              }
            ],
            "srcIpRanges": ["0.0.0.0/0"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      },
      {
        "action": "deny",
        "description": "default-deny-ingress: Generic description",
        "direction": "INGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "all"
              }
            ],
            "srcIpRanges": ["0.0.0.0/0"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 2,
        "enableLogging": false
      },
      {
        "action": "goto_next",
        "description": "restrict_egress: Generic description",
        "direction": "EGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp"
              },
              {
                "ipProtocol": "icmp"
              },
              {
                "ipProtocol": "udp"
              }
            ],
            "destIpRanges": ["0.0.0.0/0"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 3,
        "enableLogging": false
      },
      {
        "action": "deny",
        "description": "default-deny-egress: Generic description",
        "direction": "EGRESS",
        "match": {
          "config": {
            "destIpRanges": ["0.0.0.0/0"],
            "layer4Configs": [
              {
                "ipProtocol": "all"
              }
            ]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 4,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_DENY_EGRESS_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "deny",
        "description": "default-deny-egress: Generic description",
        "direction": "EGRESS",
        "match": {
          "config": {
            "destIpRanges": ["0.0.0.0/0"],
            "layer4Configs": [
              {
                "ipProtocol": "all"
              }
            ]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_IPV6_DENY_EGRESS_GA = """
[
  {
    "shortName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "deny",
        "description": "default-deny-egress: Generic description",
        "direction": "EGRESS",
        "match": {
          "destIpRanges": ["::/0"],
          "layer4Configs": [
            {
              "ipProtocol": "all"
            }
          ],
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_MIXED_DENY_EGRESS_GA = """
[
  {
    "shortName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "deny",
        "description": "default-deny-egress: Generic description",
        "direction": "EGRESS",
        "match": {
          "destIpRanges": ["0.0.0.0/0"],
          "layer4Configs": [
            {
              "ipProtocol": "all"
            }
          ],
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      },
      {
        "action": "deny",
        "description": "default-deny-egress-v6: Generic description",
        "direction": "EGRESS",
        "match": {
          "destIpRanges": ["::/0"],
          "layer4Configs": [
            {
              "ipProtocol": "all"
            }
          ],
          "versionedExpr": "FIREWALL"
        },
        "priority": 2,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_COST_OF_ONE = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-traffic-to-port: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp",
                "ports": ["80"]
              }
            ],
            "srcIpRanges": ["10.1.1.0/24"]/t
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1
      }
    ]
  }
]
"""

EXPECTED_CHUNKED_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-traffic-to-port: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp",
                "ports": ["80"]
              }
            ],
            "srcIpRanges": [
              "10.0.0.1/32",
              "10.0.1.1/32",
              "192.168.0.0/32",
              "192.168.1.0/32",
              "192.168.2.0/32",
              "192.168.3.0/32",
              "192.168.4.0/32",
              "192.168.5.0/32",
              "192.168.6.0/32",
              "192.168.7.0/32",
              "192.168.8.0/32",
              "192.168.9.0/32",
              "192.168.10.0/32",
              "192.168.11.0/32",
              "192.168.12.0/32",
              "192.168.13.0/32",
              "192.168.14.0/32",
              "192.168.15.0/32",
              "192.168.16.0/32",
              "192.168.17.0/32",
              "192.168.18.0/32",
              "192.168.19.0/32",
              "192.168.20.0/32",
              "192.168.21.0/32",
              "192.168.22.0/32",
              "192.168.23.0/32",
              "192.168.24.0/32",
              "192.168.25.0/32",
              "192.168.26.0/32",
              "192.168.27.0/32",
              "192.168.28.0/32",
              "192.168.29.0/32",
              "192.168.30.0/32",
              "192.168.31.0/32",
              "192.168.32.0/32",
              "192.168.33.0/32",
              "192.168.34.0/32",
              "192.168.35.0/32",
              "192.168.36.0/32",
              "192.168.37.0/32",
              "192.168.38.0/32",
              "192.168.39.0/32",
              "192.168.40.0/32",
              "192.168.41.0/32",
              "192.168.42.0/32",
              "192.168.43.0/32",
              "192.168.44.0/32",
              "192.168.45.0/32",
              "192.168.46.0/32",
              "192.168.47.0/32",
              "192.168.48.0/32",
              "192.168.49.0/32",
              "192.168.50.0/32",
              "192.168.51.0/32",
              "192.168.52.0/32",
              "192.168.53.0/32",
              "192.168.54.0/32",
              "192.168.55.0/32",
              "192.168.56.0/32",
              "192.168.57.0/32",
              "192.168.58.0/32",
              "192.168.59.0/32",
              "192.168.60.0/32",
              "192.168.61.0/32",
              "192.168.62.0/32",
              "192.168.63.0/32",
              "192.168.64.0/32",
              "192.168.65.0/32",
              "192.168.66.0/32",
              "192.168.67.0/32",
              "192.168.68.0/32",
              "192.168.69.0/32",
              "192.168.70.0/32",
              "192.168.71.0/32",
              "192.168.72.0/32",
              "192.168.73.0/32",
              "192.168.74.0/32",
              "192.168.75.0/32",
              "192.168.76.0/32",
              "192.168.77.0/32",
              "192.168.78.0/32",
              "192.168.79.0/32",
              "192.168.80.0/32",
              "192.168.81.0/32",
              "192.168.82.0/32",
              "192.168.83.0/32",
              "192.168.84.0/32",
              "192.168.85.0/32",
              "192.168.86.0/32",
              "192.168.87.0/32",
              "192.168.88.0/32",
              "192.168.89.0/32",
              "192.168.90.0/32",
              "192.168.91.0/32",
              "192.168.92.0/32",
              "192.168.93.0/32",
              "192.168.94.0/32",
              "192.168.95.0/32",
              "192.168.96.0/32",
              "192.168.97.0/32",
              "192.168.98.0/32",
              "192.168.99.0/32",
              "192.168.100.0/32",
              "192.168.101.0/32",
              "192.168.102.0/32",
              "192.168.103.0/32",
              "192.168.104.0/32",
              "192.168.105.0/32",
              "192.168.106.0/32",
              "192.168.107.0/32",
              "192.168.108.0/32",
              "192.168.109.0/32",
              "192.168.110.0/32",
              "192.168.111.0/32",
              "192.168.112.0/32",
              "192.168.113.0/32",
              "192.168.114.0/32",
              "192.168.115.0/32",
              "192.168.116.0/32",
              "192.168.117.0/32",
              "192.168.118.0/32",
              "192.168.119.0/32",
              "192.168.120.0/32",
              "192.168.121.0/32",
              "192.168.122.0/32",
              "192.168.123.0/32",
              "192.168.124.0/32",
              "192.168.125.0/32",
              "192.168.126.0/32",
              "192.168.127.0/32",
              "192.168.128.0/32",
              "192.168.129.0/32",
              "192.168.130.0/32",
              "192.168.131.0/32",
              "192.168.132.0/32",
              "192.168.133.0/32",
              "192.168.134.0/32",
              "192.168.135.0/32",
              "192.168.136.0/32",
              "192.168.137.0/32",
              "192.168.138.0/32",
              "192.168.139.0/32",
              "192.168.140.0/32",
              "192.168.141.0/32",
              "192.168.142.0/32",
              "192.168.143.0/32",
              "192.168.144.0/32",
              "192.168.145.0/32",
              "192.168.146.0/32",
              "192.168.147.0/32",
              "192.168.148.0/32",
              "192.168.149.0/32",
              "192.168.150.0/32",
              "192.168.151.0/32",
              "192.168.152.0/32",
              "192.168.153.0/32",
              "192.168.154.0/32",
              "192.168.155.0/32",
              "192.168.156.0/32",
              "192.168.157.0/32",
              "192.168.158.0/32",
              "192.168.159.0/32",
              "192.168.160.0/32",
              "192.168.161.0/32",
              "192.168.162.0/32",
              "192.168.163.0/32",
              "192.168.164.0/32",
              "192.168.165.0/32",
              "192.168.166.0/32",
              "192.168.167.0/32",
              "192.168.168.0/32",
              "192.168.169.0/32",
              "192.168.170.0/32",
              "192.168.171.0/32",
              "192.168.172.0/32",
              "192.168.173.0/32",
              "192.168.174.0/32",
              "192.168.175.0/32",
              "192.168.176.0/32",
              "192.168.177.0/32",
              "192.168.178.0/32",
              "192.168.179.0/32",
              "192.168.180.0/32",
              "192.168.181.0/32",
              "192.168.182.0/32",
              "192.168.183.0/32",
              "192.168.184.0/32",
              "192.168.185.0/32",
              "192.168.186.0/32",
              "192.168.187.0/32",
              "192.168.188.0/32",
              "192.168.189.0/32",
              "192.168.190.0/32",
              "192.168.191.0/32",
              "192.168.192.0/32",
              "192.168.193.0/32",
              "192.168.194.0/32",
              "192.168.195.0/32",
              "192.168.196.0/32",
              "192.168.197.0/32",
              "192.168.198.0/32",
              "192.168.199.0/32",
              "192.168.200.0/32",
              "192.168.201.0/32",
              "192.168.202.0/32",
              "192.168.203.0/32",
              "192.168.204.0/32",
              "192.168.205.0/32",
              "192.168.206.0/32",
              "192.168.207.0/32",
              "192.168.208.0/32",
              "192.168.209.0/32",
              "192.168.210.0/32",
              "192.168.211.0/32",
              "192.168.212.0/32",
              "192.168.213.0/32",
              "192.168.214.0/32",
              "192.168.215.0/32",
              "192.168.216.0/32",
              "192.168.217.0/32",
              "192.168.218.0/32",
              "192.168.219.0/32",
              "192.168.220.0/32",
              "192.168.221.0/32",
              "192.168.222.0/32",
              "192.168.223.0/32",
              "192.168.224.0/32",
              "192.168.225.0/32",
              "192.168.226.0/32",
              "192.168.227.0/32",
              "192.168.228.0/32",
              "192.168.229.0/32",
              "192.168.230.0/32",
              "192.168.231.0/32",
              "192.168.232.0/32",
              "192.168.233.0/32",
              "192.168.234.0/32",
              "192.168.235.0/32",
              "192.168.236.0/32",
              "192.168.237.0/32",
              "192.168.238.0/32",
              "192.168.239.0/32",
              "192.168.240.0/32",
              "192.168.241.0/32",
              "192.168.242.0/32",
              "192.168.243.0/32",
              "192.168.244.0/32",
              "192.168.245.0/32",
              "192.168.246.0/32",
              "192.168.247.0/32",
              "192.168.248.0/32",
              "192.168.249.0/32",
              "192.168.250.0/32",
              "192.168.251.0/32",
              "192.168.252.0/32",
              "192.168.253.0/32"
            ]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1
      },
      {
        "action": "goto_next",
        "description": "allow-traffic-to-port: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp",
                "ports": ["80"]
              }
            ],
            "srcIpRanges": [
              "192.168.254.0/32",
              "192.168.255.0/32"
            ]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 2
      }
    ]
  }
]
"""

EXPECTED_EGRESS_CHUNKED_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-traffic-to-port: Generic description",
        "direction": "EGRESS",
        "enableLogging": false,
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp",
                "ports": ["80"]
              }
            ],
            "destIpRanges": [
              "10.0.0.1/32",
              "10.0.1.1/32",
              "192.168.0.0/32",
              "192.168.1.0/32",
              "192.168.2.0/32",
              "192.168.3.0/32",
              "192.168.4.0/32",
              "192.168.5.0/32",
              "192.168.6.0/32",
              "192.168.7.0/32",
              "192.168.8.0/32",
              "192.168.9.0/32",
              "192.168.10.0/32",
              "192.168.11.0/32",
              "192.168.12.0/32",
              "192.168.13.0/32",
              "192.168.14.0/32",
              "192.168.15.0/32",
              "192.168.16.0/32",
              "192.168.17.0/32",
              "192.168.18.0/32",
              "192.168.19.0/32",
              "192.168.20.0/32",
              "192.168.21.0/32",
              "192.168.22.0/32",
              "192.168.23.0/32",
              "192.168.24.0/32",
              "192.168.25.0/32",
              "192.168.26.0/32",
              "192.168.27.0/32",
              "192.168.28.0/32",
              "192.168.29.0/32",
              "192.168.30.0/32",
              "192.168.31.0/32",
              "192.168.32.0/32",
              "192.168.33.0/32",
              "192.168.34.0/32",
              "192.168.35.0/32",
              "192.168.36.0/32",
              "192.168.37.0/32",
              "192.168.38.0/32",
              "192.168.39.0/32",
              "192.168.40.0/32",
              "192.168.41.0/32",
              "192.168.42.0/32",
              "192.168.43.0/32",
              "192.168.44.0/32",
              "192.168.45.0/32",
              "192.168.46.0/32",
              "192.168.47.0/32",
              "192.168.48.0/32",
              "192.168.49.0/32",
              "192.168.50.0/32",
              "192.168.51.0/32",
              "192.168.52.0/32",
              "192.168.53.0/32",
              "192.168.54.0/32",
              "192.168.55.0/32",
              "192.168.56.0/32",
              "192.168.57.0/32",
              "192.168.58.0/32",
              "192.168.59.0/32",
              "192.168.60.0/32",
              "192.168.61.0/32",
              "192.168.62.0/32",
              "192.168.63.0/32",
              "192.168.64.0/32",
              "192.168.65.0/32",
              "192.168.66.0/32",
              "192.168.67.0/32",
              "192.168.68.0/32",
              "192.168.69.0/32",
              "192.168.70.0/32",
              "192.168.71.0/32",
              "192.168.72.0/32",
              "192.168.73.0/32",
              "192.168.74.0/32",
              "192.168.75.0/32",
              "192.168.76.0/32",
              "192.168.77.0/32",
              "192.168.78.0/32",
              "192.168.79.0/32",
              "192.168.80.0/32",
              "192.168.81.0/32",
              "192.168.82.0/32",
              "192.168.83.0/32",
              "192.168.84.0/32",
              "192.168.85.0/32",
              "192.168.86.0/32",
              "192.168.87.0/32",
              "192.168.88.0/32",
              "192.168.89.0/32",
              "192.168.90.0/32",
              "192.168.91.0/32",
              "192.168.92.0/32",
              "192.168.93.0/32",
              "192.168.94.0/32",
              "192.168.95.0/32",
              "192.168.96.0/32",
              "192.168.97.0/32",
              "192.168.98.0/32",
              "192.168.99.0/32",
              "192.168.100.0/32",
              "192.168.101.0/32",
              "192.168.102.0/32",
              "192.168.103.0/32",
              "192.168.104.0/32",
              "192.168.105.0/32",
              "192.168.106.0/32",
              "192.168.107.0/32",
              "192.168.108.0/32",
              "192.168.109.0/32",
              "192.168.110.0/32",
              "192.168.111.0/32",
              "192.168.112.0/32",
              "192.168.113.0/32",
              "192.168.114.0/32",
              "192.168.115.0/32",
              "192.168.116.0/32",
              "192.168.117.0/32",
              "192.168.118.0/32",
              "192.168.119.0/32",
              "192.168.120.0/32",
              "192.168.121.0/32",
              "192.168.122.0/32",
              "192.168.123.0/32",
              "192.168.124.0/32",
              "192.168.125.0/32",
              "192.168.126.0/32",
              "192.168.127.0/32",
              "192.168.128.0/32",
              "192.168.129.0/32",
              "192.168.130.0/32",
              "192.168.131.0/32",
              "192.168.132.0/32",
              "192.168.133.0/32",
              "192.168.134.0/32",
              "192.168.135.0/32",
              "192.168.136.0/32",
              "192.168.137.0/32",
              "192.168.138.0/32",
              "192.168.139.0/32",
              "192.168.140.0/32",
              "192.168.141.0/32",
              "192.168.142.0/32",
              "192.168.143.0/32",
              "192.168.144.0/32",
              "192.168.145.0/32",
              "192.168.146.0/32",
              "192.168.147.0/32",
              "192.168.148.0/32",
              "192.168.149.0/32",
              "192.168.150.0/32",
              "192.168.151.0/32",
              "192.168.152.0/32",
              "192.168.153.0/32",
              "192.168.154.0/32",
              "192.168.155.0/32",
              "192.168.156.0/32",
              "192.168.157.0/32",
              "192.168.158.0/32",
              "192.168.159.0/32",
              "192.168.160.0/32",
              "192.168.161.0/32",
              "192.168.162.0/32",
              "192.168.163.0/32",
              "192.168.164.0/32",
              "192.168.165.0/32",
              "192.168.166.0/32",
              "192.168.167.0/32",
              "192.168.168.0/32",
              "192.168.169.0/32",
              "192.168.170.0/32",
              "192.168.171.0/32",
              "192.168.172.0/32",
              "192.168.173.0/32",
              "192.168.174.0/32",
              "192.168.175.0/32",
              "192.168.176.0/32",
              "192.168.177.0/32",
              "192.168.178.0/32",
              "192.168.179.0/32",
              "192.168.180.0/32",
              "192.168.181.0/32",
              "192.168.182.0/32",
              "192.168.183.0/32",
              "192.168.184.0/32",
              "192.168.185.0/32",
              "192.168.186.0/32",
              "192.168.187.0/32",
              "192.168.188.0/32",
              "192.168.189.0/32",
              "192.168.190.0/32",
              "192.168.191.0/32",
              "192.168.192.0/32",
              "192.168.193.0/32",
              "192.168.194.0/32",
              "192.168.195.0/32",
              "192.168.196.0/32",
              "192.168.197.0/32",
              "192.168.198.0/32",
              "192.168.199.0/32",
              "192.168.200.0/32",
              "192.168.201.0/32",
              "192.168.202.0/32",
              "192.168.203.0/32",
              "192.168.204.0/32",
              "192.168.205.0/32",
              "192.168.206.0/32",
              "192.168.207.0/32",
              "192.168.208.0/32",
              "192.168.209.0/32",
              "192.168.210.0/32",
              "192.168.211.0/32",
              "192.168.212.0/32",
              "192.168.213.0/32",
              "192.168.214.0/32",
              "192.168.215.0/32",
              "192.168.216.0/32",
              "192.168.217.0/32",
              "192.168.218.0/32",
              "192.168.219.0/32",
              "192.168.220.0/32",
              "192.168.221.0/32",
              "192.168.222.0/32",
              "192.168.223.0/32",
              "192.168.224.0/32",
              "192.168.225.0/32",
              "192.168.226.0/32",
              "192.168.227.0/32",
              "192.168.228.0/32",
              "192.168.229.0/32",
              "192.168.230.0/32",
              "192.168.231.0/32",
              "192.168.232.0/32",
              "192.168.233.0/32",
              "192.168.234.0/32",
              "192.168.235.0/32",
              "192.168.236.0/32",
              "192.168.237.0/32",
              "192.168.238.0/32",
              "192.168.239.0/32",
              "192.168.240.0/32",
              "192.168.241.0/32",
              "192.168.242.0/32",
              "192.168.243.0/32",
              "192.168.244.0/32",
              "192.168.245.0/32",
              "192.168.246.0/32",
              "192.168.247.0/32",
              "192.168.248.0/32",
              "192.168.249.0/32",
              "192.168.250.0/32",
              "192.168.251.0/32",
              "192.168.252.0/32",
              "192.168.253.0/32"
            ]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1
      },
      {
        "action": "goto_next",
        "description": "allow-traffic-to-port: Generic description",
        "direction": "EGRESS",
        "enableLogging": false,
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "tcp",
                "ports": ["80"]
              }
            ],
            "destIpRanges": [
              "192.168.254.0/32",
              "192.168.255.0/32"
            ]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 2
      }
    ]
  }
]
"""

EXPECTED_ONE_RULE_NUMBERED_PROTOCOL_BETA = """
[
  {
    "displayName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "term-numbered-protocol: Generic description",
        "direction": "INGRESS",
        "match": {
          "config": {
            "layer4Configs": [
              {
                "ipProtocol": "2"
              }
            ],
            "srcIpRanges": ["0.0.0.0/0"]
          },
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_ONE_RULE_IPV6_PROTOCOL_GA = """
[
  {
    "shortName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-traffic-to-port: Generic description",
        "direction": "INGRESS",
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "tcp",
              "ports": ["80"]
            }
          ],
          "srcIpRanges": ["2001:4860:8000::5/128"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_ONE_RULE_MIXED_IPV6_PROTOCOL_GA = """
[
  {
    "shortName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-traffic-to-port-v6: Generic description",
        "direction": "INGRESS",
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "tcp",
              "ports": ["80"]
            }
          ],
          "srcIpRanges": ["2001:4860:8000::5/128"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_ONE_RULE_MIXED_IPV4_PROTOCOL_GA = """
[
  {
    "shortName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-traffic-to-port: Generic description",
        "direction": "INGRESS",
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "tcp",
              "ports": ["80"]
            }
          ],
          "srcIpRanges": ["10.0.0.0/8"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 1,
        "enableLogging": false
      }
    ]
  }
]
"""

EXPECTED_MULTIPLE_MIXED_RULE_INGRESS_GA = """
[
  {
    "shortName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-traffic-to-port: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "tcp",
              "ports": ["80"]
            }
          ],
          "srcIpRanges": ["10.0.0.0/8"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 1
      },
      {
        "action": "goto_next",
        "description": "allow-traffic-to-port-v6: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "tcp",
              "ports": ["80"]
            }
          ],
          "srcIpRanges": ["2001:4860:8000::5/128"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 2
      }
    ]
  }
]
"""

EXPECTED_MULTIPLE_MIXED_RULE_INGRESS_WITH_ICMP_GA = """
[
  {
    "shortName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-internal-traffic: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "tcp"
            },
            {
              "ipProtocol": "icmp"
            },
            {
              "ipProtocol": "udp"
            }
          ],
          "srcIpRanges": ["10.0.0.0/8"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 1
      },
      {
        "action": "goto_next",
        "description": "allow-internal-traffic-v6: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "tcp"
            },
            {
              "ipProtocol": "udp"
            }
          ],
          "srcIpRanges": ["2001:4860:8000::5/128"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 2
      }
    ]
  }
]
"""

EXPECTED_MULTIPLE_MIXED_RULE_INGRESS_WITH_ICMPV6_GA = """
[
  {
    "shortName": "displayname",
    "type": "FIREWALL",
    "rules": [
      {
        "action": "goto_next",
        "description": "allow-internal-traffic: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "tcp"
            },
            {
              "ipProtocol": "udp"
            }
          ],
          "srcIpRanges": ["10.0.0.0/8"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 1
      },
      {
        "action": "goto_next",
        "description": "allow-internal-traffic-v6: Generic description",
        "direction": "INGRESS",
        "enableLogging": false,
        "match": {
          "layer4Configs": [
            {
              "ipProtocol": "tcp"
            },
            {
              "ipProtocol": "58"
            },
            {
              "ipProtocol": "udp"
            }
          ],
          "srcIpRanges": ["2001:4860:8000::5/128"],
          "versionedExpr": "FIREWALL"
        },
        "priority": 2
      }
    ]
  }
]
"""

SUPPORTED_TOKENS = frozenset({
    'action',
    'comment',
    'destination_address',
    'destination_port',
    'destination_tag',
    'logging',
    'name',
    'option',
    'protocol',
    'source_address',
    'source_port',
    'source_tag',
    'stateless_reply',
    'target_resources',
    'translated',
    'platform',
    'platform_exclude',
})

SUPPORTED_SUB_TOKENS = {
    'action': {
        'accept', 'deny', 'next'
    }
}

EXP_INFO = 2

TEST_IP = [nacaddr.IP('10.0.0.0/8')]
TEST_IPV6_IP = [
    nacaddr.IP('2001:4860:8000::5/128'),
    nacaddr.IP('::ffff:a02:301/128'),  # IPv4-mapped
    nacaddr.IP('2002::/16'),  # 6to4
    nacaddr.IP('::0000:a02:301/128'),  # IPv4-compatible
]
TEST_MIXED_IPS = [
    nacaddr.IP('10.0.0.0/8'),
    nacaddr.IP('2001:4860:8000::5/128'),
    nacaddr.IP('::ffff:a02:301/128'),  # IPv4-mapped
    nacaddr.IP('2002::/16'),  # 6to4
    nacaddr.IP('::0000:a02:301/128'),  # IPv4-compatible
]
ALL_IPV4_IPS = [nacaddr.IP('0.0.0.0/0')]
ALL_IPV6_IPS = [nacaddr.IP('::/0')]
MANY_IPS = [nacaddr.IP('192.168.' + str(x) +'.0/32') for x in range(
    0, 256)]
MANY_IPS.extend([nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.1.1')])


class GcpHfTest(parameterized.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def _StripAclHeaders(self, acl):
    return '\n'.join([line for line in str(acl).split('\n')
                      if not line.lstrip().startswith('#')])

  def testDefaultHeader(self):
    """Test that a header without options is accepted."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_ALLOW_ALL_INTERNAL,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_INGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testOptionMaxHeader(self):
    """Test that a header with a default maximum cost is accepted."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_MAX + TERM_ALLOW_ALL_INTERNAL,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_INGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testOptionEgressHeader(self):
    """Test that a header with direction is accepted."""
    self.naming.GetNetAddr.return_value = TEST_IP

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_EGRESS + TERM_RESTRICT_EGRESS,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_EGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testOptionAFHeader(self):
    """Test that a header with address family is accepted."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_AF + TERM_ALLOW_ALL_INTERNAL,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_INGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testOptionEgressAndMaxHeader(self):
    """Test a header with direction and default maximum cost is accepted."""
    self.naming.GetNetAddr.return_value = TEST_IP

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_EGRESS_AND_MAX + TERM_RESTRICT_EGRESS,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_EGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testOptionEgressAndAF(self):
    """Test a header with a direction and address family is accepted."""
    self.naming.GetNetAddr.return_value = TEST_IP

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_EGRESS_AND_AF + TERM_RESTRICT_EGRESS,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_EGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testOptionMaxAndAF(self):
    """Test a header with default maximum cost & address family is accepted."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_MAX_AND_AF + TERM_ALLOW_ALL_INTERNAL,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_INGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testOptionApiVersionAFHeader(self):
    """Test that a header with api_version is accepted."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_BETA + TERM_ALLOW_ALL_INTERNAL,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_INGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testRaisesHeaderErrorOnUnknownOption(self):
    """Test that an unknown header option raises a HeaderError."""
    with self.assertRaises(gcp.HeaderError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(BAD_HEADER_UNKNOWN_OPTION
                             + TERM_ALLOW_ALL_INTERNAL,
                             self.naming),
          EXP_INFO)

  def testRaisesHeaderErrorOnUnknownDirection(self):
    """Test that an unknown direction option raises a HeaderError."""
    with self.assertRaises(gcp.HeaderError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(BAD_HEADER_UNKNOWN_DIRECTION
                             + TERM_ALLOW_ALL_INTERNAL,
                             self.naming),
          EXP_INFO)

  def testRaisesHeaderErrorOnInvalidMaxCost(self):
    """Test that a maximum default cost over 2^16 raises a HeaderError."""
    with self.assertRaises(gcp.HeaderError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(
              BAD_HEADER_INVALID_MAX_COST + TERM_ALLOW_ALL_INTERNAL,
              self.naming), EXP_INFO)

  def testRaisesHeaderErrorOnUnequalMaxCostInMultiplePolicies(self):
    """Test that unequal max costs across multiple policies raises a HeaderError."""
    with self.assertRaises(gcp.HeaderError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(
              HEADER_OPTION_MAX + TERM_ALLOW_ALL_INTERNAL +
              HEADER_OPTION_HIGH_QUOTA + TERM_ALLOW_ALL_INTERNAL, self.naming),
          EXP_INFO)

  def testRaisesHeaderErrorOnUnequalMaxCostInMultiplePoliciesWithDefault(self):
    """Test that unspecified, and set max costs across multiple policies raises a HeaderError."""
    with self.assertRaises(gcp.HeaderError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(
              HEADER_OPTION_MAX + TERM_ALLOW_ALL_INTERNAL +
              HEADER_NO_OPTIONS + TERM_ALLOW_ALL_INTERNAL, self.naming),
          EXP_INFO)

  def testRaisesHeaderErrorOnLongDisplayName(self):
    """Test that a long displayName raises a HeaderError."""
    with self.assertRaises(gcp.HeaderError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(BAD_HEADER_LONG_DISPLAYNAME
                             + TERM_ALLOW_ALL_INTERNAL,
                             self.naming),
          EXP_INFO)

  def testRaisesHeaderErrorOnHeaderWithoutDisplayName(self):
    """Test that a header without a policy name raises a HeaderError."""
    with self.assertRaises(gcp.HeaderError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(BAD_HEADER_NO_DISPLAYNAME
                             + TERM_ALLOW_ALL_INTERNAL, self.naming),
          EXP_INFO)

  def testRaisesHeaderErrorOnIncorrectDisplayName1(self):
    """Test that an invalid displayName raises a HeaderError."""
    with self.assertRaises(gcp.HeaderError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(BAD_HEADER_INVALID_DISPLAYNAME_1
                             + TERM_ALLOW_ALL_INTERNAL,
                             self.naming),
          EXP_INFO)

  def testRaisesHeaderErrorOnIncorrectDisplayName2(self):
    """Test that an invalid displayName raises a HeaderError."""
    with self.assertRaises(gcp.HeaderError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(BAD_HEADER_INVALID_DISPLAYNAME_2
                             + TERM_ALLOW_ALL_INTERNAL,
                             self.naming),
          EXP_INFO)

  def testRaisesHeaderErrorOnIncorrectDisplayName3(self):
    """Test that an invalid displayName raises a HeaderError."""
    with self.assertRaises(gcp.HeaderError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(BAD_HEADER_INVALID_DISPLAYNAME_3
                             + TERM_ALLOW_ALL_INTERNAL,
                             self.naming),
          EXP_INFO)

  def testRaisesTermErrorOnTermWithDestinationTag(self):
    """Test that a term with a destination tag raises an error.

    Tags are not supported in HF.
    """
    with self.assertRaises(gcp.TermError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(HEADER_NO_OPTIONS + BAD_TERM_USING_DEST_TAG,
                             self.naming),
          EXP_INFO)

  def testRaisesTermErrorOnTermWithSourceTag(self):
    """Test that a term with a source tag raises an error.

    Tags are not supported in HF.
    """
    with self.assertRaises(gcp.TermError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(HEADER_NO_OPTIONS + BAD_TERM_USING_SOURCE_TAG,
                             self.naming),
          EXP_INFO)

  def testTermWithNumberedProtocol(self):
    """Test that a protocol number is supported."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_NUMBERED_PROTOCOL,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_NUMBERED_PROTOCOL_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testRaisesTermErrorOnTermWithSourcePort(self):
    """Test that a term with a source port raises Term error."""
    self.naming.GetNetAddr.return_value = TEST_IP
    self.naming.GetServiceByProto.side_effect = [['53']]

    with self.assertRaises(gcp.TermError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(HEADER_NO_OPTIONS + BAD_TERM_SOURCE_PORT,
                             self.naming), EXP_INFO)

  def testRaisesTermErrorOnTermWithTooManyTargetResources(self):
    """Test that a term with > 256 targetResources raises TermError."""
    self.naming.GetNetAddr.return_value = TEST_IP

    with self.assertRaises(gcp.TermError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(HEADER_NO_OPTIONS + BAD_TERM_TARGET_RESOURCES,
                             self.naming), EXP_INFO)

  def testRaisesTermErrorOnTermWithTooManyDestinationPorts(self):
    """Test that a term with > 256 destination ports raises TermError."""
    self.naming.GetNetAddr.return_value = TEST_IP

    # Create a list of 260 numbers to use as destination ports and raise an
    # error.
    # Using even numbers ensures that the port list does not get condensed to a
    # range.
    se_array = []
    for x in range(2000, 2520):
      if x % 2 == 0:
        se_array.append([str(x)])
    # Use destination port list to successively mock return values.
    self.naming.GetServiceByProto.side_effect = se_array

    with self.assertRaises(gcp.TermError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(HEADER_NO_OPTIONS + BAD_TERM_DESTINATION_PORTS,
                             self.naming), EXP_INFO)

  def testRaisesTermErrorOnTermWithOptions(self):
    """Test that a term with a source port raises Term error."""
    self.naming.GetNetAddr.return_value = TEST_IP

    with self.assertRaises(gcp.TermError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(HEADER_NO_OPTIONS + BAD_TERM_OPTIONS,
                             self.naming),
          EXP_INFO)

  def testRaisesTermErrorOnInvalidProjectID(self):
    """Test that an invalid project ID on target resources raises Term error."""
    self.naming.GetNetAddr.return_value = TEST_IP

    with self.assertRaises(gcp.TermError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(HEADER_NO_OPTIONS + BAD_TERM_NON_VALID_PROJECT_ID,
                             self.naming),
          EXP_INFO)

  def testRaisesTermErrorOnInvalidVPCName(self):
    """Test that an invalid VPC name on target resources raises Term error."""
    self.naming.GetNetAddr.return_value = TEST_IP

    with self.assertRaises(gcp.TermError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(HEADER_NO_OPTIONS + BAD_TERM_NON_VALID_VPC_NAME,
                             self.naming),
          EXP_INFO)

  def testRaisesDifferentPolicyNameErrorWhenDifferentPolicyNames(self):
    """Test that different policy names raises DifferentPolicyNameError."""
    with self.assertRaises(gcp_hf.DifferentPolicyNameError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_DENY_INGRESS
                             + HEADER_OPTION_EGRESS_2 + TERM_DENY_EGRESS,
                             self.naming),
          EXP_INFO)

  def testIgnorePolicyFromADifferentPlatform(self):
    """Test that a policy with a header from a different platform is ignored."""
    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(BAD_HEADER_WRONG_PLATFORM
                           + TERM_ALLOW_ALL_INTERNAL,
                           self.naming),
        EXP_INFO)
    self.assertEqual([], json.loads(self._StripAclHeaders(str(acl))))

  def testIgnoreTermWithPlatformExclude(self):
    """Test that a term with platform exclude is ignored."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(
            HEADER_OPTION_AF + TERM_PLATFORM_EXCLUDE + TERM_ALLOW_ALL_INTERNAL,
            self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_INGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testTermWithPlatformExists(self):
    """Test that a term with platform is rendered."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(
            HEADER_OPTION_AF + TERM_PLATFORM_ALLOW_ALL_INTERNAL,
            self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_INGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testIgnoreTermWithICMPv6(self):
    """Test that a term with only an icmpv6 protocol is not rendered."""
    self.naming.GetNetAddr.return_value = TEST_IP

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_AF
                           + BAD_TERM_IP_VERSION_MISMATCH,
                           self.naming),
        EXP_INFO)
    exp = [{'displayName': 'displayname', 'rules': [], 'type': 'FIREWALL'}]
    self.assertEqual(exp, json.loads(self._StripAclHeaders(str(acl))))

  def testInet6IgnoreTermWithICMP(self):
    """Test that a term with only an icmp protocol is not rendered for inet6."""
    self.naming.GetNetAddr.return_value = TEST_IP

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_INET6
                           + BAD_TERM_ICMP_VERSION_MISMATCH,
                           self.naming),
        EXP_INFO)
    exp = [{'shortName': 'displayname', 'rules': [], 'type': 'FIREWALL'}]
    self.assertEqual(exp, json.loads(self._StripAclHeaders(str(acl))))

  def testInet6IgnoreTermWithIGMP(self):
    """Test that a term with only an igmp protocol is not rendered for inet6."""
    self.naming.GetNetAddr.return_value = TEST_IP

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_INET6
                           + BAD_TERM_IGMP_VERSION_MISMATCH,
                           self.naming),
        EXP_INFO)
    exp = [{'shortName': 'displayname', 'rules': [], 'type': 'FIREWALL'}]
    self.assertEqual(exp, json.loads(self._StripAclHeaders(str(acl))))

  def testInet6TermWithIPv6Addresses(self):
    """Test that IPv6 addresses are supported with inet6."""
    self.naming.GetNetAddr.return_value = TEST_IPV6_IP
    self.naming.GetServiceByProto.side_effect = [['80']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_INET6 + TERM_ALLOW_PORT,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_IPV6_PROTOCOL_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testInet6TermWithMixedAddresses(self):
    """Test that Mixed addresses are supported with inet6."""
    self.naming.GetNetAddr.return_value = TEST_MIXED_IPS
    self.naming.GetServiceByProto.side_effect = [['80']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_INET6 + TERM_ALLOW_PORT,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_IPV6_PROTOCOL_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testInet6TermWithIPv4Addresses(self):
    """Test that IPv4 addresses are not rendered with inet6."""
    self.naming.GetNetAddr.return_value = TEST_IP
    self.naming.GetServiceByProto.side_effect = [['80']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_INET6 + TERM_ALLOW_PORT,
                           self.naming), EXP_INFO)
    exp = [{'shortName': 'displayname', 'rules': [], 'type': 'FIREWALL'}]
    self.assertEqual(exp, json.loads(self._StripAclHeaders(str(acl))))

  def testInetTermWithMixedAddresses(self):
    """Test that Mixed addresses are supported with inet."""
    self.naming.GetNetAddr.return_value = TEST_MIXED_IPS
    self.naming.GetServiceByProto.side_effect = [['80']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_EGRESS_AND_AF + TERM_RESTRICT_EGRESS,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_EGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testInetTermWithIPv6Addresses(self):
    """Test that IPv6 addresses are not rendered with inet."""
    self.naming.GetNetAddr.return_value = TEST_IPV6_IP
    self.naming.GetServiceByProto.side_effect = [['80']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_EGRESS_AND_AF + TERM_RESTRICT_EGRESS,
                           self.naming), EXP_INFO)
    exp = [{'displayName': 'displayname', 'rules': [], 'type': 'FIREWALL'}]
    self.assertEqual(exp, json.loads(self._StripAclHeaders(str(acl))))

  def testMixedTermWithMixedAddresses(self):
    """Test that IPv4 and IPv6 addresses are supported with mixed."""
    self.naming.GetNetAddr.return_value = TEST_MIXED_IPS
    self.naming.GetServiceByProto.side_effect = [['80']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_MIXED + TERM_ALLOW_PORT,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_MULTIPLE_MIXED_RULE_INGRESS_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testMixedTermWithIPv4Addresses(self):
    """Test that IPv4 addresses are supported with mixed."""
    self.naming.GetNetAddr.return_value = TEST_IP
    self.naming.GetServiceByProto.side_effect = [['80']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_MIXED + TERM_ALLOW_PORT,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_MIXED_IPV4_PROTOCOL_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testMixedTermWithIPv6Addresses(self):
    """Test that IPv6 addresses are supported with mixed."""
    self.naming.GetNetAddr.return_value = TEST_IPV6_IP
    self.naming.GetServiceByProto.side_effect = [['80']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_MIXED + TERM_ALLOW_PORT,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_MIXED_IPV6_PROTOCOL_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testMixedTermWithICMP(self):
    """Test that ICMP protocol is supported with mixed."""
    self.naming.GetNetAddr.return_value = TEST_MIXED_IPS
    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_MIXED + TERM_ALLOW_MULTIPLE_PROTOCOL,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_MULTIPLE_MIXED_RULE_INGRESS_WITH_ICMP_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testMixedTermWithICMPv6(self):
    """Test that ICMPv6 protocol is supported with mixed."""
    self.naming.GetNetAddr.return_value = TEST_MIXED_IPS
    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(
            HEADER_OPTION_MIXED + TERM_ALLOW_MULTIPLE_PROTOCOL_ICMPV6,
            self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_MULTIPLE_MIXED_RULE_INGRESS_WITH_ICMPV6_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testInetIsDefaultInetVersion(self):
    """Test that inet is the default inet version when not specified."""
    self.naming.GetNetAddr.return_value = TEST_MIXED_IPS
    self.naming.GetServiceByProto.side_effect = [['80']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_GA_NO_INET_OPTIONS + TERM_ALLOW_PORT,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_MIXED_IPV4_PROTOCOL_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testPriority(self):
    """Test that priority is set based on terms' ordering."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_ALLOW_ALL_INTERNAL
                           + TERM_ALLOW_DNS, self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_MULTIPLE_RULE_INGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testLogging(self):
    """Test that logging is used when it is set on a term."""
    self.naming.GetNetAddr.return_value = TEST_IP
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_WITH_LOGGING, self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_ONE_RULE_INGRESS_W_LOGGING_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testTargetResources(self):
    """Test that the target resources is used correctly."""
    self.naming.GetNetAddr.return_value = [nacaddr.IP('0.0.0.0/0')]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_WITH_TARGET_RESOURCES,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_DENY_INGRESS_ON_TARGET_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testSecondWayOfPassingTargetResources(self):
    """Test that the target resources is used correctly."""
    self.naming.GetNetAddr.return_value = [nacaddr.IP('0.0.0.0/0')]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_WITH_TARGET_RESOURCES_2,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_DENY_INGRESS_ON_TARGET_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testMultiplePolicies(self):
    """Tests that both ingress and egress rules are included in one policy."""
    self.maxDiff = None
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_ALLOW_ALL_INTERNAL +
                           TERM_DENY_INGRESS + HEADER_OPTION_EGRESS +
                           TERM_RESTRICT_EGRESS + TERM_DENY_EGRESS,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_INGRESS_AND_EGRESS_W_DENY_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testPortRange(self):
    """Test that a port range is accepted and used correctly."""
    self.naming.GetNetAddr.return_value = TEST_IP
    self.naming.GetServiceByProto.side_effect = [['8000-9000']]

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_ALLOW_PORT_RANGE,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_PORT_RANGE_INGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testTermLongComment(self):
    """Test that a term's long comment gets truncated and prefixed with term name."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_LONG_COMMENT,
                           self.naming),
        EXP_INFO)
    comment_truncated = EXPECTED_ONE_RULE_INGRESS_BETA.replace(
        'Generic description',
        'This is a very long description, it is l')
    expected = json.loads(comment_truncated)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testDefaultDenyIngressCreation(self):
    """Test that the correct IP is correctly set on a deny all ingress term."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_DENY_INGRESS, self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_DENY_INGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testInet6DefaultDenyIngressCreation(self):
    """Test that the IPv6 IP is correctly set on a deny all ingress term."""
    self.naming.GetNetAddr.return_value = ALL_IPV6_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_INET6 + TERM_DENY_INGRESS,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_IPV6_DENY_INGRESS_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testMixedDefaultDenyIngressCreation(self):
    """Test that the mixed IPs are correctly set on a deny all ingress term."""

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_MIXED + TERM_DENY_INGRESS,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_MIXED_DENY_INGRESS_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testDefaultDenyEgressCreation(self):
    """Test that the correct IP is correctly set on a deny all egress term."""
    self.naming.GetNetAddr.return_value = ALL_IPV4_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_EGRESS + TERM_DENY_EGRESS,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_DENY_EGRESS_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testInet6DefaultDenyEgressCreation(self):
    """Test that the IPv6 IP is correctly set on a deny all egress term."""
    self.naming.GetNetAddr.return_value = ALL_IPV6_IPS

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_EGRESS_INET6 + TERM_DENY_EGRESS,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_IPV6_DENY_EGRESS_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testMixedDefaultDenyEgressCreation(self):
    """Test that the mixed IPs are correctly set on a deny all egress term."""

    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_EGRESS_MIXED + TERM_DENY_EGRESS,
                           self.naming), EXP_INFO)
    expected = json.loads(EXPECTED_MIXED_DENY_EGRESS_GA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testBuildTokens(self):
    """Test that _BuildTokens generates the expected list of tokens."""
    self.naming.GetNetAddr.side_effect = [TEST_IP]

    pol1 = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_NO_OPTIONS + TERM_ALLOW_ALL_INTERNAL,
                           self.naming),
        EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testRaisesExceededCostError(self):
    """Test that ExceededCostError is raised when policy exceeds max cost."""
    self.naming.GetNetAddr.side_effect = [TEST_IP]
    with self.assertRaises(gcp_hf.ExceededCostError):
      gcp_hf.HierarchicalFirewall(
          policy.ParsePolicy(HEADER_VERY_LOW_DEFAULT_MAX
                             + TERM_ALLOW_ALL_INTERNAL, self.naming),
          EXP_INFO)

  def testChunkedIPRanges(self):
    """Test that source IP ranges that exceed limit are chunked."""
    self.maxDiff = None
    self.naming.GetNetAddr.side_effect = [MANY_IPS]
    self.naming.GetServiceByProto.side_effect = [['80']]
    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_HIGH_QUOTA + TERM_ALLOW_PORT,
                           self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_CHUNKED_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  def testChunkedEgressIPRanges(self):
    """Test that destination IP ranges that exceed limit are chunked."""
    self.maxDiff = None
    self.naming.GetNetAddr.side_effect = [MANY_IPS]
    self.naming.GetServiceByProto.side_effect = [['80']]
    acl = gcp_hf.HierarchicalFirewall(
        policy.ParsePolicy(HEADER_OPTION_EGRESS_HIGH_QUOTA +
                           TERM_ALLOW_EGRESS_PORT, self.naming),
        EXP_INFO)
    expected = json.loads(EXPECTED_EGRESS_CHUNKED_BETA)
    self.assertEqual(expected, json.loads(self._StripAclHeaders(str(acl))))

  @parameterized.named_parameters(
      ('1 ip, 2 protocols',
       {'match': {
           'config': {
               'destIpRanges': ['0.0.0.0/0'],
               'layer4Configs': [
                   {'ipProtocol': 'tcp'},
                   {'ipProtocol': 'icmp'}
               ]
           }
       }}, 3),
      ('1 ip, 3 protocols, ',
       {'match': {
           'config': {
               'srcIpRanges': ['0.0.0.0/0'],
               'layer4Configs': [
                   {'ipProtocol': 'tcp'},
                   {'ipProtocol': 'icmp'},
                   {'ipProtocol': 'udp'}
               ]
           }
       }}, 4),
      ('1 ip, 1 protocol with 1 port',
       {'match': {
           'config': {
               'srcIpRanges': ['0.0.0.0/0'],
               'layer4Configs': [
                   {'ipProtocol': 'tcp', 'ports': ['22']}
               ]
           }
       }}, 3),
      ('1 ip, 2 protocols with 2 ports each',
       {'match': {
           'config': {
               'srcIpRanges': ['0.0.0.0/0'],
               'layer4Configs': [
                   {'ipProtocol': 'tcp', 'ports': ['22']},
                   {'ipProtocol': 'udp', 'ports': ['22']}
               ]
           }
       }}, 5),
      ('1 ip, 1 protocol with 2 ports',
       {'match': {
           'config': {
               'srcIpRanges': ['0.0.0.0/0'],
               'layer4Configs': [
                   {'ipProtocol': 'tcp', 'ports': ['22', '23']}
               ]
           }
       }}, 4),
      ('2 ips, 1 protocol with 2 ports',
       {'match': {
           'config': {
               'srcIpRanges': ['1.4.6.8/10', '1.2.3.4/5'],
               'layer4Configs': [
                   {'ipProtocol': 'tcp', 'ports': ['22', '23']}
               ]
           }
       }}, 5),
      ('2 ips, 2 protocols with 2 ports each',
       {'match': {
           'config': {
               'srcIpRanges': ['1.4.6.8/10', '1.2.3.4/5'],
               'layer4Configs': [
                   {'ipProtocol': 'tcp', 'ports': ['22', '23']},
                   {'ipProtocol': 'udp', 'ports': ['22', '23']}
               ]
           }
       }}, 8),
      ('1 ip, 2 protocols, 2 targets',
       {'match': {
           'config': {
               'destIpRanges': ['0.0.0.0/0'],
               'layer4Configs': [
                   {'ipProtocol': 'tcp'},
                   {'ipProtocol': 'icmp'}
               ]
           }
       },
        'targetResources': ['target1', 'target2']
       }, 5),
  )
  def testGetRuleTupleCount(self, dict_term, expected):
    self.assertEqual(gcp_hf.GetRuleTupleCount(dict_term, 'beta'), expected)

  @parameterized.named_parameters(
      ('1 ip, 2 protocols', {
          'match': {
              'destIpRanges': ['0.0.0.0/0'],
              'layer4Configs': [{
                  'ipProtocol': 'tcp'
              }, {
                  'ipProtocol': 'icmp'
              }]
          }
      }, 3),
      ('1 ip, 3 protocols, ', {
          'match': {
              'srcIpRanges': ['0.0.0.0/0'],
              'layer4Configs': [{
                  'ipProtocol': 'tcp'
              }, {
                  'ipProtocol': 'icmp'
              }, {
                  'ipProtocol': 'udp'
              }]
          }
      }, 4),
      ('1 ip, 1 protocol with 1 port', {
          'match': {
              'srcIpRanges': ['0.0.0.0/0'],
              'layer4Configs': [{
                  'ipProtocol': 'tcp',
                  'ports': ['22']
              }]
          }
      }, 3),
      ('1 ip, 2 protocols with 2 ports each', {
          'match': {
              'srcIpRanges': ['0.0.0.0/0'],
              'layer4Configs': [{
                  'ipProtocol': 'tcp',
                  'ports': ['22']
              }, {
                  'ipProtocol': 'udp',
                  'ports': ['22']
              }]
          }
      }, 5),
      ('1 ip, 1 protocol with 2 ports', {
          'match': {
              'srcIpRanges': ['0.0.0.0/0'],
              'layer4Configs': [{
                  'ipProtocol': 'tcp',
                  'ports': ['22', '23']
              }]
          }
      }, 4),
      ('2 ips, 1 protocol with 2 ports', {
          'match': {
              'srcIpRanges': ['1.4.6.8/10', '1.2.3.4/5'],
              'layer4Configs': [{
                  'ipProtocol': 'tcp',
                  'ports': ['22', '23']
              }]
          }
      }, 5),
      ('2 ips, 2 protocols with 2 ports each', {
          'match': {
              'srcIpRanges': ['1.4.6.8/10', '1.2.3.4/5'],
              'layer4Configs': [{
                  'ipProtocol': 'tcp',
                  'ports': ['22', '23']
              }, {
                  'ipProtocol': 'udp',
                  'ports': ['22', '23']
              }]
          }
      }, 8),
      ('1 ip, 2 protocols, 2 targets', {
          'match': {
              'destIpRanges': ['0.0.0.0/0'],
              'layer4Configs': [{
                  'ipProtocol': 'tcp'
              }, {
                  'ipProtocol': 'icmp'
              }]
          },
          'targetResources': ['target1', 'target2']
      }, 5),
  )
  def testGAGetRuleTupleCount(self, dict_term, expected):
    self.assertEqual(gcp_hf.GetRuleTupleCount(dict_term, 'ga'), expected)


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


# tests/lib/gcp_test.py

"""Unittest for GCP Firewall Generator module."""

import unittest

from absl.testing import absltest

from absl.testing import parameterized

from capirca.lib import gcp


class HelperFunctionsTest(parameterized.TestCase):

  @parameterized.named_parameters(
      ('lowercase', 'project'),
      ('lowercase_w_hyphen', 'project-id'),
      ('lowercase_w_numbers', 'project123'),
      ('lowercase_w_numbers_hyphens', 'project-1-2-3'))
  def testIsProjectIDValidPasses(self, project):
    self.assertTrue(gcp.IsProjectIDValid(project))

  @parameterized.named_parameters(
      ('trailing_hyphen', 'project-'), ('start_w_number', '1project'),
      ('start_w_hyphen', '-project'), ('uppercase', 'Project'),
      ('too_short_by_one_char', 'proje'), ('too_long_by_one_char', 31 * 'a'))
  def testIsProjectIDValidFails(self, project):
    self.assertFalse(gcp.IsProjectIDValid(project))

  @parameterized.named_parameters(
      ('lowercase', 'vpc'), ('lowercase_w_hyphen', 'v-p-c'),
      ('lowercase_w_numbers', 'vpc123'),
      ('lowercase_w_numbers_hyphens', 'vpc-1-2-3'), ('one_letter', 'v'))
  def testIsVPCNameValidPasses(self, vpc):
    self.assertTrue(gcp.IsVPCNameValid(vpc))

  @parameterized.named_parameters(
      ('trailing_hyphen', 'vpc-'), ('start_w_number', '1vpc'),
      ('start_w_hyphen', '-vpc'), ('uppercase', 'Vpc'),
      ('too_short_by_one_char', ''), ('too_long_by_one_char', 64 * 'a'))
  def testIsVPCNameValidFails(self, vpc):
    self.assertFalse(gcp.IsVPCNameValid(vpc))

  @parameterized.named_parameters(
      ('term', 'good-term', 'good-term-v6'),
      ('term_with_v6_suffix', 'good-term-v6', 'good-term-v6-v6'),
      ('one_letter', 'v', 'v-v6'))
  def testGetIpv6TermName(self, term_name, expected):
    self.assertEqual(expected, gcp.GetIpv6TermName(term_name))

  @parameterized.named_parameters(
      ('ipv4_mapped', ['::ffff:a02:301/128'], []),
      ('6to4', ['2002::/16'], []),
      ('ipv4_compatible', ['::0000:a02:301/128'], []),
      ('ipv4', ['10.2.3.4/32'], ['10.2.3.4/32']),
      ('ipv6', ['2001:4860:8000::5/128'], ['2001:4860:8000::5/128']),
      ('ipv4_wildcard', ['0.0.0.0/0'], ['0.0.0.0/0']),
      ('ipv6_wildcard', ['::/0'], ['::/0']))
  def testFilterIPv4InIPv6FormatAddrs(self, addrs, expected):
    self.assertEqual(expected, gcp.FilterIPv4InIPv6FormatAddrs(addrs))
    with self.assertRaises(ValueError):
      gcp.FilterIPv4InIPv6FormatAddrs(['dshjgsjfhgsd'])


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


# tests/lib/ipset_test.py

# Copyright 2013 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for Ipset rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import ipset
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy


GOOD_HEADER_1 = """
header {
  comment:: "this is a test acl"
  target:: ipset OUTPUT DROP
}
"""

GOOD_HEADER_2 = """
header {
  comment:: "this is a test acl"
  target:: ipset OUTPUT DROP exists
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  source-address:: INTERNAL
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  destination-address:: EXTERNAL
  action:: accept
}
"""

GOOD_TERM_3 = """
term good-term-3 {
  source-address:: INTERNAL
  destination-address:: EXTERNAL
  action:: accept
}
"""

GOOD_TERM_4 = """
term good-term-4 {
  source-address:: INTERNAL
  destination-address:: EXTERNAL
  policer:: batman
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'counter',
    'destination_address',
    'destination_address_exclude',
    'destination_interface',
    'destination_port',
    'destination_prefix',
    'expiration',
    'fragment_offset',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'log_limit',
    'name',
    'option',
    'owner',
    'packet_length',
    'platform',
    'platform_exclude',
    'protocol',
    'routing_instance',
    'source_address',
    'source_address_exclude',
    'source_interface',
    'source_port',
    'source_prefix',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'first-fragment',
               'initial',
               'sample',
               'tcp-established',
               'tcp-initial',
               'syn',
               'ack',
               'fin',
               'rst',
               'urg',
               'psh',
               'all',
               'none'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class IpsetTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testMarkers(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IPv4('10.0.0.0/8')]

    acl = ipset.Ipset(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                         self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# begin:ipset-rules', result)
    self.assertIn('# end:ipset-rules', result)

    self.naming.GetNetAddr.assert_called_once_with('INTERNAL')

  def testGenerateSetName(self):
    # iptables superclass currently limits term name length to 26 characters,
    # but that could change
    policy_term = mock.MagicMock()
    policy_term.name = 'filter_name'
    policy_term.protocol = ['tcp']
    term = ipset.Term(policy_term, 'filter_name', False, None)
    self.assertEqual(term._GenerateSetName('good-term-1', 'src'),
                     'good-term-1-src')
    self.assertEqual(term._GenerateSetName('good-but-way-too-long-term-name',
                                           'src'),
                     'good-but-way-too-long-term--src')
    term = ipset.Term(policy_term, 'filter_name', False, None, 'inet6')
    self.assertEqual(term._GenerateSetName('good-term-1', 'src'),
                     'good-term-1-src-v6')
    self.assertEqual(term._GenerateSetName('good-but-way-too-long-term-name',
                                           'src'),
                     'good-but-way-too-long-te-src-v6')

  def testOneSourceAddress(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IPv4('10.0.0.0/8')]

    acl = ipset.Ipset(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                         self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('-s 10.0.0.0/8', result)
    self.assertNotIn('-m set --match-set good-term-3-src src', result)

    self.naming.GetNetAddr.assert_called_once_with('INTERNAL')

  def testOneDestinationAddress(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IPv4('172.16.0.0/12')]

    acl = ipset.Ipset(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2,
                                         self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('-d 172.16.0.0/12', result)
    self.assertNotIn('-m set --match-set good-term-2-dst dst', result)

    self.naming.GetNetAddr.assert_called_once_with('EXTERNAL')

  def testOneSourceAndDestinationAddress(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('172.16.0.0/12')]]

    acl = ipset.Ipset(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_3,
                                         self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('-s 10.0.0.0/8', result)
    self.assertIn('-d 172.16.0.0/12', result)
    self.assertNotIn('-m set --match-set good-term-3-src src', result)
    self.assertNotIn('-m set --match-set good-term-3-dst dst', result)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('INTERNAL'),
        mock.call('EXTERNAL')])

  def testManySourceAddresses(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IPv4('10.0.0.0/24'), nacaddr.IPv4('10.1.0.0/24')]

    acl = ipset.Ipset(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                         self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('create good-term-1-src hash:net family inet hashsize'
                  ' 4 maxelem 4', result)
    self.assertIn('add good-term-1-src 10.0.0.0/24', result)
    self.assertIn('add good-term-1-src 10.1.0.0/24', result)
    self.assertIn('-m set --match-set good-term-1-src src', result)
    self.assertNotIn('-s ', result)
    self.assertNotIn('-exist', result)

    self.naming.GetNetAddr.assert_called_once_with('INTERNAL')

  def testManyDestinationAddresses(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IPv4('172.16.0.0/24'), nacaddr.IPv4('172.17.0.0/24')]

    acl = ipset.Ipset(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2,
                                         self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('create good-term-2-dst hash:net family inet hashsize '
                  '4 maxelem 4', result)
    self.assertIn('add good-term-2-dst 172.16.0.0/24', result)
    self.assertIn('add good-term-2-dst 172.17.0.0/24', result)
    self.assertIn('-m set --match-set good-term-2-dst dst', result)
    self.assertNotIn('-s ', result)
    self.assertNotIn('-exist', result)

    self.naming.GetNetAddr.assert_called_once_with('EXTERNAL')

  def testManySourceAndDestinationAddresses(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/24'), nacaddr.IPv4('10.1.0.0/24')],
        [nacaddr.IPv4('172.16.0.0/24'), nacaddr.IPv4('172.17.0.0/24')]]

    acl = ipset.Ipset(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_3,
                                         self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('create good-term-3-src hash:net family inet hashsize '
                  '4 maxelem 4', result)
    self.assertIn('create good-term-3-dst hash:net family inet hashsize '
                  '4 maxelem 4', result)
    self.assertIn('add good-term-3-src 10.0.0.0/24', result)
    self.assertIn('add good-term-3-src 10.1.0.0/24', result)
    self.assertIn('add good-term-3-dst 172.16.0.0/24', result)
    self.assertIn('add good-term-3-dst 172.17.0.0/24', result)
    self.assertIn('-m set --match-set good-term-3-src src', result)
    self.assertIn('-m set --match-set good-term-3-dst dst', result)
    self.assertNotIn('-s ', result)
    self.assertNotIn('-d ', result)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('INTERNAL'),
        mock.call('EXTERNAL')])

  def testBuildTokens(self):
    pol1 = ipset.Ipset(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_3,
                                          self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = ipset.Ipset(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_4,
                                          self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testAddsExistsOption(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IPv4('10.0.0.0/24'), nacaddr.IPv4('10.1.0.0/24')]
    acl = ipset.Ipset(policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_1,
                                         self.naming), EXP_INFO)
    self.assertIn('create -exist', str(acl))
    self.assertIn('add -exist', str(acl))


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


# tests/lib/iptables_test.py

# Copyright 2008 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for iptables rendering module."""

import datetime
import re
from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import iptables
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy


GOOD_HEADER_1 = """
header {
  comment:: "this is a test acl"
  target:: iptables INPUT ACCEPT
}
"""

GOOD_HEADER_2 = """
header {
  comment:: "this is a test acl"
  target:: iptables OUTPUT DROP
}
"""

GOOD_HEADER_3 = """
header {
  comment:: "this is a test acl with abbreviation"
  target:: iptables INPUT ACCEPT abbreviateterms
}
"""

GOOD_HEADER_4 = """
header {
  comment:: "this is a test acl with truncation"
  target:: iptables INPUT ACCEPT truncateterms
}
"""

GOOD_HEADER_5 = """
header {
  comment:: "this is a test acl with no default target"
  target:: iptables INPUT
}
"""

GOOD_HEADER_6 = """
header {
  comment:: "this is a test acl with a custom chain and no default target"
  target:: iptables foo
}
"""

GOOD_HEADER_7 = """
header {
  comment:: "this is a test acl with a custom chain and no default target"
  target:: iptables foo noverbose
}
"""

IPV6_HEADER_1 = """
header {
  comment:: "test header for inet6 terms"
  target:: iptables INPUT DROP inet6
}
"""

NON_STANDARD_CHAIN = """
header {
  comment:: "this is a test acl with non-standard chain"
  target:: iptables foo ACCEPT
}
"""

NOSTATE_HEADER = """
header {
  comment:: "iptables filter without stateful"
  target:: iptables INPUT ACCEPT nostate
}
"""

CHAIN_HEADER_1 = """
header {
  comment:: "this is a test acl"
  target:: iptables foobar_chain nostate
}
"""

BAD_HEADER_2 = """
header {
  target:: juniper
}
"""

BAD_HEADER_3 = """
header {
  target:: iptables INPUT MAYBE
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  source-address:: INTERNAL
  source-exclude:: OOB_NET
  protocol:: tcp
  source-port:: HTTP
  action:: accept
}
"""

GOOD_TERM_3 = """
term good-term-3 {
  source-port:: HTTP
  protocol:: tcp
  option:: rst fin tcp-established established
  action:: accept
}
"""

GOOD_TERM_4 = """
term good-term-4 {
  protocol:: tcp udp esp ah gre icmp 50
  action:: accept
}
"""

GOOD_TERM_5 = """
term good-term-5 {
  verbatim:: iptables "mary had a little lamb"
  verbatim:: cisco "mary had second lamb"
  verbatim:: juniper "mary had third lamb"
}
"""

GOOD_TERM_6 = """
term good-term-6 {
  comment:: "Some text describing what this block does,
             possibly including newines, blank lines,
             and extra-long comments (over 255 characters)
             %(long_line)s

             All these cause problems if passed verbatim to iptables.
             "
  comment:: ""
  protocol:: tcp
  action:: accept

}
""" % {'long_line': '-' * 260}


GOOD_TERM_7 = """
term drop-short-initial-fragments {
  option:: first-fragment
  packet-length:: 1-119
  action:: deny
}

term drop-header-overwrite {
  fragment-offset:: 1-119
  action:: deny
}
"""

GOOD_TERM_8 = """
term block-some-icmp {
  protocol:: icmp
  icmp-type:: router-solicitation information-request unreachable echo-reply
  action:: deny
}
"""

GOOD_TERM_9 = """
term good-term-9 {
  source-address:: SOME_SOURCE
  destination-address:: SOME_DEST
  protocol:: tcp
  source-port:: HTTP
  action:: accept
}
"""

GOOD_TERM_10 = """
term good-term-10 {
  owner:: foo@google.com
  action:: accept
}
"""

GOOD_TERM_11 = """
term good_term_11 {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 3 4
  action:: accept
}
"""

GOOD_TERM_12 = """
term good_term_12 {
  comment:: "FOOO"
  action:: accept
}
"""

GOOD_TERM_13 = """
term good_term_13 {
  logging:: syslog
  log-limit:: 99/day
  action:: accept
}
"""

HOPOPT_TERM = """
term hopopt-term {
  protocol:: hopopt
  action:: accept
}
"""

BAD_LOGGING_TERM = """
term bad_logging_term {
  log-limit:: 99/day
  action:: accept
}
"""

BAD_QUOTE_TERM_1 = """
term bad-quote-term-1 {
  comment:: "Text describing without quotes"
  protocol:: tcp
  action:: accept
}
"""

IPV6_TERM_1 = """
term inet6-icmp {
  protocol:: icmpv6
  icmp-type:: destination-unreachable time-exceeded echo-reply
  action:: deny
}
"""

IPV6_HEADERS = """
term ipv6-header-1 {
  protocol:: hopopt
  action:: deny
}

term ipv6-header-2 {
  protocol:: fragment
  action:: deny
}
"""

ICMPV6_TERM_1 = """
term inet6-icmp {
  source-address:: IPV6_INTERNAL
  protocol:: icmpv6
  icmp-type:: destination-unreachable
  action:: deny
}
"""

LOGGING_TERM_1 = """
term foo {
  protocol:: tcp
  logging:: syslog
  action:: accept
}
"""

UDP_STATE_TERM = """
term test-conntrack-udp {
  protocol:: udp
  option:: established
  action:: accept
}
"""

TCP_STATE_TERM = """
term tcp-established-only {
  protocol:: tcp
  option:: established
  action:: accept
}
"""

STATEFUL_ONLY_TERM = """
term stateful-only {
  option:: established
  action:: accept
}
"""

BAD_LONG_TERM_NAME = """
term this-term-name-is-really-far-too-long {
  protocol:: tcp
  action:: accept
}
"""

GOOD_LONG_TERM_NAME = """
term google-experiment-abbreviations {
  protocol:: tcp
  action:: accept
}
"""

GOOD_MULTIPORT = """
term multiport {
  source-port:: FOURTEEN_PORTS
  protocol:: tcp
  action:: accept
}
"""

MULTIPORT_SWAP = """
term multiport {
  source-port:: HTTP HTTPS
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

EXPIRED_TERM = """
term is_expired {
    expiration:: 2001-01-01
    action:: accept
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

GOOD_MULTIPORT_RANGE = """
term bad-mport-ranges {
  destination-port:: FIFTEEN_PORTS_WITH_RANGES
  protocol:: tcp
  action:: accept
}
"""

LARGE_MULTIPORT = """
term bad-multiport {
  destination-port:: LOTS_OF_PORTS
  protocol:: tcp
  action:: accept
}
"""

DUAL_LARGE_MULTIPORT = """
term bad-multiport {
  source-port:: LOTS_OF_SPORTS
  destination-port:: LOTS_OF_DPORTS
  protocol:: tcp
  action:: accept
}
"""

UNSUPPORTED_TERM = """
term ether-type-filter {
  ether-type:: arp
  action:: accept
}
"""

UNKNOWN_TERM_KEYWORD = """
term unknown-keyword {
  comment:: "imaginary new keyword added to the policy library."
  comment:: "i.e. ip-options-count:: 2-255"
  comment:: "must be added in tests due to checking in policy library."
  action:: deny
}
"""

UNSUPPORTED_EXCEPT = """
term block-non-standard {
  protocol-except:: tcp udp icmp
  action:: deny
}
"""

REJECT_TERM1 = """
term reject-term1 {
  action:: reject-with-tcp-rst
}
"""

REJECT_TERM2 = """
term reject-term2 {
  action:: reject
}
"""

NEXT_TERM1 = """
term next-term1 {
  action:: next
}
"""

BAD_PROTOCOL_MATCHES = """
term proto-accept-and-reject {
  protocol:: tcp udp icmp
  protocol-except:: gre
  action:: accept
}
"""

SOURCE_INTERFACE_TERM = """
term src-interface {
  protocol:: tcp
  source-interface:: eth0
  action:: accept
}
"""

DESTINATION_INTERFACE_TERM = """
term dst-interface {
  protocol:: tcp
  destination-interface:: eth0
  action:: accept
}
"""

GOOD_WARNING_TERM = """
term good-warning-term {
  source-port:: HTTP
  protocol:: tcp
  option:: rst fin tcp-established established
  policer:: batman
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'counter',
    'destination_address',
    'destination_address_exclude',
    'destination_interface',
    'destination_port',
    'destination_prefix',
    'expiration',
    'fragment_offset',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'log_limit',
    'name',
    'option',
    'owner',
    'packet_length',
    'platform',
    'platform_exclude',
    'protocol',
    'routing_instance',
    'source_address',
    'source_address_exclude',
    'source_interface',
    'source_port',
    'source_prefix',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'first-fragment',
               'initial',
               'sample',
               'tcp-established',
               'tcp-initial',
               'syn',
               'ack',
               'fin',
               'rst',
               'urg',
               'psh',
               'all',
               'none'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class FakeTerm:
  name = ''
  protocol = ['tcp']


class AclCheckTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  @mock.patch.object(iptables.logging, 'warning')
  def testChainFilter(self, mock_warn):
    filter_name = 'foobar_chain'
    pol = policy.ParsePolicy(CHAIN_HEADER_1 + GOOD_TERM_1, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    # is the chain right?
    self.assertIn('-A foobar_chain -j f_good-term-1', result)
    # is the term named appropriately?
    self.assertIn('-N f_good-term-1', result)

    mock_warn.assert_called_once_with(
        'Filter is generating a non-standard chain that will '
        'not apply to traffic unless linked from INPUT, '
        'OUTPUT or FORWARD filters. New chain name is: %s',
        filter_name)

  def testUnsupportedTargetOption(self):
    pol = policy.ParsePolicy(BAD_HEADER_3 + GOOD_TERM_1, self.naming)
    self.assertRaises(iptables.UnsupportedTargetOptionError,
                      iptables.Iptables, pol, EXP_INFO)

  def testGoodPolicy(self):
    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_1,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('-P OUTPUT DROP', result,
                  'output default policy of drop not set.')
    self.assertIn('-N O_good-term-1', result,
                  'did not find new chain for good-term-1.')
    self.assertIn('-A O_good-term-1 -p icmp -m state '
                  '--state NEW,ESTABLISHED,RELATED -j ACCEPT', result,
                  'did not find append for good-term-1.')

  def testCustomChain(self):
    acl = iptables.Iptables(policy.ParsePolicy(NON_STANDARD_CHAIN + GOOD_TERM_1,
                                               self.naming), EXP_INFO)
    result = str(acl).split('\n')
    self.assertIn('-N foo', result, 'did not find new chain for foo.')
    self.assertNotIn('-P foo', result, 'chain foo may not have a policy set.')

  def testChainNoTarget(self):
    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_5 + GOOD_TERM_1,
                                               self.naming), EXP_INFO)
    result = str(acl).split('\n')
    for line in result:
      self.assertFalse(line.startswith(':INPUT'),
                       'chain may not have a policy set.')
      self.assertFalse(line.startswith('-P INPUT'),
                       'chain may not have a policy set.')
      self.assertFalse(line.startswith('-N INPUT'),
                       'attempting to create a built-in chain.')

  def testCustomChainNoTarget(self):
    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_6 + GOOD_TERM_1,
                                               self.naming), EXP_INFO)
    result = str(acl).split('\n')
    self.assertIn('-N foo', result, 'did not find a new chain for foo.')
    for line in result:
      self.assertFalse(line.startswith(':foo'),
                       'chain may not have a policy set.')
      self.assertFalse(line.startswith('-P foo'),
                       'chain may not have a policy set.')

  def testExcludeReturnsPolicy(self):
    #
    # In this test, we should get fewer lines of output by performing
    # early return jumps on excluded addresses.
    #
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.0.0.0/24')]]
    self.naming.GetServiceByProto.return_value = ['80']

    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('-P INPUT ACCEPT', result, 'no default policy found.')
    self.assertIn('-p tcp', result, 'no protocol specification found.')
    self.assertIn('-s ', result, 'no source address found.')
    self.assertIn('-s 10.0.0.0/24 -j RETURN', result,
                  'expected address 10.0.0.0/24 not jumping to RETURN.')
    self.assertIn('--sport 80 -s 10.0.0.0/8', result,
                  'expected source address 10.0.0.0/8 not accepted.')

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('INTERNAL'),
        mock.call('OOB_NET')])
    self.naming.GetServiceByProto.assert_called_once_with('HTTP', 'tcp')

  def testExcludeAddressesPolicy(self):
    #
    # In this test, we should get fewer lines of output from excluding
    # addresses from the specified destination.
    #
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.128.0.0/9'), nacaddr.IPv4('10.64.0.0/10')]]
    self.naming.GetServiceByProto.return_value = ['80']

    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('--sport 80 -s 10.0.0.0/10', result,
                  'expected source address 10.0.0.0/10 not accepted.')

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('INTERNAL'),
        mock.call('OOB_NET')])
    self.naming.GetServiceByProto.assert_called_once_with('HTTP', 'tcp')

  def testAddExcludeSourceForLengthPolicy(self):
    #
    # In this test, we should generate fewer lines of output by
    # excluding the inverted the source and performing early returns on
    # the excluded range.
    #
    source_range = []
    for i in range(18):
      address = nacaddr.IPv4(10 * 256 * 256 * 256 + i * 256 * 256)
      source_range.append(address.supernet(15))  # Grow to /17

    dest_range = []
    for i in range(40):
      address = nacaddr.IPv4(10 * 256 * 256 * 256 + i * 256)
      dest_range.append(address.supernet(7))  # Grow to /25

    self.naming.GetNetAddr.side_effect = [source_range, dest_range]
    self.naming.GetServiceByProto.return_value = ['80']

    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_9,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('-P INPUT ACCEPT', result, 'no default policy found.')
    self.assertIn('-p tcp', result, 'no protocol specification found.')
    self.assertLess(result.count('\n'), len(source_range) * len(dest_range),
                    'expected less than %d rows, got %d' %
                    (len(source_range) * len(dest_range), result.count('\n')))
    self.assertIn(
        '-s 0.0.0.0/5 -j RETURN', result,
        'expected address 0.0.0.0/5 to RETURN:\n' + result)
    self.assertIn(
        '-s 10.0.128.0/17 -j RETURN', result,
        'expected address 10.0.128.0/17 not jumping to RETURN:\n' + result)
    self.assertTrue(
        re.search('--sport 80 -d 10.0.1.0/25 [^\n]* -j ACCEPT', result),
        'expected destination addresss 10.0.1.0/25 accepted:\n' + result)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('SOME_SOURCE'),
        mock.call('SOME_DEST')])
    self.naming.GetServiceByProto.assert_called_once_with('HTTP', 'tcp')

  def testAddExcludeDestForLengthPolicy(self):
    #
    # In this test, we should generate fewer lines of output by
    # excluding the inverted the source and performing early returns on
    # the excluded range.
    #
    source_range = []
    for i in range(40):
      address = nacaddr.IPv4(10 * 256 * 256 * 256 + i * 256)
      source_range.append(address.supernet(7))  # Grow to /25

    dest_range = []
    for i in range(18):
      address = nacaddr.IPv4(10 * 256 * 256 * 256 + i * 256 * 256)
      dest_range.append(address.supernet(15))  # Grow to /17

    self.naming.GetNetAddr.side_effect = [source_range, dest_range]
    self.naming.GetServiceByProto.return_value = ['80']

    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_9,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('-P INPUT ACCEPT', result, 'no default policy found.')
    self.assertIn('-p tcp', result, 'no protocol specification found.')
    self.assertLess(result.count('\n'), len(source_range) * len(dest_range),
                    'expected less than %d rows, got %d' %
                    (len(source_range) * len(dest_range), result.count('\n')))
    self.assertIn(
        '-d 0.0.0.0/5 -j RETURN', result,
        'expected address 0.0.0.0/5 to RETURN:\n' + result)
    self.assertIn(
        '-d 10.0.128.0/17 -j RETURN', result,
        'expected address 10.0.128.0/17 not jumping to RETURN:\n' + result)
    self.assertTrue(
        re.search('--sport 80 -s 10.0.1.0/25 [^\n]* -j ACCEPT', result),
        'expected destination addresss 10.0.1.0/25 accepted:\n' + result)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('SOME_SOURCE'),
        mock.call('SOME_DEST')])
    self.naming.GetServiceByProto.assert_called_once_with('HTTP', 'tcp')

  def testOptions(self):
    self.naming.GetServiceByProto.return_value = ['80']

    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_3,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('--tcp-flags FIN,RST FIN,RST', result,
                  'tcp flags missing or incorrect.')
    self.assertNotIn('-dport 1024:65535', result,
                     'destination port present.')
    self.assertIn(
        '-m state --state ESTABLISHED,RELATED', result,
        'missing or incorrect state information.')

    self.naming.GetServiceByProto.assert_called_once_with('HTTP', 'tcp')

  def testRejectReset(self):
    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + REJECT_TERM1,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('-j REJECT --reject-with tcp-reset', result,
                  'missing or incorrect reject specification.')

  def testReject(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + REJECT_TERM2, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('-j REJECT --reject-with icmp-host-prohibited', result,
                  'missing or incorrect reject specification.')

  def testRejectIpv6(self):
    pol = policy.ParsePolicy(IPV6_HEADER_1 + REJECT_TERM2, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertNotIn('-p all', result, 'protocol spec present')
    self.assertIn('-j REJECT --reject-with icmp6-adm-prohibited', result,
                  'missing or incorrect reject specification.')

  def testIPv6Headers(self):
    pol = policy.ParsePolicy(IPV6_HEADER_1 + IPV6_HEADERS, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('-m u32 --u32 "0x3&0xff=0x0"', result,
                  'match for hop-by-hop header is missing')
    self.assertIn('-m u32 --u32 "0x3&0xff=0x2c"', result,
                  'match for fragment header is missing')

  def testNextTerm(self):
    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + NEXT_TERM1,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('-j RETURN', result,
                  'jump to RETURN not found.')

  def testProtocols(self):
    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_4,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('-p tcp', result, 'protocol tcp not found.')
    self.assertIn('-p udp', result, 'protocol udp not found.')
    self.assertIn('-p esp', result, 'protocol esp not found.')
    self.assertIn('-p ah', result, 'protocol ah not found.')
    self.assertIn('-p gre', result, 'protocol gre not found.')
    self.assertIn('-p icmp', result, 'protocol icmp not found.')
    self.assertIn('-p 50', result, 'protocol 50 not found.')

  def testVerbatimTerm(self):
    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_5,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('mary had a little lamb', result,
                  'first verbatim output is missing or incorrect.')
    # check if another platforms verbatim shows up
    self.assertNotIn('mary had a second lamb', result,
                     'second vebatim output is missing or incorrect.')
    self.assertNotIn('mary had a third lamb', result,
                     'third verbatim output is missing or incorrect.')

  def testCommentReflowing(self):
    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_6,
                                               self.naming), EXP_INFO)
    result = str(acl)
    self.assertNotIn('--comments ""', result,
                     'Iptables cannot handle empty comments')
    self.assertFalse(re.search('--comments "[^"]{256,}"', result),
                     'Iptables comments must be under 255 characters.')
    self.assertFalse(re.search('--comments "[^"]*\n', result),
                     'Iptables comments may not contain newline characters.')

  def testCommentQuoteStripping(self):

    parsed_policy = policy.ParsePolicy(GOOD_HEADER_1 + BAD_QUOTE_TERM_1,
                                       self.naming)
    parsed_policy.filters[0][1][0].comment = ['Text "describing" "with" quotes']

    acl = iptables.Iptables(parsed_policy, EXP_INFO)
    result = str(acl)

    self.assertTrue(re.search(
        '--comment "Text describing with quotes"', result),
        'Iptables did not strip out quotes')

  def testLongTermName(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + BAD_LONG_TERM_NAME, self.naming)
    self.assertRaises(aclgenerator.TermNameTooLongError,
                      iptables.Iptables, pol, EXP_INFO)

  def testLongTermAbbreviation(self):
    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_LONG_TERM_NAME, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('-abbreviations', result,
                  'Our strings disappeared during abbreviation.')

  def testLongTermTruncation(self):
    pol = policy.ParsePolicy(GOOD_HEADER_4 + GOOD_LONG_TERM_NAME, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('google-experiment-abbrev', result,
                  'Our strings disappeared during truncation.')
    self.assertNotIn('google-experiment-abbreviations', result,
                     'Term name was not truncated as expected.')

  def testFragmentOptions(self):
    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_7, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('--u32 4&0x3FFF=0x2000', result,
                  'first-fragment rule is missing')
    self.assertIn('--length 1:119', result,
                  'length match is missing')
    self.assertIn('--u32 4&0x1FFF=1:119', result,
                  'fragment-offset rule is missing')

  def testIcmpMatching(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_8, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('--icmp-type 0', result,
                  'icmp-type 0 (echo-reply) is missing')
    self.assertIn('--icmp-type 3', result,
                  'icmp-type 3 (destination-unreachable) is missing')
    self.assertIn('--icmp-type 10', result,
                  'icmp-type 10 (router-solicit) is missing')
    self.assertIn('--icmp-type 15', result,
                  'icmp-type 15 (info-request) is missing')

  def testIcmpCode(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_11, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('--icmp-type 3/3', result, result)
    self.assertIn('--icmp-type 3/4', result, result)

  def testConntrackUDP(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + UDP_STATE_TERM, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('-m state --state ESTABLISHED,RELATED', result,
                  'udp connection tracking is missing state module')
    self.assertNotIn('-dport 1024:65535', result,
                     'udp connection tracking contains destination high-ports')
    self.assertIn('-p udp', result,
                  'udp connection tracking is missing protocol specification')

  def testConntrackAll(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + STATEFUL_ONLY_TERM, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('-m state --state ESTABLISHED,RELATED', result,
                  'connection tracking is missing state module arguments')
    self.assertNotIn('-dport 1024:65535', result,
                     'High-ports should not appear for non-TCP/UDP protocols')

  def testTcpEstablishedNostate(self):
    pol = policy.ParsePolicy(NOSTATE_HEADER + TCP_STATE_TERM, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn(
        '%s %s' %
        ('--tcp-flags ACK,FIN,RST,SYN RST', '--dport 1024:65535 -j ACCEPT'),
        result, 'No rule matching packets with RST bit only.\n' + result)
    self.assertNotIn('--state', result,
                     'Nostate header should not use nf_conntrack --state flag')

  def testUdpEstablishedNostate(self):
    pol = policy.ParsePolicy(NOSTATE_HEADER + UDP_STATE_TERM, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('-p udp --dport 1024:65535 -j ACCEPT', result,
                  'No rule matching TCP packets with ACK bit.\n' + result)
    self.assertNotIn('--state', result,
                     'Nostate header should not use nf_conntrack --state flag')

  def testEstablishedNostate(self):
    # when using "nostate" filter and a term with "option:: established"
    # have any protocol other than TCP and/or UDP should raise error.
    pol = policy.ParsePolicy(NOSTATE_HEADER + STATEFUL_ONLY_TERM, self.naming)
    self.assertRaises(aclgenerator.EstablishedError,
                      iptables.Iptables, pol, EXP_INFO)

  def testUnsupportedFilter(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + UNSUPPORTED_TERM, self.naming)
    self.assertRaises(aclgenerator.UnsupportedFilterError, iptables.Iptables,
                      pol, EXP_INFO)

  def testUnknownTermKeyword(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + UNKNOWN_TERM_KEYWORD, self.naming)
    # Adding a (fake) new property, e.g. if policy.py is updated.
    pol.filters[0][1][0].ip_options_count = '2-255'
    self.assertRaises(aclgenerator.UnsupportedFilterError, iptables.Iptables,
                      pol, EXP_INFO)

  def testProtocolExceptUnsupported(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + UNSUPPORTED_EXCEPT, self.naming)
    self.assertRaises(aclgenerator.UnsupportedFilterError, iptables.Iptables,
                      pol, EXP_INFO)

  def testTermNameConflict(self):
    pol = policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_1 +
                             GOOD_TERM_1 + GOOD_TERM_1, self.naming)
    self.assertRaises(aclgenerator.DuplicateTermError,
                      iptables.Iptables, pol, EXP_INFO)

  def testMultiPort(self):
    ports = [str(x) for x in range(1, 29, 2)]
    self.naming.GetServiceByProto.return_value = ports

    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_MULTIPORT,
                                               self.naming), EXP_INFO)
    self.assertIn('-m multiport --sports %s' % ','.join(ports), str(acl),
                  'multiport module not used as expected.')
    # b/10626420
    self.assertNotIn('-m multiport --dports  -d', str(acl),
                     'invalid multiport syntax produced.')

    self.naming.GetServiceByProto.assert_called_once_with(
        'FOURTEEN_PORTS', 'tcp')

  def testMultiPortWithRanges(self):
    ports = [str(x) for x in (1, 3, 5, 7, 9, 11, 13, 15, 17, '19-21', '23-25',
                              '27-29')]
    self.naming.GetServiceByProto.return_value = ports

    acl = iptables.Iptables(policy.ParsePolicy(
        GOOD_HEADER_1 + GOOD_MULTIPORT_RANGE, self.naming), EXP_INFO)
    expected = '-m multiport --dports %s' % ','.join(ports).replace('-', ':')
    self.assertIn(expected, str(acl),
                  'multiport module not used as expected.')

    self.naming.GetServiceByProto.assert_called_once_with(
        'FIFTEEN_PORTS_WITH_RANGES', 'tcp')

  def testMultiportSwap(self):
    self.naming.GetServiceByProto.side_effect = [['80'], ['443'], ['22']]

    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + MULTIPORT_SWAP,
                                               self.naming), EXP_INFO)
    expected = '--dport 22 -m multiport --sports 80,443'
    self.assertIn(expected, str(acl),
                  'failing to move single port before multiport values.')

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('HTTP', 'tcp'),
        mock.call('HTTPS', 'tcp'),
        mock.call('SSH', 'tcp')])

  def testMultiportLargePortCount(self):
    ports = [str(x) for x in range(1, 71, 2)]
    self.naming.GetServiceByProto.return_value = ports

    acl = iptables.Iptables(policy.ParsePolicy(
        GOOD_HEADER_1 + LARGE_MULTIPORT, self.naming), EXP_INFO)
    self.assertIn('-m multiport --dports 1,3,5,7,9', str(acl))
    self.assertIn('-m multiport --dports 29,31,33,35,37', str(acl))
    self.assertIn('-m multiport --dports 57,59,61,63,65,67,69', str(acl))

    self.naming.GetServiceByProto.assert_called_once_with(
        'LOTS_OF_PORTS', 'tcp')

  def testMultiportDualLargePortCount(self):
    ports = [str(x) for x in range(1, 71, 2)]
    self.naming.GetServiceByProto.return_value = ports

    acl = iptables.Iptables(policy.ParsePolicy(
        GOOD_HEADER_1 + DUAL_LARGE_MULTIPORT, self.naming), EXP_INFO)
    self.assertIn('-m multiport --sports 1,3,5', str(acl))
    self.assertIn('-m multiport --sports 29,31,33', str(acl))
    self.assertIn('-m multiport --sports 57,59,61', str(acl))
    self.assertIn('23,25,27 -m multiport --dports 1,3,5', str(acl))
    self.assertIn('23,25,27 -m multiport --dports 29,31,33', str(acl))
    self.assertIn('23,25,27 -m multiport --dports 57,59,61', str(acl))
    self.assertIn('51,53,55 -m multiport --dports 1,3,5', str(acl))
    self.assertIn('51,53,55 -m multiport --dports 29,31,33', str(acl))
    self.assertIn('51,53,55 -m multiport --dports 57,59,61', str(acl))
    self.assertIn('65,67,69 -m multiport --dports 1,3,5', str(acl))
    self.assertIn('65,67,69 -m multiport --dports 29,31,33', str(acl))
    self.assertIn('65,67,69 -m multiport --dports 57,59,61', str(acl))

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('LOTS_OF_SPORTS', 'tcp'),
        mock.call('LOTS_OF_DPORTS', 'tcp')])

  def testGeneratePortBadArguments(self):
    term = iptables.Term(FakeTerm(), 'test', True, 'test')
    # Both source and dest are true
    self.assertRaises(iptables.BadPortsError,
                      term._GeneratePortStatement,
                      [(1, 1), (2, 2)], source=True, dest=True)

  def testGeneratePortNotImplemented(self):
    term = iptables.Term(FakeTerm(), 'test', True, 'test')
    # Both source and dest are false
    self.assertRaises(NotImplementedError,
                      term._GeneratePortStatement,
                      [(1, 1), (2, 2)], source=False, dest=False)

  def testLogging(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + LOGGING_TERM_1, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('-j LOG --log-prefix foo', result,
                  'logging jump does not appear in output.')
    self.assertIn('-j ACCEPT', result,
                  'action jump does not appear in output.')

  def testSourceInterface(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + SOURCE_INTERFACE_TERM, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('-i eth0', result,
                  'source interface specification not in output.')

  def testDestinationInterface(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + DESTINATION_INTERFACE_TERM,
                             self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('-o eth0', result,
                  'destination interface specification not in output.')

  @mock.patch.object(iptables.logging, 'warning')
  def testExpired(self, mock_warn):
    _ = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + EXPIRED_TERM,
                                             self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired'
        ' and will not be rendered.', 'is_expired', 'INPUT')

  @mock.patch.object(iptables.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    _ = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + EXPIRING_TERM %
                                             exp_date.strftime('%Y-%m-%d'),
                                             self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring', 'INPUT')

  def testIPv6Icmp(self):
    pol = policy.ParsePolicy(IPV6_HEADER_1 + IPV6_TERM_1, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('--icmpv6-type 1', result,
                  'icmpv6-type 1 (echo-reply) is missing')
    self.assertIn('--icmpv6-type 3', result,
                  'icmpv6-type 3 (destination-unreachable) is missing')
    self.assertIn('--icmpv6-type 129', result,
                  'icmpv6-type 129 (router-solicit) is missing')

  def testIPv6IcmpOrder(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IPv6('fd87:6044:ac54:3558::/64')]

    pol = policy.ParsePolicy(IPV6_HEADER_1 + ICMPV6_TERM_1, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)
    self.assertIn('-s fd87:6044:ac54:3558::/64 -p ipv6-icmp -m icmp6'
                  ' --icmpv6-type 1', result,
                  'incorrect order of ICMPv6 match elements')

    self.naming.GetNetAddr.assert_called_once_with('IPV6_INTERNAL')

  @mock.patch.object(iptables.logging, 'debug')
  def testIcmpv6InetMismatch(self, mock_debug):
    acl = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + IPV6_TERM_1,
                                               self.naming), EXP_INFO)
    # output happens in __str_
    str(acl)

    mock_debug.assert_called_once_with(
        'Term inet6-icmp will not be rendered,'
        ' as it has icmpv6 match specified but '
        'the ACL is of inet address family.')

  @mock.patch.object(iptables.logging, 'debug')
  def testIcmpInet6Mismatch(self, mock_debug):
    acl = iptables.Iptables(policy.ParsePolicy(IPV6_HEADER_1 +
                                               GOOD_TERM_1,
                                               self.naming), EXP_INFO)
    # output happens in __str_
    str(acl)

    mock_debug.assert_called_once_with(
        'Term good-term-1 will not be rendered,'
        ' as it has icmp match specified but '
        'the ACL is of inet6 address family.')

  def testOwner(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_10, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl).split('\n')
    self.assertIn('-A I_good-term-10 -m comment --comment "Owner: '
                  'foo@google.com"', result,
                  'missing or incorrect comment specification.')

  def testSetTarget(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    acl.SetTarget('OUTPUT', 'DROP')
    result = str(acl).split('\n')
    self.assertIn('-P OUTPUT DROP', result,
                  'output default policy of drop not set.')

  def testSetCustomTarget(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    acl.SetTarget('foobar')
    result = str(acl).split('\n')
    self.assertIn('-N foobar', result,
                  'did not find a new chain for foobar.')

  def testBuildTokens(self):
    pol1 = iptables.Iptables(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_5,
                                                self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    self.naming.GetServiceByProto.return_value = ['80']

    pol1 = iptables.Iptables(
        policy.ParsePolicy(GOOD_HEADER_1 + GOOD_WARNING_TERM,
                           self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testNoVerbose(self):
    pol = policy.ParsePolicy(GOOD_HEADER_7 + GOOD_TERM_12, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    self.assertNotIn('comment --comment "FOOO"', str(acl), acl)

  def testLogLimit(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_13, self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    self.assertIn(
        '-m --limit 99/day -j LOG --log-prefix good_term_13', str(acl), acl)

  def testLogLimitFailsWithoutLogging(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + BAD_LOGGING_TERM, self.naming)
    self.assertRaises(iptables.LimitButNoLogError,
                      iptables.Iptables, pol, EXP_INFO)

  def testSkipHopByHopinV4(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + HOPOPT_TERM + GOOD_TERM_1,
                             self.naming)
    acl = iptables.Iptables(pol, EXP_INFO)
    result = str(acl)

    self.assertNotIn('-m u32 --u32 "0x3&0xff=0x0"', result,
                     'match for hop-by-hop header is missing')


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


# tests/lib/juniper_test.py

# Copyright 2007 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for juniper acl rendering module."""

import datetime
import re
from absl.testing import absltest
from unittest import mock

from absl import logging
from absl.testing import parameterized
from capirca.lib import aclgenerator
from capirca.lib import juniper
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: juniper test-filter
}
"""
GOOD_HEADER_2 = """
header {
  target:: juniper test-filter bridge
}
"""
GOOD_HEADER_V6 = """
header {
  target:: juniper test-filter inet6
}
"""
GOOD_HEADER_MIXED = """
header {
  target:: juniper test-filter mixed
}
"""
GOOD_HEADER_BRIDGE = """
header {
  target:: juniper test-filter bridge
}
"""
GOOD_DSMO_HEADER = """
header {
  target:: juniper test-filter enable_dsmo
}
"""
GOOD_FILTER_ENHANCED_MODE_HEADER = """
header {
  target:: juniper test-filter filter_enhanced_mode
}
"""
GOOD_NOVERBOSE_V4_HEADER = """
header {
  target:: juniper test-filter inet noverbose
}
"""
GOOD_NOVERBOSE_V6_HEADER = """
header {
  target:: juniper test-filter inet6 noverbose
}
"""
GOOD_HEADER_NOT_INTERFACE_SPECIFIC = """
header {
  target:: juniper test-filter bridge not-interface-specific
}
"""
BAD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: cisco test-filter
}
"""
BAD_HEADER_2 = """
header {
  target:: juniper test-filter inetpoop
}
"""
EXPIRED_TERM = """
term is_expired {
  expiration:: 2001-01-01
  action:: accept
}
"""
EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}

term good-term-2 {
  protocol:: tcp
  destination-port:: SMTP
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_1_V6 = """
term good-term-1 {
  protocol:: icmpv6
  action:: accept
}

term good-term-2 {
  protocol:: tcp
  destination-port:: SMTP
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_2 = """
term good-term-3 {
  protocol:: tcp
  destination-address:: SOME_HOST
  source-port:: HTTP
  option:: established tcp-established
  action:: accept
}
"""
GOOD_TERM_3 = """
term good-term-3 {
  protocol:: icmp
  icmp-type:: echo-reply information-reply information-request
  icmp-type:: router-solicitation timestamp-request
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  protocol:: icmp
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_7 = """
term good-term-7 {
  protocol-except:: tcp
  action:: accept
}
"""
GOOD_TERM_8 = """
term good-term-8 {
  source-prefix:: foo_prefix_list
  destination-prefix:: bar_prefix_list baz_prefix_list
  action:: accept
}
"""
GOOD_TERM_9 = """
term good-term-9 {
  ether-type:: arp
  action:: accept
}
"""
GOOD_TERM_10 = """
term good-term-10 {
  traffic-type:: unknown-unicast
  action:: accept
}
"""
GOOD_TERM_11 = """
term good-term-11 {
  verbatim:: juniper "mary had a little lamb"
  verbatim:: iptables "mary had a second lamb"
  verbatim:: cisco "mary had a third lamb"
}
"""
GOOD_TERM_12 = """
term good-term-12 {
  source-address:: LOCALHOST
  action:: accept
}
"""
GOOD_TERM_13 = """
term routing-instance-setting {
  protocol:: tcp
  routing-instance:: EXTERNAL-NAT
}
"""
GOOD_TERM_14 = """
term loss-priority-setting {
  protocol:: tcp
  loss-priority:: low
  action:: accept
}
"""
GOOD_TERM_15 = """
term precedence-setting {
  protocol:: tcp
  destination-port:: SSH
  precedence:: 7
  action:: accept
}
"""
GOOD_TERM_16 = """
term precedence-setting {
  protocol:: tcp
  destination-port:: SSH
  precedence:: 5 7
  action:: accept
}
"""
GOOD_TERM_17 = """
term owner-term {
  owner:: foo@google.com
  action:: accept
}
"""
GOOD_TERM_18_SRC = """
term address-exclusions {
  source-address:: INTERNAL
  source-exclude:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_18_DST = """
term address-exclusions {
  destination-address:: INTERNAL
  destination-exclude:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_19 = """
term minimize-prefix-list {
  source-address:: INCLUDES
  source-exclude:: EXCLUDES
  action:: accept
}
"""
GOOD_TERM_V6_HOP_LIMIT = """
term good-term-v6-hl {
  hop-limit:: 25
  action:: accept
}
"""
GOOD_TERM_20_V6 = """
term good-term-20-v6 {
  protocol-except:: icmpv6
  action:: accept
}
"""
GOOD_TERM_21 = """
term good_term_21 {
  ttl:: 10
  action:: accept
}
"""
GOOD_TERM_22 = """
term good_term_22 {
  protocol:: tcp
  source-port:: DNS
  dscp-set:: b111000
  action:: accept
}
"""
GOOD_TERM_23 = """
term good_term_23 {
  protocol:: tcp
  source-port:: DNS
  dscp-set:: af42
  dscp-match:: af41-af42 5
  dscp-except:: be
  action:: accept
}
"""
GOOD_TERM_24 = """
term good_term_24 {
  protocol:: tcp
  source-port:: DNS
  qos:: af1
  action:: accept
}
"""
GOOD_TERM_25 = """
term good_term_25 {
  protocol:: tcp
  source-port:: DNS
  action:: accept
}
"""
GOOD_TERM_26 = """
term good_term_26 {
  protocol:: tcp
  source-port:: DNS
  action:: deny
}
"""
GOOD_TERM_26_V6 = """
term good_term_26-v6 {
  protocol:: tcp
  source-port:: DNS
  action:: deny
}
"""
GOOD_TERM_26_V6_REJECT = """
term good_term_26-v6 {
  protocol:: tcp
  source-port:: DNS
  action:: reject
}
"""
GOOD_TERM_27 = """
term good_term_27 {
  forwarding-class:: Floop
  action:: deny
}
"""
GOOD_TERM_28 = """
term good_term_28 {
  next-ip:: TEST_NEXT
}
"""
GOOD_TERM_29 = """
term multiple-forwarding-class {
  forwarding-class:: floop fluup fleep
  action:: deny
}
"""
GOOD_TERM_30 = """
term good-term-30 {
  source-prefix-except:: foo_prefix_list
  destination-prefix-except:: bar_prefix_list
  action:: accept
}
"""
GOOD_TERM_31 = """
term good-term-31 {
  source-prefix:: foo_prefix
  source-prefix-except:: foo_except
  destination-prefix:: bar_prefix
  destination-prefix-except:: bar_except
  action:: accept
}
"""
GOOD_TERM_32 = """
term good_term_32 {
  forwarding-class-except:: floop
  action:: deny
}
"""
GOOD_TERM_33 = """
term multiple-forwarding-class-except {
  forwarding-class-except:: floop fluup fleep
  action:: deny
}
"""
GOOD_TERM_34 = """
term good_term_34 {
  traffic-class-count:: floop
  action:: deny
}
"""
GOOD_TERM_35 = """
term good_term_35 {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 3 4
  action:: accept
}
"""
GOOD_TERM_36 = """
term good-term-36 {
  protocol:: tcp
  destination-address:: SOME_HOST
  destination-address:: SOME_HOST
  option:: inactive
  action:: accept
}
"""
GOOD_TERM_37 = """
term good-term-37 {
  destination-address:: SOME_HOST
  restrict-address-family:: inet
  action:: accept
}
"""
GOOD_TERM_COMMENT = """
term good-term-comment {
  comment:: "This is a COMMENT"
  action:: accept
}
"""
GOOD_TERM_FILTER = """
term good-term-filter {
  comment:: "This is a COMMENT"
  filter-term:: my-filter
}
"""
BAD_TERM_1 = """
term bad-term-1 {
  protocol:: tcp udp
  source-port:: DNS
  option:: tcp-established
  action:: accept
}
"""
ESTABLISHED_TERM_1 = """
term established-term-1 {
  protocol:: tcp
  source-port:: DNS
  option:: established
  action:: accept
}
"""
OPTION_TERM_1 = """
term option-term {
  protocol:: tcp
  source-port:: SSH
  option:: is-fragment
  action:: accept
}
"""
BAD_ICMPTYPE_TERM_1 = """
term icmptype-mismatch {
  comment:: "error when icmpv6 paired with inet filter"
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""
BAD_ICMPTYPE_TERM_2 = """
term icmptype-mismatch {
  comment:: "error when icmp paired with inet6 filter"
  protocol:: icmp
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""
DEFAULT_TERM_1 = """
term default-term-1 {
  action:: deny
}
"""
ENCAPSULATE_GOOD_TERM_1 = """
term good-term-1 {
  protocol:: tcp
  encapsulate:: template-name
}
"""
ENCAPSULATE_GOOD_TERM_2 = """
term good-term-2 {
  protocol:: tcp
  encapsulate:: template-name
  counter:: count-name
}
"""
ENCAPSULATE_BAD_TERM_1 = """
term bad-term-1 {
  protocol:: tcp
  encapsulate:: template-name
  action:: accept
}
"""
ENCAPSULATE_BAD_TERM_2 = """
term bad-term-2 {
  protocol:: tcp
  encapsulate:: template-name
  routing-instance:: instance-name
}
"""
DECAPSULATE_GOOD_TERM_1 = """
term good-term-1 {
  protocol:: udp
  decapsulate:: template-name
}
"""
DECAPSULATE_GOOD_TERM_2 = """
term good-term-2 {
  protocol:: udp
  decapsulate:: template-name
  counter:: count-name
}
"""
DECAPSULATE_BAD_TERM_1 = """
term bad-term-1 {
  protocol:: udp
  decapsulate:: template-name
  action:: accept
}
"""
DECAPSULATE_BAD_TERM_2 = """
term bad-term-2 {
  protocol:: udp
  decapsulate:: template-name
  routing-instance:: instance-name
}
"""
DECAPSULATE_BAD_TERM_3 = """
term bad-term-2 {
  protocol:: udp
  decapsulate:: template-name
  encapsulate:: something
}
"""
PORTMIRROR_GOOD_TERM_1 = """
term good-term-1 {
  protocol:: tcp
  port-mirror:: true
}
"""
PORTMIRROR_GOOD_TERM_2 = """
term good-term-2 {
  protocol:: tcp
  port-mirror:: true
  counter:: count-name
  action:: deny
}
"""
LONG_COMMENT_TERM_1 = """
term long-comment-term-1 {
  comment:: "this is very very very very very very very very very very very
  comment:: "very very very very very very very long."
  action:: deny
}
"""
LONG_POLICER_TERM_1 = """
term long-policer-term-1 {
  policer:: this-is-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-long
  action:: deny
}
"""
HOPOPT_TERM = """
term good-term-1 {
  protocol:: hopopt
  action:: accept
}
"""
HOPOPT_TERM_EXCEPT = """
term good-term-1 {
  protocol-except:: hopopt
  action:: accept
}
"""
FRAGOFFSET_TERM = """
term good-term-1 {
  fragment-offset:: 1-7
  action:: accept
}
"""
GOOD_FLEX_MATCH_TERM = """
term flex-match-term-1 {
  protocol:: tcp
  flexible-match-range:: bit-length 8
  flexible-match-range:: range 0x08
  flexible-match-range:: match-start payload
  flexible-match-range:: byte-offset 16
  flexible-match-range:: bit-offset 7
  action:: deny
}
"""
BAD_FLEX_MATCH_TERM_1 = """
term flex-match-term-1 {
  protocol:: tcp
  flexible-match-range:: bit-length 36
  flexible-match-range:: range 0x08
  flexible-match-range:: match-start payload
  flexible-match-range:: byte-offset 16
  flexible-match-range:: bit-offset 7
  action:: deny
}
"""
BAD_FLEX_MATCH_TERM_2 = """
term flex-match-term-1 {
  protocol:: tcp
  flexible-match-range:: bit-length 8
  flexible-match-range:: range 0x08
  flexible-match-range:: match-start wrong
  flexible-match-range:: byte-offset 16
  flexible-match-range:: bit-offset 7
  action:: deny
}
"""
BAD_FLEX_MATCH_TERM_3 = """
term flex-match-term-1 {
  protocol:: tcp
  flexible-match-range:: bit-length 8
  flexible-match-range:: range 0x08
  flexible-match-range:: match-start payload
  flexible-match-range:: byte-offset 260
  flexible-match-range:: bit-offset 7
  action:: deny
}
"""
BAD_FLEX_MATCH_TERM_4 = """
term flex-match-term-1 {
  protocol:: tcp
  flexible-match-range:: bit-length 8
  flexible-match-range:: range 0x08
  flexible-match-range:: match-start payload
  flexible-match-range:: byte-offset 16
  flexible-match-range:: bit-offset 8
  action:: deny
}
"""
BAD_TERM_FILTER = """
term bad_term_filter {
  filter-term:: my-filter
  action:: deny
}
"""

MIXED_TESTING_TERM = """
term good-term {
  protocol:: tcp
  source-address:: SOME_HOST
  destination-port:: SMTP
  destination-address:: SOME_OTHER_HOST
  action:: accept
}
"""

SUPPORTED_TOKENS = frozenset([
    'action',
    'address',
    'comment',
    'counter',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'destination_prefix',
    'destination_prefix_except',
    'dscp_except',
    'dscp_match',
    'dscp_set',
    'decapsulate',
    'encapsulate',
    'ether_type',
    'expiration',
    'filter_term',
    'flexible_match_range',
    'forwarding_class',
    'forwarding_class_except',
    'fragment_offset',
    'hop_limit',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'loss_priority',
    'name',
    'next_ip',
    'option',
    'owner',
    'packet_length',
    'platform',
    'platform_exclude',
    'policer',
    'port',
    'port_mirror',
    'precedence',
    'protocol',
    'protocol_except',
    'qos',
    'restrict_address_family',
    'routing_instance',
    'source_address',
    'source_address_exclude',
    'source_port',
    'source_prefix',
    'source_prefix_except',
    'traffic_class_count',
    'traffic_type',
    'translated',
    'ttl',
    'verbatim'])

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'first-fragment',
               'inactive',
               'is-fragment',
               '.*',   # not actually a lex token!
               'sample',
               'tcp-established',
               'tcp-initial'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class JuniperTest(parameterized.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testOptions(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['80']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_2,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('destination-port 1024-65535;', output, output)
    # Verify that tcp-established; doesn't get duplicated if both 'established'
    # and 'tcp-established' options are included in term
    self.assertEqual(output.count('tcp-established;'), 1)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('HTTP', 'tcp')

  def testTermAndFilterName(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('term good-term-1 {', output, output)
    self.assertIn('replace: filter test-filter {', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testBadFilterType(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(BAD_HEADER_2 + GOOD_TERM_1, self.naming)
    self.assertRaises(aclgenerator.UnsupportedAFError, juniper.Juniper,
                      pol, EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testBridgeFilterType(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_1,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('ip-protocol tcp;', output, output)
    self.assertNotIn(' destination-address {', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testCommentShrinking(self):
    long_comment = ' this is a very descriptive comment ' * 10
    expected = (
        ' ' * 24 + '/* this is a very descriptive comment  this is a\n' +
        ' ' * 25 + '** very descriptive comment  this is a very\n' +
        ' ' * 25 + '** descriptive comment  this is a very descript */'
    )
    self.naming.GetNetAddr.return_value = (
        [nacaddr.IPv4('10.0.0.0/8', comment=long_comment)])
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn(expected, output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testDefaultDeny(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + DEFAULT_TERM_1,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertNotIn('from {', output, output)

  def testEncapsulate(self):
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + ENCAPSULATE_GOOD_TERM_1, self.naming),
        EXP_INFO)
    output = str(jcl)
    self.assertIn('encapsulate template-name;', output, output)
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + ENCAPSULATE_GOOD_TERM_2, self.naming),
        EXP_INFO)
    output = str(jcl)
    self.assertIn('encapsulate template-name;', output, output)
    self.assertIn('count count-name;', output, output)

  def testFailEncapsulate(self):
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + ENCAPSULATE_BAD_TERM_1, self.naming),
        EXP_INFO)
    self.assertRaises(juniper.JuniperMultipleTerminatingActionError, str, jcl)
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + ENCAPSULATE_BAD_TERM_2, self.naming),
        EXP_INFO)
    self.assertRaises(juniper.JuniperMultipleTerminatingActionError, str, jcl)

  def testDecapsulate(self):
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + DECAPSULATE_GOOD_TERM_1, self.naming),
        EXP_INFO)
    output = str(jcl)
    self.assertIn('decapsulate template-name;', output, output)
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + DECAPSULATE_GOOD_TERM_2, self.naming),
        EXP_INFO)
    output = str(jcl)
    self.assertIn('decapsulate template-name;', output, output)
    self.assertIn('count count-name;', output, output)

  def testFailDecapsulate(self):
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + DECAPSULATE_BAD_TERM_1, self.naming),
        EXP_INFO)
    self.assertRaises(juniper.JuniperMultipleTerminatingActionError, str, jcl)
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + DECAPSULATE_BAD_TERM_2, self.naming),
        EXP_INFO)
    self.assertRaises(juniper.JuniperMultipleTerminatingActionError, str, jcl)
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + DECAPSULATE_BAD_TERM_3, self.naming),
        EXP_INFO)
    self.assertRaises(juniper.JuniperMultipleTerminatingActionError, str, jcl)

  def testPortMirror(self):
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + PORTMIRROR_GOOD_TERM_1, self.naming),
        EXP_INFO)
    output = str(jcl)
    self.assertIn('port-mirror;', output, output)
    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + PORTMIRROR_GOOD_TERM_2, self.naming),
        EXP_INFO)
    output = str(jcl)
    self.assertIn('port-mirror;', output, output)
    self.assertIn('count count-name;', output, output)
    self.assertIn('discard;', output, output)

  def testIcmpType(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_3,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    # verify proper translation from policy icmp-type text to juniper-esque
    self.assertIn(' icmp-type [', output, output)
    self.assertIn(' 0 ', output, output)
    self.assertIn(' 15 ', output, output)
    self.assertIn(' 10 ', output, output)
    self.assertIn(' 13 ', output, output)
    self.assertIn(' 16 ', output, output)
    self.assertIn('];', output, output)

  def testIcmpCode(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_35,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('icmp-code [ 3 4 ];', output, output)

  def testInactiveTerm(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_36,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('inactive: term good-term-36 {', output)

  def testInet6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001::/33')]
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER_V6 + GOOD_TERM_1_V6,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertTrue('next-header icmp6;' in output and
                    'next-header tcp;' in output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testNotInterfaceSpecificHeader(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER_NOT_INTERFACE_SPECIFIC + GOOD_TERM_1,
                           self.naming), EXP_INFO)
    output = str(jcl)
    self.assertNotIn('interface-specific;', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testInterfaceSpecificHeader(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('interface-specific;', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testFilterEnhancedModeHeader(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_FILTER_ENHANCED_MODE_HEADER + GOOD_TERM_1,
                           self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('enhanced-mode;', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testHopLimit(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER_V6 +
                                             GOOD_TERM_V6_HOP_LIMIT,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('hop-limit 25;', output, output)

  def testHopLimitInet(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER +
                                             GOOD_TERM_V6_HOP_LIMIT,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertNotIn('hop-limit 25;', output, output)

  def testProtocolExcept(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER_V6 + GOOD_TERM_7,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('next-header-except tcp;', output, output)

  def testIcmpv6Except(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER_V6 + GOOD_TERM_20_V6,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('next-header-except icmp6;', output, output)

  def testProtocolCase(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_5,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('protocol [ icmp tcp ];', output, output)

  def testPrefixList(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_8,
                                             self.naming), EXP_INFO)
    spfx_re = re.compile(r'source-prefix-list {\W+foo_prefix_list;\W+}')
    dpfx_re = re.compile(
        r'destination-prefix-list {\W+bar_prefix_list;\W+baz_prefix_list;\W+}')
    output = str(jcl)
    self.assertTrue(spfx_re.search(output), output)
    self.assertTrue(dpfx_re.search(output), output)

  def testPrefixListExcept(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_30,
                                             self.naming), EXP_INFO)
    spfx_re = re.compile(r'source-prefix-list {\W+foo_prefix_list except;\W+}')
    dpfx_re = re.compile(
        r'destination-prefix-list {\W+bar_prefix_list except;\W+}')
    output = str(jcl)
    self.assertTrue(spfx_re.search(output), output)
    self.assertTrue(dpfx_re.search(output), output)

  def testPrefixListMixed(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_31,
                                             self.naming), EXP_INFO)
    spfx_re = re.compile(r'source-prefix-list {\W+foo_prefix;\W+'
                         r'foo_except except;\W+}')
    dpfx_re = re.compile(r'destination-prefix-list {\W+bar_prefix;\W+'
                         r'bar_except except;\W+}')
    output = str(jcl)
    self.assertTrue(spfx_re.search(output), output)
    self.assertTrue(dpfx_re.search(output), output)

  def testEtherType(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_9,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('ether-type arp;', output, output)

  def testTrafficType(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_10,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('traffic-type unknown-unicast;', output, output)

  def testVerbatimTerm(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_11,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('mary had a little lamb', output, output)
    # check if other platforms verbatim shows up in output
    self.assertNotIn('mary had a second lamb', output, output)
    self.assertNotIn('mary had a third lamb', output, output)

  def testDscpByte(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER + GOOD_TERM_22
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('dscp b111000;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testDscpClass(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER + GOOD_TERM_23
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('dscp af42;', output, output)
    self.assertIn('dscp [ af41-af42 5 ];', output, output)
    self.assertIn('dscp-except [ be ];', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testDscpIPv6(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER_V6 + GOOD_TERM_23
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('traffic-class af42;', output, output)
    self.assertIn('traffic-class [ af41-af42 5 ];', output, output)
    self.assertIn('traffic-class-except [ be ];', output, output)
    self.assertNotIn('dscp', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testSimplifiedThenStatement(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER + GOOD_TERM_24
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('forwarding-class af1', output, output)
    self.assertIn('accept', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testSimplifiedThenStatementWithSingleAction(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER + GOOD_TERM_25
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('then accept;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testSimplifiedThenStatementWithSingleActionDiscardIPv4(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER + GOOD_TERM_26
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('then {', output, output)
    self.assertIn('discard;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testSimplifiedThenStatementWithSingleActionDiscardIPv6(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER_V6 + GOOD_TERM_26_V6
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('then discard;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testSimplifiedThenStatementWithSingleActionRejectIPv6(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER_V6 + GOOD_TERM_26_V6_REJECT
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('then {', output, output)
    self.assertIn('reject;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testTcpEstablished(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER + ESTABLISHED_TERM_1
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('tcp-established', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testNonTcpWithTcpEstablished(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER + BAD_TERM_1
    pol_obj = policy.ParsePolicy(policy_text, self.naming)
    jcl = juniper.Juniper(pol_obj, EXP_INFO)
    self.assertRaises(juniper.TcpEstablishedWithNonTcpError, str, jcl)

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'), mock.call('DNS', 'udp')])

  def testMixedFilterInetType(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IPv4('127.0.0.1'), nacaddr.IPv6('::1/128')]

    jcl = juniper.Juniper(policy.ParsePolicy(
        GOOD_HEADER_MIXED + GOOD_TERM_12, self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('test-filter4', output, output)
    self.assertIn('127.0.0.1', output, output)
    self.assertIn('test-filter6', output, output)
    self.assertIn('::1/128', output, output)

    self.naming.GetNetAddr.assert_called_once_with('LOCALHOST')

  def testRestrictAddressFamilyType(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IPv4('127.0.0.1'), nacaddr.IPv6('::1/128')]

    jcl = juniper.Juniper(policy.ParsePolicy(
        GOOD_HEADER_MIXED + GOOD_TERM_37, self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('127.0.0.1', output, output)
    self.assertNotIn('::1/128', output, output)
    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testBridgeFilterInetType(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IPv4('127.0.0.1'), nacaddr.IPv6('::1/128')]

    jcl = juniper.Juniper(policy.ParsePolicy(
        GOOD_HEADER_BRIDGE + GOOD_TERM_12, self.naming), EXP_INFO)
    output = str(jcl)
    self.assertNotIn('::1/128', output, output)

    self.naming.GetNetAddr.assert_called_once_with('LOCALHOST')

  def testNoVerboseV4(self):
    addr_list = list()
    for octet in range(0, 256):
      net = nacaddr.IP('192.168.' + str(octet) + '.64/27')
      addr_list.append(net)
    self.naming.GetNetAddr.return_value = addr_list
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(
        policy.ParsePolicy(
            GOOD_NOVERBOSE_V4_HEADER + GOOD_TERM_1 + GOOD_TERM_COMMENT,
            self.naming), EXP_INFO)
    self.assertIn('192.168.0.64/27;', str(jcl))
    self.assertNotIn('COMMENT', str(jcl))
    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testNoVerboseV6(self):
    addr_list = list()
    for octet in range(0, 256):
      net = nacaddr.IPv6('2001:db8:1010:' + str(octet) + '::64/64',
                         strict=False)
      addr_list.append(net)
    self.naming.GetNetAddr.return_value = addr_list
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(
        policy.ParsePolicy(
            GOOD_NOVERBOSE_V6_HEADER + GOOD_TERM_1 + GOOD_TERM_COMMENT,
            self.naming), EXP_INFO)
    self.assertIn('2001:db8:1010:90::/61;', str(jcl))
    self.assertNotIn('COMMENT', str(jcl))
    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testDsmo(self):
    addr_list = list()
    for octet in range(0, 256):
      net = nacaddr.IP('192.168.' + str(octet) + '.64/27')
      addr_list.append(net)
    self.naming.GetNetAddr.return_value = addr_list
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_DSMO_HEADER + GOOD_TERM_1,
                                             self.naming), EXP_INFO)
    self.assertIn('192.168.0.64/255.255.0.224;', str(jcl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testDsmoJuniperFriendly(self):
    addr_list = [nacaddr.IP('192.168.%d.0/24' % octet) for octet in range(256)]
    self.naming.GetNetAddr.return_value = addr_list
    self.naming.GetServiceByProto.return_value = ['25']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_DSMO_HEADER + GOOD_TERM_1,
                                             self.naming), EXP_INFO)
    self.assertIn('192.168.0.0/16;', str(jcl))

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testDsmoExclude(self):
    big = nacaddr.IPv4('0.0.0.0/1')
    ip1 = nacaddr.IPv4('192.168.0.64/27')
    ip2 = nacaddr.IPv4('192.168.1.64/27')
    terms = (GOOD_TERM_18_SRC, GOOD_TERM_18_DST)
    self.naming.GetNetAddr.side_effect = [[big], [ip1, ip2]] * len(terms)

    mock_calls = []
    for term in terms:
      jcl = juniper.Juniper(
          policy.ParsePolicy(GOOD_DSMO_HEADER + term, self.naming),
          EXP_INFO)
      self.assertIn('192.168.0.64/255.255.254.224 except;', str(jcl))
      mock_calls.append(mock.call('INTERNAL'))
      mock_calls.append(mock.call('SOME_HOST'))

    self.naming.GetNetAddr.assert_has_calls(mock_calls)

  def testTermTypeIndexKeys(self):
    # ensure an _INET entry for each _TERM_TYPE entry
    self.assertEqual(sorted(juniper.Term._TERM_TYPE.keys()),
                     sorted(juniper.Term.AF_MAP.keys()))

  def testRoutingInstance(self):
    jcl = juniper.Juniper(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_13, self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('routing-instance EXTERNAL-NAT;', output, output)

  def testLossPriority(self):
    jcl = juniper.Juniper(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_14, self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('loss-priority low;', output, output)

  def testPrecedence(self):
    self.naming.GetServiceByProto.return_value = ['22']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_15,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('precedence 7;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testMultiplePrecedence(self):
    self.naming.GetServiceByProto.return_value = ['22']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_16,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('precedence [ 5 7 ];', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testFilterTerm(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_FILTER,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('filter my-filter;', output, output)

  def testFilterActionTerm(self):
    with self.assertRaises(policy.InvalidTermActionError):
      policy.ParsePolicy(GOOD_HEADER + BAD_TERM_FILTER, self.naming)

  def testArbitraryOptions(self):
    self.naming.GetServiceByProto.return_value = ['22']

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + OPTION_TERM_1,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('is-fragment;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  @mock.patch.object(juniper.logging, 'debug')
  def testIcmpv6InetMismatch(self, mock_debug):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + BAD_ICMPTYPE_TERM_1,
                                             self.naming), EXP_INFO)
    # output happens in __str_
    str(jcl)

    mock_debug.assert_called_once_with(
        'Term icmptype-mismatch will not be rendered,'
        ' as it has icmpv6 match specified but '
        'the ACL is of inet address family.')

  @mock.patch.object(juniper.logging, 'debug')
  def testIcmpInet6Mismatch(self, mock_debug):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER_V6 +
                                             BAD_ICMPTYPE_TERM_2,
                                             self.naming), EXP_INFO)
    # output happens in __str_
    str(jcl)

    mock_debug.assert_called_once_with(
        'Term icmptype-mismatch will not be rendered,'
        ' as it has icmp match specified but '
        'the ACL is of inet6 address family.')

  @mock.patch.object(juniper.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    _ = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + EXPIRED_TERM,
                                           self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and will '
        'not be rendered.', 'is_expired', 'test-filter')

  @mock.patch.object(juniper.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    _ = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + EXPIRING_TERM %
                                           exp_date.strftime('%Y-%m-%d'),
                                           self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring', 'test-filter')

  def testOwnerTerm(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_17,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('            /*\n'
                  '             ** Owner: foo@google.com\n'
                  '             */', output, output)

  def testAddressExclude(self):
    big = nacaddr.IPv4('0.0.0.0/1')
    ip1 = nacaddr.IPv4('10.0.0.0/8')
    ip2 = nacaddr.IPv4('172.16.0.0/12')
    terms = (GOOD_TERM_18_SRC, GOOD_TERM_18_DST)
    self.naming.GetNetAddr.side_effect = [[big, ip1, ip2], [ip1]] * len(terms)

    mock_calls = []
    for term in terms:
      jcl = juniper.Juniper(
          policy.ParsePolicy(GOOD_HEADER + term, self.naming),
          EXP_INFO)
      output = str(jcl)
      self.assertIn('10.0.0.0/8 except;', output, output)
      self.assertNotIn('10.0.0.0/8;', output, output)
      self.assertIn('172.16.0.0/12;', output, output)
      self.assertNotIn('172.16.0.0/12 except;', output, output)
      mock_calls.append(mock.call('INTERNAL'))
      mock_calls.append(mock.call('SOME_HOST'))

    self.naming.GetNetAddr.assert_has_calls(mock_calls)

  def testMinimizePrefixes(self):
    includes = ['1.0.0.0/8', '2.0.0.0/8']
    excludes = ['1.1.1.1/32', '2.0.0.0/8', '3.3.3.3/32']

    expected = ['1.0.0.0/8;',
                '1.1.1.1/32 except;']
    unexpected = ['2.0.0.0/8;',
                  '2.0.0.0/8 except;',
                  '3.3.3.3/32']

    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4(ip) for ip in includes],
        [nacaddr.IPv4(ip) for ip in excludes]]

    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_19, self.naming),
        EXP_INFO)
    output = str(jcl)
    for result in expected:
      self.assertIn(result, output,
                    'expected "%s" in %s' % (result, output))
    for result in unexpected:
      self.assertNotIn(result, output,
                       'unexpected "%s" in %s' % (result, output))

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('INCLUDES'), mock.call('EXCLUDES')])

  def testNoMatchReversal(self):
    includes = ['10.0.0.0/8', '10.0.0.0/10']
    excludes = ['10.0.0.0/9']

    expected = ['10.0.0.0/8;',
                '10.0.0.0/10;',
                '10.0.0.0/9 except;']

    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4(ip) for ip in includes],
        [nacaddr.IPv4(ip) for ip in excludes]]

    jcl = juniper.Juniper(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_19, self.naming),
        EXP_INFO)
    output = str(jcl)
    for result in expected:
      self.assertIn(result, output)

  def testConfigHelper(self):
    config = juniper.Config()
    config.Append('test {')
    config.Append('blah {')
    config.Append('foo;')
    config.Append('bar;')
    config.Append('}')  # close blah{}
    config.Append(' Mr. T Pities the fool!', verbatim=True)

    # haven't closed everything yet
    self.assertRaises(juniper.JuniperIndentationError, lambda: str(config))

    config.Append('}')  # close test{}
    self.assertMultiLineEqual(str(config),
                              'test {\n'
                              '    blah {\n'
                              '        foo;\n'
                              '        bar;\n'
                              '    }\n'
                              ' Mr. T Pities the fool!\n'
                              '}')

    # one too many '}'
    self.assertRaises(juniper.JuniperIndentationError,
                      lambda: config.Append('}'))

  def testForwardingClass(self):
    policy_text = GOOD_HEADER + GOOD_TERM_27
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('forwarding-class Floop;', output, output)

  def testForwardingClassExcept(self):
    policy_text = GOOD_HEADER + GOOD_TERM_32
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('forwarding-class-except floop;', output, output)

  def testTrafficClassCount(self):
    policy_text = GOOD_HEADER + GOOD_TERM_34
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('traffic-class-count floop;', output, output)

  def testFragmentOffset(self):
    policy_text = GOOD_HEADER + FRAGOFFSET_TERM
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('fragment-offset 1-7;', output, output)

  def testMultipleForwardingClass(self):
    policy_text = GOOD_HEADER + GOOD_TERM_29
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('forwarding-class [ floop fluup fleep ];', output,
                  output)

  def testMultipleForwardingClassExcept(self):
    policy_text = GOOD_HEADER + GOOD_TERM_33
    jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                          EXP_INFO)
    output = str(jcl)
    self.assertIn('forwarding-class-except [ floop fluup fleep ];', output,
                  output)

  def testLongPolicer(self):
    with mock.patch.object(juniper.logging, 'warning',
                           spec=logging.warn) as warn:
      policy_text = GOOD_HEADER + LONG_POLICER_TERM_1
      jcl = juniper.Juniper(policy.ParsePolicy(policy_text, self.naming),
                            EXP_INFO)
      _ = str(jcl)
      warn.assert_called_with('WARNING: %s is longer than %d bytes. Due to'
                              ' limitation in JUNOS, OIDs longer than %dB'
                              ' can cause SNMP timeout issues.', 'this-is-very'
                              '-very-very-very-very-very-very-very-very-very'
                              '-very-very-very-very-very-very-very-very-very'
                              '-very-very-very-very-very-very-very-very-very'
                              '-very-very-long', 128, 128)

  def testNextIp(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_28,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn(
        ('next-ip 10.1.1.1/32'), output)

    self.naming.GetNetAddr.assert_called_once_with('TEST_NEXT')

  def testTTL(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_21,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('ttl 10;', output)

  def testTTLInet6(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER_V6 + GOOD_TERM_21,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertNotIn('ttl 10;', output)

  def testNextIpFormat(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/32')]

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_28,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn(
        ('                then {\n'
         '                    next-ip 10.1.1.1/32;\n'
         '                }'), output)

    self.naming.GetNetAddr.assert_called_once_with('TEST_NEXT')

  def testNextIpv6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001::/128')]

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_28,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn(
        ('next-ip6 2001::/128;'), output)

    self.naming.GetNetAddr.assert_called_once_with('TEST_NEXT')

  def testFailNextIpMultipleIP(self):
    self.naming.GetNetAddr.return_value = [
        nacaddr.IP('10.1.1.1/32'), nacaddr.IP('192.168.1.1/32')]
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_28,
                                             self.naming), EXP_INFO)
    self.assertRaises(juniper.JuniperNextIpError, str, jcl)

    self.naming.GetNetAddr.assert_called_once_with('TEST_NEXT')

  def testFailNextIpNetworkIP(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/26',
                                                      strict=False)]

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_28,
                                             self.naming), EXP_INFO)
    self.assertRaises(juniper.JuniperNextIpError, str, jcl)

    self.naming.GetNetAddr.assert_called_once_with('TEST_NEXT')

  def testBuildTokens(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.1.1.1/26',
                                                      strict=False)]

    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_28,
                                             self.naming), EXP_INFO)
    st, sst = jcl._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_28,
                                             self.naming), EXP_INFO)
    st, sst = jcl._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testHopOptProtocol(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + HOPOPT_TERM,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('protocol hop-by-hop;', output, output)
    self.assertNotIn('protocol hopopt;', output, output)

  def testHopOptProtocolExcept(self):
    jcl = juniper.Juniper(policy.ParsePolicy(GOOD_HEADER + HOPOPT_TERM_EXCEPT,
                                             self.naming), EXP_INFO)
    output = str(jcl)
    self.assertIn('protocol-except hop-by-hop;', output, output)
    self.assertNotIn('protocol-except hopopt;', output, output)

  def testFlexibleMatch(self):
    jcl = juniper.Juniper(policy.ParsePolicy(
        GOOD_HEADER + GOOD_FLEX_MATCH_TERM, self.naming), EXP_INFO)

    output = str(jcl)

    flexible_match_expected = [
        'flexible-match-range {',
        'bit-length 8;',
        'range 0x08;',
        'match-start payload;',
        'byte-offset 16;',
        'bit-offset 7;'
    ]

    self.assertEqual(all([x in output for x in flexible_match_expected]), True)

  def testFlexibleMatchIPv6(self):
    jcl = juniper.Juniper(policy.ParsePolicy(
        GOOD_HEADER_V6 + GOOD_FLEX_MATCH_TERM, self.naming), EXP_INFO)
    output = str(jcl)

    flexible_match_expected = [
        'flexible-match-range {',
        'bit-length 8;',
        'range 0x08;',
        'match-start payload;',
        'byte-offset 16;',
        'bit-offset 7;'
    ]

    self.assertEqual(all([x in output for x in flexible_match_expected]), True)

  def testFailIsFragmentInV6(self):
    self.naming.GetServiceByProto.return_value = ['22']
    pol = policy.ParsePolicy(GOOD_HEADER_V6 + OPTION_TERM_1, self.naming)

    self.assertRaises(juniper.JuniperFragmentInV6Error, juniper.Juniper, pol,
                      EXP_INFO)

  def testFailFlexibleMatch(self):

    # bad bit-length
    self.assertRaises(policy.FlexibleMatchError,
                      policy.ParsePolicy,
                      GOOD_HEADER + BAD_FLEX_MATCH_TERM_1,
                      self.naming)
    self.assertRaises(policy.FlexibleMatchError,
                      policy.ParsePolicy,
                      GOOD_HEADER_V6 + BAD_FLEX_MATCH_TERM_1,
                      self.naming)

    # bad match-start
    self.assertRaises(policy.FlexibleMatchError,
                      policy.ParsePolicy,
                      GOOD_HEADER + BAD_FLEX_MATCH_TERM_2,
                      self.naming)
    self.assertRaises(policy.FlexibleMatchError,
                      policy.ParsePolicy,
                      GOOD_HEADER_V6 + BAD_FLEX_MATCH_TERM_2,
                      self.naming)

    # bad byte-offset
    self.assertRaises(policy.FlexibleMatchError,
                      policy.ParsePolicy,
                      GOOD_HEADER + BAD_FLEX_MATCH_TERM_3,
                      self.naming)
    self.assertRaises(policy.FlexibleMatchError,
                      policy.ParsePolicy,
                      GOOD_HEADER_V6 + BAD_FLEX_MATCH_TERM_3,
                      self.naming)

    # bad bit-offset
    self.assertRaises(policy.FlexibleMatchError,
                      policy.ParsePolicy,
                      GOOD_HEADER + BAD_FLEX_MATCH_TERM_4,
                      self.naming)
    self.assertRaises(policy.FlexibleMatchError,
                      policy.ParsePolicy,
                      GOOD_HEADER_V6 + BAD_FLEX_MATCH_TERM_4,
                      self.naming)

  @parameterized.named_parameters(
      ('MIXED_TO_V4',
       [[nacaddr.IPv4('0.0.0.0/1'),
         nacaddr.IPv6('2001::/33')], [nacaddr.IPv4('192.168.0.0/24')]], [
             '            term good-term {\n' +
             '                from {\n' +
             '                    source-address {\n' +
             '                        0.0.0.0/1;\n' +
             '                    }\n' +
             '                    destination-address {\n' +
             '                        192.168.0.0/24;\n' +
             '                    }'
         ], ['2001::/33']),
      ('V4_TO_MIXED', [
          [nacaddr.IPv4('192.168.0.0/24')],
          [nacaddr.IPv4('0.0.0.0/1'),
           nacaddr.IPv6('2001::/33')],
      ], [
          '            term good-term {\n' +
          '                from {\n' +
          '                    source-address {\n' +
          '                        192.168.0.0/24;\n' +
          '                    }\n' +
          '                    destination-address {\n' +
          '                        0.0.0.0/1;\n' +
          '                    }'
      ], ['2001::/33']),
      ('MIXED_TO_V6',
       [[nacaddr.IPv4('0.0.0.0/1'),
         nacaddr.IPv6('2001::/33')], [nacaddr.IPv6('2201::/48')]], [
             '            term good-term {\n' +
             '                from {\n' +
             '                    source-address {\n' +
             '                        2001::/33;\n' +
             '                    }\n' +
             '                    destination-address {\n' +
             '                        2201::/48;\n' +
             '                    }'
         ], ['0.0.0.0/1']),
      ('V6_TO_MIXED', [[
          nacaddr.IPv6('2201::/48')
      ], [nacaddr.IPv4('0.0.0.0/1'),
          nacaddr.IPv6('2001::/33')]], [
              '            term good-term {\n' +
              '                from {\n' +
              '                    source-address {\n' +
              '                        2201::/48;\n' +
              '                    }\n' +
              '                    destination-address {\n' +
              '                        2001::/33;\n' +
              '                    }'
        ], ['0.0.0.0/1']),
      ('MIXED_TO_MIXED', [[
          nacaddr.IPv4('0.0.0.0/1'),
          nacaddr.IPv6('2001::/33')
      ], [nacaddr.IPv4('192.168.0.0/24'),
          nacaddr.IPv6('2201::/48')]], [
              '            term good-term {\n' +
              '                from {\n' +
              '                    source-address {\n' +
              '                        0.0.0.0/1;\n' +
              '                    }\n' +
              '                    destination-address {\n' +
              '                        192.168.0.0/24;\n' +
              '                    }',
              '            term good-term {\n' +
              '                from {\n' +
              '                    source-address {\n' +
              '                        2001::/33;\n' +
              '                    }\n' +
              '                    destination-address {\n' +
              '                        2201::/48;\n' +
              '                    }'
        ], []),
      ('V4_TO_V4', [[nacaddr.IPv4('0.0.0.0/1')],
                    [nacaddr.IPv4('192.168.0.0/24')]], [
                        '            term good-term {\n' +
                        '                from {\n' +
                        '                    source-address {\n' +
                        '                        0.0.0.0/1;\n' +
                        '                    }\n' +
                        '                    destination-address {\n' +
                        '                        192.168.0.0/24;\n' +
                        '                    }'
      ], []),
      ('V6_TO_V6', [[nacaddr.IPv6('2001::/33')], [nacaddr.IPv6('2201::/48')]], [
          '            term good-term {\n' +
          '                from {\n' +
          '                    source-address {\n' +
          '                        2001::/33;\n' +
          '                    }\n' +
          '                    destination-address {\n' +
          '                        2201::/48;\n' +
          '                    }'
      ], []),
      (
          'V4_TO_V6',
          [[nacaddr.IPv4('0.0.0.0/1')], [nacaddr.IPv6('2201::/48')]],
          [],
          ['0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48'],
      ),
      (
          'V6_TO_V4',
          [[nacaddr.IPv6('2001::/33')], [nacaddr.IPv4('192.168.0.0/24')]],
          [],
          ['0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48'],
      ),
      (
          'PARTLY_UNSPECIFIED',
          [[nacaddr.IPv6('2001::/33')], [nacaddr.IPv4('192.168.0.0/24')]],
          ['term good_term_25 '],
          [
              '0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48',
              'term good-term-both-icmp-and-icmpv6-'
          ],
      ),
  )
  def testMixed(self, addresses, expected, notexpected):
    self.naming.GetNetAddr.side_effect = addresses
    self.naming.GetServiceByProto.return_value = ['25']
    jcl = juniper.Juniper(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + MIXED_TESTING_TERM + GOOD_TERM_25, self.naming),
        EXP_INFO)
    output = str(jcl)
    for expect in expected:
      self.assertIn(expect, output, output)
    for notexpect in notexpected:
      self.assertNotIn(notexpect, output, output)


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


# tests/lib/juniperevo_test.py

# Copyright 2022 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for juniper evo acl rendering module."""

from unittest import mock

from absl.testing import absltest
from absl.testing import parameterized
from capirca.lib import juniperevo
from capirca.lib import naming
from capirca.lib import policy


GOOD_HEADER_1 = """
header {
  comment:: "this is a test acl"
  target:: juniperevo test-filter inet6 ingress
}
"""
GOOD_HEADER_2 = """
header {
  comment:: "this is a test acl"
  target:: juniperevo test-filter inet6 ingress loopback
}
"""
GOOD_HEADER_3 = """
header {
  comment:: "this is a test acl"
  target:: juniperevo test-filter inet6 egress physical
}
"""
GOOD_HEADER_4 = """
header {
  comment:: "this is a test acl"
  target:: juniperevo test-filter inet6 egress loopback
}
"""
BAD_HEADER_1 = """
header {
  comment:: "this is a test acl"
  target:: juniperevo test-filter inet6
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: hopopt
  action:: deny
}
"""
GOOD_TERM_2 = """
term good-term-2 {
  protocol-except:: hopopt
  action:: accept
}
"""
GOOD_TERM_3 = """
term good-term-3 {
  protocol:: fragment
  action:: accept
}
"""
GOOD_TERM_4 = """
term good-term-4 {
  protocol-except:: fragment
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_6 = """
term good-term-6 {
  protocol-except:: tcp
  action:: accept
}
"""

SUPPORTED_TOKENS = frozenset([
    'action',
    'address',
    'comment',
    'counter',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'destination_prefix',
    'destination_prefix_except',
    'dscp_except',
    'dscp_match',
    'dscp_set',
    'encapsulate',
    'ether_type',
    'expiration',
    'filter_term',
    'flexible_match_range',
    'forwarding_class',
    'forwarding_class_except',
    'fragment_offset',
    'hop_limit',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'loss_priority',
    'name',
    'next_ip',
    'option',
    'owner',
    'packet_length',
    'platform',
    'platform_exclude',
    'policer',
    'port',
    'port_mirror',
    'precedence',
    'protocol',
    'protocol_except',
    'qos',
    'restrict_address_family',
    'routing_instance',
    'source_address',
    'source_address_exclude',
    'source_port',
    'source_prefix',
    'source_prefix_except',
    'traffic_class_count',
    'traffic_type',
    'translated',
    'ttl',
    'verbatim'])

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'first-fragment',
               'inactive',
               'is-fragment',
               '.*',   # not actually a lex token!
               'sample',
               'tcp-established',
               'tcp-initial'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class JuniperEvoTest(parameterized.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testIPv6HopOptProtocolIngressPhysical(self):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                GOOD_HEADER_1 + GOOD_TERM_1, self.naming),
            EXP_INFO))
    self.assertIn('next-header hop-by-hop;', output,
                  'missing or incorrect HOPOPT specification')
    self.assertNotIn('payload-protocol hop-by-hop;', output,
                     'missing or incorrect HOPOPT specification')
    self.assertNotIn('next-header 0;', output,
                     'missing or incorrect HOPOPT specification')
    self.assertNotIn('payload-protocol 0;', output,
                     'missing or incorrect HOPOPT specification')

  def testIPv6HopOptProtocolExceptIngressPhysical(self):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                GOOD_HEADER_1 + GOOD_TERM_2, self.naming),
            EXP_INFO))
    self.assertIn('next-header-except hop-by-hop;', output,
                  'missing or incorrect HOPOPT specification')
    self.assertNotIn('payload-protocol-except hop-by-hop;', output,
                     'missing or incorrect HOPOPT specification')
    self.assertNotIn('next-header-except 0;', output,
                     'missing or incorrect HOPOPT specification')
    self.assertNotIn('payload-protocol-except 0;', output,
                     'missing or incorrect HOPOPT specification')

  def testIPv6FragmentProtocolIngressPhysical(self):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                GOOD_HEADER_1 + GOOD_TERM_3, self.naming),
            EXP_INFO))
    self.assertIn('next-header fragment;', output,
                  'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('payload-protocol fragment;', output,
                     'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('next-header 44;', output,
                     'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('payload-protocol 44;', output,
                     'missing or incorrect IPv6-Frag specification')

  def testIPv6FragmentProtocolExceptIngressPhysical(self):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                GOOD_HEADER_1 + GOOD_TERM_4, self.naming),
            EXP_INFO))
    self.assertIn('next-header-except fragment;', output,
                  'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('payload-protocol-except fragment;', output,
                     'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('next-header-except 44;', output,
                     'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('payload-protocol-except 44;', output,
                     'missing or incorrect IPv6-Frag specification')

  def testIPv6TcpProtocolIngressPhysical(self):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                GOOD_HEADER_1 + GOOD_TERM_5, self.naming),
            EXP_INFO))
    self.assertIn('payload-protocol tcp;', output,
                  'missing or incorrect TCP specification')
    self.assertNotIn('next-header tcp;', output,
                     'missing or incorrect TCP specification')
    self.assertNotIn('payload-protocol 6;', output,
                     'missing or incorrect TCP specification')
    self.assertNotIn('next-header 6;', output,
                     'missing or incorrect TCP specification')

  def testIPv6TcpProtocolExceptIngressPhysical(self):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                GOOD_HEADER_1 + GOOD_TERM_6, self.naming),
            EXP_INFO))
    self.assertIn('payload-protocol-except tcp;', output,
                  'missing or incorrect TCP specification')
    self.assertNotIn('next-header-except tcp;', output,
                     'missing or incorrect TCP specification')
    self.assertNotIn('payload-protocol-except 6;', output,
                     'missing or incorrect TCP specification')
    self.assertNotIn('next-header-except 6;', output,
                     'missing or incorrect TCP specification')

  @parameterized.named_parameters(
      ('IngressLoopback', GOOD_HEADER_2),
      ('EgressPhysical', GOOD_HEADER_3),
      ('EgressLoopback', GOOD_HEADER_4),
  )
  def testIPv6HopOptProtocol(self, header):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                header + GOOD_TERM_1, self.naming),
            EXP_INFO))
    self.assertIn('payload-protocol 0;', output,
                  'missing or incorrect HOPOPT specification')
    self.assertNotIn('next-header 0;', output,
                     'missing or incorrect HOPOPT specification')
    self.assertNotIn('payload-protocol hop-by-hop;', output,
                     'missing or incorrect HOPOPT specification')
    self.assertNotIn('next-header hop-by-hop;', output,
                     'missing or incorrect HOPOPT specification')

  @parameterized.named_parameters(
      ('IngressLoopback', GOOD_HEADER_2),
      ('EgressPhysical', GOOD_HEADER_3),
      ('EgressLoopback', GOOD_HEADER_4),
  )
  def testIPv6HopOptProtocolExcept(self, header):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                header + GOOD_TERM_2, self.naming),
            EXP_INFO))
    self.assertIn('payload-protocol-except 0;', output,
                  'missing or incorrect HOPOPT specifications')
    self.assertNotIn('next-header-except 0;', output,
                     'missing or incorrect HOPOPT specifications')
    self.assertNotIn('payload-protocol-except hop-by-hop;', output,
                     'missing or incorrect HOPOPT specifications')
    self.assertNotIn('next-header-except hop-by-hop;', output,
                     'missing or incorrect HOPOPT specifications')

  @parameterized.named_parameters(
      ('IngressLoopback', GOOD_HEADER_2),
      ('EgressPhysical', GOOD_HEADER_3),
      ('EgressLoopback', GOOD_HEADER_4),
  )
  def testIPv6FragmentProtocol(self, header):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                header + GOOD_TERM_3, self.naming),
            EXP_INFO))
    self.assertIn('payload-protocol 44;', output,
                  'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('next-header 44;', output,
                     'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('payload-protocol fragment;', output,
                     'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('next-header fragment;', output,
                     'missing or incorrect IPv6-Frag specification')

  @parameterized.named_parameters(
      ('IngressLoopback', GOOD_HEADER_2),
      ('EgressPhysical', GOOD_HEADER_3),
      ('EgressLoopback', GOOD_HEADER_4),
  )
  def testIPv6FragmentProtocolExcept(self, header):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                header + GOOD_TERM_4, self.naming),
            EXP_INFO))
    self.assertIn('payload-protocol-except 44;', output,
                  'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('next-header-except 44;', output,
                     'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('payload-protocol-except fragment;', output,
                     'missing or incorrect IPv6-Frag specification')
    self.assertNotIn('next-header-except fragment;', output,
                     'missing or incorrect IPv6-Frag specification')

  @parameterized.named_parameters(
      ('IngressLoopback', GOOD_HEADER_2),
      ('EgressPhysical', GOOD_HEADER_3),
      ('EgressLoopback', GOOD_HEADER_4),
  )
  def testIPv6TcpProtocol(self, header):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                header + GOOD_TERM_5, self.naming),
            EXP_INFO))
    self.assertIn('payload-protocol tcp;', output,
                  'missing or incorrect TCP specification')
    self.assertNotIn('next-header tcp;', output,
                     'missing or incorrect TCP specification')
    self.assertNotIn('payload-protocol 6;', output,
                     'missing or incorrect TCP specification')
    self.assertNotIn('next-header 6;', output,
                     'missing or incorrect TCP specification')

  @parameterized.named_parameters(
      ('IngressLoopback', GOOD_HEADER_2),
      ('EgressPhysical', GOOD_HEADER_3),
      ('EgressLoopback', GOOD_HEADER_4),
  )
  def testIPv6TcpProtocolExcept(self, header):
    output = str(
        juniperevo.JuniperEvo(
            policy.ParsePolicy(
                header + GOOD_TERM_6, self.naming),
            EXP_INFO))
    self.assertIn('payload-protocol-except tcp;', output,
                  'missing or incorrect TCP specification')
    self.assertNotIn('next-header-except tcp;', output,
                     'missing or incorrect TCP specification')
    self.assertNotIn('payload-protocol-except 6;', output,
                     'missing or incorrect TCP specification')
    self.assertNotIn('next-header-except 6;', output,
                     'missing or incorrect TCP specification')

  def testIPv6FilterWithNoDirection(self):
    evojcl = juniperevo.JuniperEvo(
        policy.ParsePolicy(BAD_HEADER_1 + GOOD_TERM_1, self.naming), EXP_INFO)
    self.assertRaises(juniperevo.FilterDirectionError, str, evojcl)

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


# tests/lib/junipermsmpc_test.py

# Copyright 2020 Google LLC
#
# 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
#
#     https://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.
#
"""Unittest for junipermsmpc acl rendering module."""

import datetime
import re
from absl.testing import absltest
from unittest import mock

from absl.testing import parameterized
from capirca.lib import junipermsmpc
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: msmpc test-filter inet
}
"""
GOOD_HEADER_V6 = """
header {
  target:: msmpc test-filter inet6
}
"""
GOOD_HEADER_MIXED = """
header {
  target:: msmpc test-filter mixed
}
"""
GOOD_HEADER_MIXED_IMPLICIT = """
header {
  target:: msmpc test-filter
}
"""
GOOD_NOVERBOSE_V4_HEADER = """
header {
  target:: msmpc test-filter noverbose inet
}
"""
GOOD_NOVERBOSE_V6_HEADER = """
header {
  target:: msmpc test-filter inet6 noverbose
}
"""
GOOD_HEADER_INGRESS = """
header {
  comment:: "this is a test acl"
  target:: msmpc test-filter ingress inet
}
"""
GOOD_HEADER_EGRESS = """
header {
  comment:: "this is a test acl"
  target:: msmpc test-filter egress inet
}
"""
BAD_HEADER_DIRECTION = """
header {
  comment:: "this is a test acl"
  target:: msmpc test-filter ingress egress inet
}
"""
BAD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: cisco test-filter
}
"""
BAD_HEADER_2 = """
header {
  target:: msmpc test-filter inetpoop
}
"""
BAD_HEADER_3 = """
header {
  target:: msmpc test-filter inet inet6
}
"""
EXPIRED_TERM = """
term is_expired {
  expiration:: 2001-01-01
  action:: accept
}
"""
EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}

term good-term-2 {
  protocol:: tcp
  destination-port:: SMTP
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_1_V6 = """
term good-term-1 {
  protocol:: icmpv6
  action:: accept
}

term good-term-2 {
  protocol:: tcp
  destination-port:: SMTP
  destination-address:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_3 = """
term good-term-3 {
  protocol:: icmp
  icmp-type:: echo-reply information-reply information-request
  icmp-type:: router-solicitation timestamp-request
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  protocol:: icmp
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_8 = """
term good-term-8 {
  source-prefix:: foo_prefix_list
  destination-prefix:: bar_prefix_list baz_prefix_list
  action:: accept
}
"""
GOOD_TERM_11 = """
term good-term-11 {
  verbatim:: msmpc "mary had a little lamb"
  verbatim:: iptables "mary had a second lamb"
  verbatim:: cisco "mary had a third lamb"
  verbatim:: juniper "mary had a fourth lamb"
}
"""
GOOD_TERM_17 = """
term owner-term {
  owner:: foo@google.com
  action:: accept
}
"""
GOOD_TERM_18_SRC = """
term address-exclusions {
  source-address:: INTERNAL
  source-exclude:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_18_DST = """
term address-exclusions {
  destination-address:: INTERNAL
  destination-exclude:: SOME_HOST
  action:: accept
}
"""
GOOD_TERM_19 = """
term minimize-prefix-list {
  source-address:: INCLUDES
  source-exclude:: EXCLUDES
  action:: accept
}
"""
GOOD_TERM_25 = """
term good_term_25 {
  protocol:: tcp
  source-port:: DNS
  action:: accept
}
"""
GOOD_TERM_26 = """
term good_term_26 {
  protocol:: tcp
  source-port:: DNS
  action:: deny
}
"""
GOOD_TERM_26_V6 = """
term good_term_26-v6 {
  protocol:: tcp
  source-port:: DNS
  action:: deny
}
"""
GOOD_TERM_26_V6_REJECT = """
term good_term_26-v6 {
  protocol:: tcp
  source-port:: DNS
  action:: reject
}
"""
GOOD_TERM_30 = """
term good-term-30 {
  source-prefix-except:: foo_prefix_list
  destination-prefix-except:: bar_prefix_list
  action:: accept
}
"""
GOOD_TERM_31 = """
term good-term-31 {
  source-prefix:: foo_prefix
  source-prefix-except:: foo_except
  destination-prefix:: bar_prefix
  destination-prefix-except:: bar_except
  action:: accept
}
"""
GOOD_TERM_35 = """
term good_term_35 {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 3 4
  action:: accept
}
"""
GOOD_TERM_36 = """
term good-term-36 {
  protocol:: tcp
  destination-address:: SOME_HOST
  destination-address:: SOME_HOST
  option:: inactive
  action:: accept
}
"""
GOOD_TERM_NUMERIC_PROTOCOL = """
term good-term-numeric {
  protocol:: %s
  action:: accept
}
"""
GOOD_TERM_COMMENT = """
term good-term-comment {
  comment:: "This is a COMMENT"
  action:: accept
}
"""
ESTABLISHED_TERM_1 = """
term established-term-1 {
  protocol:: tcp
  source-port:: DNS
  option:: established
  action:: accept
}
"""
BAD_ICMPTYPE_TERM_1 = """
term icmptype-mismatch {
  comment:: "error when icmpv6 paired with inet filter"
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""
BAD_ICMPTYPE_TERM_2 = """
term icmptype-mismatch {
  comment:: "error when icmp paired with inet6 filter"
  protocol:: icmp
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""
DEFAULT_TERM_1 = """
term default-term-1 {
  action:: deny
}
"""
RANGE_PORTS_TERM = """
term ranged-ports-1 {
  protocol:: udp
  destination-port:: BOOTPS
  destination-port:: BOOTPC
  action:: accept
}
"""
MIXED_TESTING_TERM = """
term good-term {
  protocol:: tcp
  source-address:: SOME_HOST
  destination-port:: SMTP
  destination-address:: SOME_OTHER_HOST
  action:: accept
}
"""

MIXED_TESTING_TERM_ICMP = """
term good-term-icmp {
  protocol:: icmp
  source-address:: SOME_HOST
  destination-address:: SOME_OTHER_HOST
  action:: accept
}

term good-term-icmp-2 {
  protocol:: icmp
  action:: accept
}

term good-term-icmpv6 {
  protocol:: icmpv6
  source-address:: SOME_HOST
  destination-address:: SOME_OTHER_HOST
  action:: accept
}

term good-term-icmpv6-2 {
  protocol:: icmpv6
  action:: accept
}

term good-term-both-icmp-and-icmpv6 {
  protocol:: icmp
  protocol:: icmpv6
  source-address:: SOME_HOST
  destination-address:: SOME_OTHER_HOST
  action:: accept
}

term good-term-both-icmp-and-icmpv6-2 {
  protocol:: icmp
  protocol:: icmpv6
  source-address:: SOME_HOST
  destination-address:: SOME_OTHER_HOST
  action:: accept
}

"""
LOGGING_TERM = """
term good-term-1 {
  protocol:: icmp
  action:: accept
  logging:: %s
}
"""
TERM_NAME_COLLISION = """
term good-term-1%s {
  protocol:: icmp
  action:: accept
}

term hood-term-1%s {
  protocol:: tcp
  action:: accept
}
"""

SUPPORTED_TOKENS = frozenset([
    'action', 'comment', 'destination_address', 'destination_address_exclude',
    'destination_port', 'destination_prefix', 'destination_prefix_except',
    'expiration', 'icmp_code', 'icmp_type', 'stateless_reply', 'logging',
    'name', 'option', 'owner', 'platform', 'platform_exclude', 'protocol',
    'source_address', 'source_address_exclude', 'source_port', 'source_prefix',
    'source_prefix_except', 'translated', 'verbatim'
])

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {
        'established',
        'inactive',
        '.*',  # not actually a lex token!
        'tcp-established',
    }
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class JuniperMSMPCTest(parameterized.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testTermAndFilterName(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn('term good-term-1 {', output, output)
    self.assertIn('rule test-filter {', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testBadFilterType(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(BAD_HEADER_2 + GOOD_TERM_1, self.naming)
    self.assertRaises(junipermsmpc.UnsupportedHeaderError,
                      junipermsmpc.JuniperMSMPC, pol, EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testMultipleFilterType(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(BAD_HEADER_3 + GOOD_TERM_1, self.naming)
    self.assertRaises(junipermsmpc.ConflictingTargetOptionsError,
                      junipermsmpc.JuniperMSMPC, pol, EXP_INFO)

  def testMixedv4(self):
    self.naming.GetNetAddr.return_value = ([nacaddr.IPv4('192.168.0.0/24')])
    self.naming.GetServiceByProto.return_value = ['25']
    expected = ('                    term good-term-2 {\n' +
                '                        from {\n' +
                '                            destination-address {\n' +
                '                                192.168.0.0/24;\n' +
                '                            }')

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_1, self.naming),
        EXP_INFO)
    output = str(msmpc)
    self.assertIn(expected, output, output)

  def testMixedv6(self):
    self.naming.GetNetAddr.return_value = ([nacaddr.IPv6('2001::/33')])
    self.naming.GetServiceByProto.return_value = ['25']
    expected = ('                    term good-term-2 {\n' +
                '                        from {\n' +
                '                            destination-address {\n' +
                '                                2001::/33;\n' +
                '                            }')

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_1, self.naming),
        EXP_INFO)
    output = str(msmpc)
    self.assertIn(expected, output, output)

  def testMixedBoth(self):
    self.naming.GetNetAddr.return_value = ([
        nacaddr.IPv4('192.168.0.0/24'),
        nacaddr.IPv6('2001::/33')
    ])
    self.naming.GetServiceByProto.return_value = ['25']
    expectedv4 = ('                    term good-term-2-inet {\n' +
                  '                        from {\n' +
                  '                            destination-address {\n' +
                  '                                192.168.0.0/24;\n' +
                  '                            }')
    expectedv6 = ('                    term good-term-2-inet6 {\n' +
                  '                        from {\n' +
                  '                            destination-address {\n' +
                  '                                2001::/33;\n' +
                  '                            }')

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_1, self.naming),
        EXP_INFO)
    output = str(msmpc)
    self.assertIn(expectedv4, output, output)
    self.assertIn(expectedv6, output, output)

  def testCommentShrinking(self):
    long_comment = ' this is a very descriptive comment ' * 10
    expected = (' ' * 32 + '/* this is a very descriptive comment  this\n' +
                ' ' * 33 + '** is a very descriptive comment  this is a\n' +
                ' ' * 33 + '** very descriptive comment  this is a very\n' +
                ' ' * 33 + '** descript */')
    self.naming.GetNetAddr.return_value = ([
        nacaddr.IPv4('10.0.0.0/8', comment=long_comment)
    ])
    self.naming.GetServiceByProto.return_value = ['25']

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn(expected, output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testDefaultDeny(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + DEFAULT_TERM_1, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertNotIn('from {', output, output)
    self.assertIn('discard;', output, output)

  def testIcmpType(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_3, self.naming), EXP_INFO)
    output = str(msmpc)
    # verify proper translation from policy icmp-type text to juniper-esque
    self.assertIn('icmp-type 0;', output, output)
    self.assertIn('icmp-type 15;', output, output)
    self.assertIn('icmp-type 10;', output, output)
    self.assertIn('icmp-type 13;', output, output)
    self.assertIn('icmp-type 16;', output, output)

  def testIcmpCode(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_35, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn('icmp-code [ 3 4 ];', output, output)

  def testInactiveTerm(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_36, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn('inactive: term good-term-36 {', output)

  def testInet6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001::/33')]
    self.naming.GetServiceByProto.return_value = ['25']

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER_V6 + GOOD_TERM_1_V6, self.naming),
        EXP_INFO)
    output = str(msmpc)
    self.assertTrue('protocol icmp6;' in output and 'protocol tcp;' in output,
                    output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testProtocolCase(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_5, self.naming), EXP_INFO)
    output = str(msmpc)
    expected_output = (
        '            application test-filtergood-term-5-app1 {\n' +
        '                protocol icmp;\n' + '            }\n' +
        '            application test-filtergood-term-5-app2 {\n' +
        '                protocol tcp;\n' +
        '                destination-port 1-65535;\n' + '            }')

    self.assertIn(expected_output, output, output)

  def testPrefixList(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_8, self.naming), EXP_INFO)
    spfx_re = re.compile(r'source-prefix-list foo_prefix_list;')
    dpfx_re = re.compile(
        r'destination-prefix-list bar_prefix_list;\W+destination-prefix-list baz_prefix_list;'
    )
    output = str(msmpc)
    self.assertTrue(spfx_re.search(output), output)
    self.assertTrue(dpfx_re.search(output), output)

  def testPrefixListExcept(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_30, self.naming), EXP_INFO)
    spfx_re = re.compile(r'source-prefix-list foo_prefix_list except;')
    dpfx_re = re.compile(r'destination-prefix-list bar_prefix_list except;')
    output = str(msmpc)
    self.assertTrue(spfx_re.search(output), output)
    self.assertTrue(dpfx_re.search(output), output)

  def testPrefixListMixed(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_31, self.naming), EXP_INFO)
    spfx_re = re.compile(r'source-prefix-list foo_prefix;\W+'
                         r'source-prefix-list foo_except except;')
    dpfx_re = re.compile(r'destination-prefix-list bar_prefix;\W+'
                         r'destination-prefix-list bar_except except;')
    output = str(msmpc)
    self.assertTrue(spfx_re.search(output), output)
    self.assertTrue(dpfx_re.search(output), output)

  def testVerbatimTerm(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_11, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn('mary had a little lamb', output, output)
    # check if other platforms verbatim shows up in output
    self.assertNotIn('mary had a second lamb', output, output)
    self.assertNotIn('mary had a third lamb', output, output)
    self.assertNotIn('mary had a fourth lamb', output, output)

  def testAccept(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER + GOOD_TERM_25
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn('then {', output, output)
    self.assertIn('accept;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testDiscardIPv4(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER + GOOD_TERM_26
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn('then {', output, output)
    self.assertIn('discard;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testDiscardIPv6(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER_V6 + GOOD_TERM_26_V6
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn('then {', output, output)
    self.assertIn('discard;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testRejectIPv6(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER_V6 + GOOD_TERM_26_V6_REJECT
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn('then {', output, output)
    self.assertIn('reject;', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testTcpEstablished(self):
    self.naming.GetServiceByProto.return_value = ['53']

    policy_text = GOOD_HEADER + ESTABLISHED_TERM_1
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(policy_text, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertNotIn('term established-term-1', output, output)
    self.assertNotIn('tcp-established', output, output)

    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testStatelessReply(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.1/32')]
    self.naming.GetServiceByProto.return_value = ['25']

    ret = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1, self.naming)

    _, terms = ret.filters[0]
    for term in terms:
      if term.protocol[0] == 'icmp':
        term.stateless_reply = True

    msmpc = junipermsmpc.JuniperMSMPC(ret, EXP_INFO)

    output = str(msmpc)
    self.assertNotIn('term good-term-1 {', output, output)
    self.assertIn('term good-term-2 {', output, output)

  def testNoVerboseV4(self):
    addr_list = list()
    for octet in range(0, 256):
      net = nacaddr.IP('192.168.' + str(octet) + '.64/27')
      addr_list.append(net)
    self.naming.GetNetAddr.return_value = addr_list
    self.naming.GetServiceByProto.return_value = ['25']

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(
            GOOD_NOVERBOSE_V4_HEADER + GOOD_TERM_1 + GOOD_TERM_COMMENT,
            self.naming), EXP_INFO)
    self.assertIn('192.168.0.64/27;', str(msmpc))
    self.assertNotIn('COMMENT', str(msmpc))
    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testNoVerboseV6(self):
    addr_list = list()
    for octet in range(0, 256):
      net = nacaddr.IPv6(
          '2001:db8:1010:' + str(octet) + '::64/64', strict=False)
      addr_list.append(net)
    self.naming.GetNetAddr.return_value = addr_list
    self.naming.GetServiceByProto.return_value = ['25']

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(
            GOOD_NOVERBOSE_V6_HEADER + GOOD_TERM_1 + GOOD_TERM_COMMENT,
            self.naming), EXP_INFO)
    self.assertIn('2001:db8:1010:90::/61;', str(msmpc))
    self.assertNotIn('COMMENT', str(msmpc))
    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testTermTypeIndexKeys(self):
    # ensure an _INET entry for each _TERM_TYPE entry
    self.assertCountEqual(junipermsmpc.Term._TERM_TYPE.keys(),
                          junipermsmpc.Term.AF_MAP.keys())

  @mock.patch.object(junipermsmpc.logging, 'debug')
  def testIcmpv6InetMismatch(self, mock_debug):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + BAD_ICMPTYPE_TERM_1, self.naming),
        EXP_INFO)
    # output happens in __str_
    str(msmpc)

    mock_debug.assert_called_once_with(
        'Term icmptype-mismatch will not be rendered,'
        ' as it has icmpv6 match specified but '
        'the ACL is of inet address family.')

  @mock.patch.object(junipermsmpc.logging, 'debug')
  def testIcmpInet6Mismatch(self, mock_debug):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER_V6 + BAD_ICMPTYPE_TERM_2, self.naming),
        EXP_INFO)
    # output happens in __str_
    str(msmpc)

    mock_debug.assert_called_once_with(
        'Term icmptype-mismatch will not be rendered,'
        ' as it has icmp match specified but '
        'the ACL is of inet6 address family.')

  @mock.patch.object(junipermsmpc.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    _ = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + EXPIRED_TERM, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and will '
        'not be rendered.', 'is_expired', 'test-filter')

  @mock.patch.object(junipermsmpc.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    _ = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(
            GOOD_HEADER + EXPIRING_TERM % exp_date.strftime('%Y-%m-%d'),
            self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring', 'test-filter')

  def testOwnerTerm(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_17, self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn(
        '                    /*\n'
        '                     ** Owner: foo@google.com\n'
        '                     */', output, output)

  def testOwnerNoVerboseTerm(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_NOVERBOSE_V4_HEADER + GOOD_TERM_17,
                           self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertNotIn('** Owner: ', output, output)

  def testAddressExclude(self):
    big = nacaddr.IPv4('0.0.0.0/1', comment='half of everything')
    ip1 = nacaddr.IPv4('10.0.0.0/8', comment='RFC1918 10-net')
    ip2 = nacaddr.IPv4('172.16.0.0/12', comment='RFC1918 172-net')
    terms = (GOOD_TERM_18_SRC, GOOD_TERM_18_DST)
    self.naming.GetNetAddr.side_effect = [[big, ip1, ip2], [ip1]] * len(terms)

    mock_calls = []
    for term in terms:
      msmpc = junipermsmpc.JuniperMSMPC(
          policy.ParsePolicy(GOOD_HEADER + term, self.naming), EXP_INFO)
      output = str(msmpc)
      expected_output = (
          '                            ' +
          ('source' if term == GOOD_TERM_18_SRC else 'destination') +
          '-address {\n' +
          '                                /* half of everything, RFC1918 '
          '10-net */\n' + '                                0.0.0.0/1;\n' +
          '                                /* RFC1918 172-net */\n' +
          '                                172.16.0.0/12;\n' +
          '                                /* RFC1918 10-net */\n' +
          '                                10.0.0.0/8 except;\n' +
          '                            }')
      self.assertIn(expected_output, output, output)
      self.assertNotIn('10.0.0.0/8;', output, output)
      self.assertNotIn('172.16.0.0/12 except;', output, output)
      mock_calls.append(mock.call('INTERNAL'))
      mock_calls.append(mock.call('SOME_HOST'))

    self.naming.GetNetAddr.assert_has_calls(mock_calls)

  def testMinimizePrefixes(self):
    includes = ['1.0.0.0/8', '2.0.0.0/8']
    excludes = ['1.1.1.1/32', '2.0.0.0/8', '3.3.3.3/32']

    expected = ['1.0.0.0/8;', '1.1.1.1/32 except;']
    unexpected = ['2.0.0.0/8;', '2.0.0.0/8 except;', '3.3.3.3/32']

    self.naming.GetNetAddr.side_effect = [[nacaddr.IPv4(ip) for ip in includes],
                                          [nacaddr.IPv4(ip) for ip in excludes]]

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_19, self.naming), EXP_INFO)
    output = str(msmpc)
    for result in expected:
      self.assertIn(result, output, 'expected "%s" in %s' % (result, output))
    for result in unexpected:
      self.assertNotIn(result, output,
                       'unexpected "%s" in %s' % (result, output))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('INCLUDES'), mock.call('EXCLUDES')])

  def testNoMatchReversal(self):
    includes = ['10.0.0.0/8', '10.0.0.0/10']
    excludes = ['10.0.0.0/9']

    expected = ['10.0.0.0/8;', '10.0.0.0/10;', '10.0.0.0/9 except;']

    self.naming.GetNetAddr.side_effect = [[nacaddr.IPv4(ip) for ip in includes],
                                          [nacaddr.IPv4(ip) for ip in excludes]]

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_19, self.naming), EXP_INFO)
    output = str(msmpc)
    for result in expected:
      self.assertIn(result, output)

  def testBuildTokens(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_35, self.naming), EXP_INFO)
    st, sst = msmpc._BuildTokens()
    self.assertSetEqual(st, SUPPORTED_TOKENS)
    self.assertDictEqual(sst, SUPPORTED_SUB_TOKENS)

  def testRangedPorts(self):
    self.naming.GetServiceByProto.side_effect = [['67'], ['68']]
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + RANGE_PORTS_TERM, self.naming),
        EXP_INFO)
    self.assertIn('destination-port 67-68;', str(msmpc))

  def testNotRangedPorts(self):
    self.naming.GetServiceByProto.side_effect = [['67'], ['69']]
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + RANGE_PORTS_TERM, self.naming),
        EXP_INFO)
    self.assertNotIn('destination-port 67-68;', str(msmpc))
    self.assertIn('destination-port 67;', str(msmpc))
    self.assertIn('destination-port 69;', str(msmpc))

  def testApplicationSets(self):
    self.naming.GetServiceByProto.side_effect = [['67'], ['69']]
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + RANGE_PORTS_TERM, self.naming),
        EXP_INFO)
    expected = ('        applications {\n'
                '            application test-filterranged-ports-1-app1 {\n'
                '                protocol udp;\n'
                '                destination-port 67;\n'
                '            }\n'
                '            application test-filterranged-ports-1-app2 {\n'
                '                protocol udp;\n'
                '                destination-port 69;\n'
                '            }\n'
                '            application-set test-filterranged-ports-1-app {\n'
                '                application test-filterranged-ports-1-app1;\n'
                '                application test-filterranged-ports-1-app2;\n'
                '            }\n'
                '        }\n')
    self.assertIn(expected, str(msmpc))

  def testGroup(self):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + DEFAULT_TERM_1, self.naming), EXP_INFO)
    self.assertEqual('b;', msmpc._Group(['B']))
    self.assertEqual('B;', msmpc._Group(['B'], lc=False))
    self.assertEqual('b;', msmpc._Group(['B'], lc=True))
    self.assertEqual('100;', msmpc._Group([100]))
    self.assertEqual('100-200;', msmpc._Group([(100, 200)]))
    self.assertEqual('[ b a ];', msmpc._Group(['b', 'A']))
    self.assertEqual('[ 99 101-199 ];', msmpc._Group([99, (101, 199)]))
    self.assertEqual('[ 99 101-199 ];', msmpc._Group([99, (101, 199)]))

  @parameterized.named_parameters(
      dict(
          testcase_name='MIXED_TO_V4',
          addresses=[[nacaddr.IPv4('0.0.0.0/1'),
                      nacaddr.IPv6('2001::/33')],
                     [nacaddr.IPv4('192.168.0.0/24')]],
          expected=[
              '                    term good-term-inet {\n' +
              '                        from {\n' +
              '                            source-address {\n' +
              '                                0.0.0.0/1;\n' +
              '                            }\n' +
              '                            destination-address {\n' +
              '                                192.168.0.0/24;\n' +
              '                            }'
          ],
          notexpected=['2001::/33']),
      dict(
          testcase_name='V4_TO_MIXED',
          addresses=[
              [nacaddr.IPv4('192.168.0.0/24')],
              [nacaddr.IPv4('0.0.0.0/1'),
               nacaddr.IPv6('2001::/33')],
          ],
          expected=[
              '                    term good-term-inet {\n' +
              '                        from {\n' +
              '                            source-address {\n' +
              '                                192.168.0.0/24;\n' +
              '                            }\n' +
              '                            destination-address {\n' +
              '                                0.0.0.0/1;\n' +
              '                            }'
          ],
          notexpected=['2001::/33']),
      dict(
          testcase_name='MIXED_TO_V6',
          addresses=[[nacaddr.IPv4('0.0.0.0/1'),
                      nacaddr.IPv6('2001::/33')], [nacaddr.IPv6('2201::/48')]],
          expected=[
              '                    term good-term-inet6 {\n' +
              '                        from {\n' +
              '                            source-address {\n' +
              '                                2001::/33;\n' +
              '                            }\n' +
              '                            destination-address {\n' +
              '                                2201::/48;\n' +
              '                            }'
          ],
          notexpected=['0.0.0.0/1']),
      dict(
          testcase_name='V6_TO_MIXED',
          addresses=[[nacaddr.IPv6('2201::/48')],
                     [nacaddr.IPv4('0.0.0.0/1'),
                      nacaddr.IPv6('2001::/33')]],
          expected=[
              '                    term good-term-inet6 {\n' +
              '                        from {\n' +
              '                            source-address {\n' +
              '                                2201::/48;\n' +
              '                            }\n' +
              '                            destination-address {\n' +
              '                                2001::/33;\n' +
              '                            }'
          ],
          notexpected=['0.0.0.0/1']),
      dict(
          testcase_name='MIXED_TO_MIXED',
          addresses=[[nacaddr.IPv4('0.0.0.0/1'),
                      nacaddr.IPv6('2001::/33')],
                     [
                         nacaddr.IPv4('192.168.0.0/24'),
                         nacaddr.IPv6('2201::/48')
                     ]],
          expected=[
              '                    term good-term-inet {\n' +
              '                        from {\n' +
              '                            source-address {\n' +
              '                                0.0.0.0/1;\n' +
              '                            }\n' +
              '                            destination-address {\n' +
              '                                192.168.0.0/24;\n' +
              '                            }',
              '                    term good-term-inet6 {\n' +
              '                        from {\n' +
              '                            source-address {\n' +
              '                                2001::/33;\n' +
              '                            }\n' +
              '                            destination-address {\n' +
              '                                2201::/48;\n' +
              '                            }'
          ],
          notexpected=[]),
      dict(
          testcase_name='V4_TO_V4',
          addresses=[[nacaddr.IPv4('0.0.0.0/1')],
                     [nacaddr.IPv4('192.168.0.0/24')]],
          expected=[
              '                    term good-term {\n' +
              '                        from {\n' +
              '                            source-address {\n' +
              '                                0.0.0.0/1;\n' +
              '                            }\n' +
              '                            destination-address {\n' +
              '                                192.168.0.0/24;\n' +
              '                            }'
          ],
          notexpected=[]),
      dict(
          testcase_name='V6_TO_V6',
          addresses=[[nacaddr.IPv6('2001::/33')], [nacaddr.IPv6('2201::/48')]],
          expected=[
              '                    term good-term {\n' +
              '                        from {\n' +
              '                            source-address {\n' +
              '                                2001::/33;\n' +
              '                            }\n' +
              '                            destination-address {\n' +
              '                                2201::/48;\n' +
              '                            }'
          ],
          notexpected=[]),
      dict(
          testcase_name='V4_TO_V6',
          addresses=[[nacaddr.IPv4('0.0.0.0/1')], [nacaddr.IPv6('2201::/48')]],
          expected=[],
          notexpected=['0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48'],
      ),
      dict(
          testcase_name='V6_TO_V4',
          addresses=[[nacaddr.IPv6('2001::/33')],
                     [nacaddr.IPv4('192.168.0.0/24')]],
          expected=[],
          notexpected=['0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48'],
      ),
      dict(
          testcase_name='PARTLY_UNSPECIFIED',
          addresses=[[nacaddr.IPv6('2001::/33')],
                     [nacaddr.IPv4('192.168.0.0/24')]],
          expected=['term good_term_25 '],
          notexpected=[
              '0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48',
              'term good-term-both-icmp-and-icmpv6-'
          ],
      ),
  )
  def testMixed(self, addresses, expected, notexpected):
    self.naming.GetNetAddr.side_effect = addresses
    self.naming.GetServiceByProto.return_value = ['25']
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + MIXED_TESTING_TERM + GOOD_TERM_25, self.naming),
        EXP_INFO)
    output = str(msmpc)
    for expect in expected:
      self.assertIn(expect, output, output)
    for notexpect in notexpected:
      self.assertNotIn(notexpect, output, output)

  @parameterized.named_parameters(
      dict(
          testcase_name='MIXED_TO_V4',
          addresses=[[nacaddr.IPv4('0.0.0.0/1'),
                      nacaddr.IPv6('2001::/33')],
                     [nacaddr.IPv4('192.168.0.0/24')]],
          expected=[
              '                    term good-term-icmp-inet {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmp-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmpv6-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-inet {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-2-inet {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
          ],
          notexpected=['2001::/33']),
      dict(
          testcase_name='V4_TO_MIXED',
          addresses=[
              [nacaddr.IPv4('192.168.0.0/24')],
              [nacaddr.IPv4('0.0.0.0/1'),
               nacaddr.IPv6('2001::/33')],
          ],
          expected=[
              '                    term good-term-icmp-inet {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmp-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmpv6-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-inet {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-2-inet {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
          ],
          notexpected=['2001::/33']),
      dict(
          testcase_name='MIXED_TO_V6',
          addresses=[[nacaddr.IPv4('0.0.0.0/1'),
                      nacaddr.IPv6('2001::/33')], [nacaddr.IPv6('2201::/48')]],
          expected=[
              '                    term good-term-icmp-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmpv6-inet6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-icmpv6-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-inet6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-2-inet6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
          ],
          notexpected=['0.0.0.0/1']),
      dict(
          testcase_name='V6_TO_MIXED',
          addresses=[[nacaddr.IPv6('2201::/48')],
                     [nacaddr.IPv4('0.0.0.0/1'),
                      nacaddr.IPv6('2001::/33')]],
          expected=[
              '                    term good-term-icmp-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmpv6-inet6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-icmpv6-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-inet6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-2-inet6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
          ],
          notexpected=['0.0.0.0/1']),
      dict(
          testcase_name='MIXED_TO_MIXED',
          addresses=[[nacaddr.IPv4('0.0.0.0/1'),
                      nacaddr.IPv6('2001::/33')],
                     [
                         nacaddr.IPv4('192.168.0.0/24'),
                         nacaddr.IPv6('2201::/48')
                     ]],
          expected=[
              '                    term good-term-icmp-inet {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmp-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmpv6-inet6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-icmpv6-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-inet {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-inet6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-2-inet {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-2-inet6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
          ],
          notexpected=[]),
      dict(
          testcase_name='V4_TO_V4',
          addresses=[[nacaddr.IPv4('0.0.0.0/1')],
                     [nacaddr.IPv4('192.168.0.0/24')]],
          expected=[
              '                    term good-term-icmp {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmp-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmpv6-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-2 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                0.0.0.0/1;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                192.168.0.0/24;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
          ],
          notexpected=[]),
      dict(
          testcase_name='V6_TO_V6',
          addresses=[[nacaddr.IPv6('2001::/33')], [nacaddr.IPv6('2201::/48')]],
          expected=[
              '                    term good-term-icmp-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmpv6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-icmpv6-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
              '                    term good-term-both-icmp-and-icmpv6-2 {\n'
              '                        from {\n'
              '                            source-address {\n'
              '                                2001::/33;\n'
              '                            }\n'
              '                            destination-address {\n'
              '                                2201::/48;\n'
              '                            }\n'
              '                            application-sets test-filterd-term-both-icmp-and-icmpv6-app;\n'
              '                        }',
          ],
          notexpected=[]),
      dict(
          testcase_name='V4_TO_V6',
          addresses=[[nacaddr.IPv4('0.0.0.0/1')], [nacaddr.IPv6('2201::/48')]],
          expected=[
              '                    term good-term-icmp-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmpv6-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
          ],
          notexpected=['0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48'],
      ),
      dict(
          testcase_name='V6_TO_V4',
          addresses=[[nacaddr.IPv6('2001::/33')],
                     [nacaddr.IPv4('192.168.0.0/24')]],
          expected=[
              '                    term good-term-icmp-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmpv6-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
          ],
          notexpected=['0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48'],
      ),
      dict(
          testcase_name='PARTLY_UNSPECIFIED',
          addresses=[[nacaddr.IPv6('2001::/33')],
                     [nacaddr.IPv4('192.168.0.0/24')]],
          expected=[
              '                    term good-term-icmp-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmp-app;\n'
              '                        }',
              '                    term good-term-icmpv6-2 {\n'
              '                        from {\n'
              '                            application-sets test-filtergood-term-icmpv6-app;\n'
              '                        }',
          ],
          notexpected=[
              '0.0.0.0/1', '192.168.0.0/24', '2001::/33', '2201::/48',
              'term good-term-icmp-i', 'term good-term-icmpv6-i',
              'term good-term-both-icmp-and-icmpv6-'
          ],
      ),
  )
  def testMixedICMP(self, addresses, expected, notexpected):
    self.naming.GetNetAddr.side_effect = addresses * 4
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER_MIXED + MIXED_TESTING_TERM_ICMP,
                           self.naming), EXP_INFO)
    output = str(msmpc)
    for expect in expected:
      self.assertIn(expect, output, output)
    for notexpect in notexpected:
      self.assertNotIn(notexpect, output, output)

  @parameterized.named_parameters(
      dict(testcase_name='true', option='true', want_logging=True),
      dict(testcase_name='True', option='True', want_logging=True),
      dict(testcase_name='syslog', option='syslog', want_logging=True),
      dict(testcase_name='local', option='local', want_logging=True),
      dict(testcase_name='disable', option='disable', want_logging=False),
      dict(testcase_name='log-both', option='log-both', want_logging=True),
  )
  def testLogging(self, option, want_logging):
    self.naming.GetNetAddr.return_value = [nacaddr.IPv4('192.168.0.0/24')]
    self.naming.GetServiceByProto.return_value = ['25']
    expected_output = (
        '    test-filter {\n' + '        services {\n' +
        '            stateful-firewall {\n' +
        '                rule test-filter {\n' +
        '                    match-direction input-output;\n' +
        '                    term good-term-1 {\n' +
        '                        from {\n' +
        '                            application-sets '
        'test-filtergood-term-1-app;\n' + '                        }\n' +
        '                        then {\n' +
        '                            accept;\n' +
        ('                            syslog;\n' if want_logging else '') +
        '                        }\n' + '                    }\n' +
        '                }\n' + '            }\n' + '        }\n' +
        '        applications {\n' +
        '            application test-filtergood-term-1-app1 {\n' +
        '                protocol icmp;\n' + '            }\n' +
        '            application-set test-filtergood-term-1-app {\n' +
        '                application test-filtergood-term-1-app1;\n' +
        '            }\n' + '        }\n' + '    }\n' + '}\n' +
        'apply-groups test-filter;')
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER_MIXED_IMPLICIT + (LOGGING_TERM % option),
                           self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn(expected_output, output, output)

  @parameterized.named_parameters(
      dict(
          testcase_name='default', header=GOOD_HEADER,
          direction='input-output'),
      dict(
          testcase_name='ingress',
          header=GOOD_HEADER_INGRESS,
          direction='input'),
      dict(
          testcase_name='egress', header=GOOD_HEADER_EGRESS,
          direction='output'))
  def testDirection(self, header, direction):
    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(header + GOOD_TERM_3, self.naming), EXP_INFO)
    output = str(msmpc)
    expected_output = ('                rule test-filter {\n' +
                       '                    match-direction %s;')
    self.assertIn(expected_output % direction, output, output)

  def testBadDirectionCombo(self):
    pol = policy.ParsePolicy(BAD_HEADER_DIRECTION + GOOD_TERM_3, self.naming)
    self.assertRaises(junipermsmpc.ConflictingTargetOptionsError,
                      junipermsmpc.JuniperMSMPC, pol, EXP_INFO)

  def testTermNameCollision(self):
    short_append = '1' * (
        junipermsmpc.MAX_IDENTIFIER_LEN // 2 - len('?ood-term-1'))
    long_append = short_append + '1'
    not_too_long_name = (TERM_NAME_COLLISION % (short_append, short_append))
    too_long_name = (TERM_NAME_COLLISION % (long_append, long_append))
    pol = policy.ParsePolicy(GOOD_HEADER + too_long_name, self.naming)
    self.assertRaises(junipermsmpc.ConflictingApplicationSetsError,
                      junipermsmpc.JuniperMSMPC, pol, EXP_INFO)
    _ = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER + not_too_long_name, self.naming),
        EXP_INFO)

  def testSlashZeroReplacement(self):
    self.naming.GetNetAddr.return_value = ([
        nacaddr.IPv4('0.0.0.0/0'),
        nacaddr.IPv6('::/0')
    ])
    self.naming.GetServiceByProto.return_value = ['25']
    expectedv4 = ('                    term good-term-2-inet {\n' +
                  '                        from {\n' +
                  '                            destination-address {\n' +
                  '                                any-ipv4;\n' +
                  '                            }')
    expectedv6 = ('                    term good-term-2-inet6 {\n' +
                  '                        from {\n' +
                  '                            destination-address {\n' +
                  '                                any-ipv6;\n' +
                  '                            }')

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_1, self.naming),
        EXP_INFO)
    output = str(msmpc)
    self.assertIn(expectedv4, output, output)
    self.assertIn(expectedv6, output, output)

  def testV6SlashFourteenReplacement(self):
    self.naming.GetNetAddr.return_value = ([
        nacaddr.IPv4('0.0.0.0/1'),
        nacaddr.IPv6('::/14')
    ])
    self.naming.GetServiceByProto.return_value = ['25']
    expectedv4 = ('                    term good-term-2-inet {\n' +
                  '                        from {\n' +
                  '                            destination-address {\n' +
                  '                                0.0.0.0/1;\n' +
                  '                            }')
    expectedv6 = ('                    term good-term-2-inet6 {\n' +
                  '                        from {\n' +
                  '                            destination-address {\n' +
                  '                                ::/16;\n' +
                  '                                1::/16;\n' +
                  '                                2::/16;\n' +
                  '                                3::/16;\n' +
                  '                            }')

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(GOOD_HEADER_MIXED + GOOD_TERM_1, self.naming),
        EXP_INFO)
    output = str(msmpc)
    self.assertIn(expectedv4, output, output)
    self.assertIn(expectedv6, output, output)

  @parameterized.named_parameters(
      dict(testcase_name='tcp', protoname='tcp', protonum='tcp'),
      dict(testcase_name='hopopt', protoname='hopopt', protonum='0'),
      dict(testcase_name='vrrp', protoname='vrrp', protonum='112'),
  )
  def testProtocolAsNumber(self, protoname, protonum):
    expected = ('            application test-filtergood-term-numeric-app1 {\n'
                + '                protocol %s;') % protonum

    msmpc = junipermsmpc.JuniperMSMPC(
        policy.ParsePolicy(
            GOOD_HEADER_MIXED + GOOD_TERM_NUMERIC_PROTOCOL % protoname,
            self.naming), EXP_INFO)
    output = str(msmpc)
    self.assertIn(expected, output, output)

  def testSupportedNamedProtocols(self):
    supported_as_names = junipermsmpc.Term._SUPPORTED_PROTOCOL_NAMES
    all_supported_protocols = junipermsmpc.Term.PROTO_MAP.keys()
    for want in supported_as_names:
      self.assertIn(want, all_supported_protocols)


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


# tests/lib/junipersrx_test.py

# Copyright 2012 Google Inc. All Rights Reserved.
#
# 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.

"""Unit test for Juniper SRX acl rendering module."""

import copy
import datetime
import re
from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import junipersrx
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy


GOOD_HEADER = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone trust to-zone untrust
}
"""
GOOD_HEADER_2 = """
header {
  comment:: "This is a header from untrust to trust"
  target:: srx from-zone untrust to-zone trust
}
"""
GOOD_HEADER_3 = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone trust to-zone untrust inet
}
"""
GOOD_HEADER_4 = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone trust to-zone untrust inet6
}
"""
GOOD_HEADER_5 = """
header {
  target:: srx from-zone trust to-zone untrust inet
  apply-groups:: tcp-test1 tcp-test2
}
"""
GOOD_HEADER_6 = """
header {
  target:: srx from-zone trust to-zone untrust inet
  apply-groups-except:: tcp-test1 tcp-test2
}
"""
GOOD_HEADER_7 = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone trust to-zone untrust address-book-zone inet
}
"""

GOOD_HEADER_8 = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone trust to-zone untrust address-book-zone inet6
}
"""

GOOD_HEADER_9 = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone trust to-zone untrust address-book-zone
}
"""

GOOD_HEADER_10 = """
header {
  comment:: "This is a test acl with a global policy"
  target:: srx from-zone all to-zone all address-book-global
}
"""

GOOD_HEADER_11 = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone trust to-zone dmz
}
"""

GOOD_HEADER_12 = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone untrust to-zone trust address-book-zone inet
}
"""

GOOD_HEADER_13 = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone trust to-zone untrust inet expresspath
}
"""

GOOD_HEADER_14 = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone trust to-zone untrust expresspath inet
}
"""

GOOD_HEADER_NOVERBOSE = """
header {
  comment:: "This is a test acl with a comment"
  target:: srx from-zone trust to-zone untrust noverbose
}
"""

BAD_HEADER = """
header {
  target:: srx something
}
"""

BAD_HEADER_1 = """
header {
  comment:: "This header has two address families"
  target:: srx from-zone trust to-zone untrust inet6 mixed
}
"""

BAD_HEADER_3 = """
header {
  comment:: "This is a test acl with a global policy"
  target:: srx from-zone all to-zone all address-book-zone
}
"""

BAD_HEADER_4 = """
header {
  comment:: "This is a test acl with a global policy"
  target:: srx from-zone test to-zone all
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  comment:: "This header is very very very very very very very very very very very very very very very very very very very very large"
  destination-address:: SOME_HOST
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  destination-address:: SOME_HOST
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_3 = """
term good-term-3 {
  destination-address:: SOME_HOST
  protocol:: tcp
  action:: accept
  vpn:: good-vpn-3
}
"""

GOOD_TERM_4 = """
term good-term-4 {
  destination-address:: SOME_HOST
  protocol:: tcp
  action:: accept
  vpn:: good-vpn-4 policy-4
}
"""

GOOD_TERM_LOG_1 = """
term good-term-5 {
  action:: accept
  logging:: log-both
}
"""
GOOD_TERM_LOG_2 = """
term good-term-5 {
  action:: deny
  logging:: log-both
}
"""

GOOD_TERM_LOG_3 = """
term good-term-5 {
  action:: accept
  logging:: true
}
"""
GOOD_TERM_LOG_4 = """
term good-term-5 {
  action:: deny
  logging:: true
}
"""

GOOD_TERM_COUNT_1 = """
term good-term-6 {
  counter:: good-counter
  action:: accept
}
"""
GOOD_TERM_COUNT_2 = """
term good-term-6 {
  counter:: good-counter
  action:: deny
}
"""

GOOD_TERM_10 = """
term good-term-10 {
  destination-address:: SOME_HOST
  action:: accept
  dscp-set:: b111000
}
"""

GOOD_TERM_11 = """
term good-term-11 {
  destination-address:: SOME_HOST
  action:: accept
  dscp-set:: af42
  dscp-match:: af41-af42 5
  dscp-except:: be
}
"""

GOOD_TERM_12 = """
term dup-of-term-1 {
  destination-address:: FOOBAR
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_13 = """
term dup-of-term-1 {
  destination-address:: FOOBAR SOME_HOST
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_14 = """
term term_to_split {
  source-address:: FOOBAR
  destination-address:: SOME_HOST
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_15 = """
term good-term-15 {
  destination-address:: SOME_HOST
  destination-port:: SMTP
  protocol:: tcp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_16 = """
term good-term-16 {
  destination-address:: BAZ
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_17 = """
term term_to_split {
  destination-address:: FOOBAR SOME_HOST
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_18 = """
term good_term_18 {
  source-exclude:: SMALL
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_19 = """
term good_term_19 {
  source-address:: LARGE
  source-exclude:: SMALL
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_20 = """
term good_term_20 {
  destination-address:: FOO
  destination-port:: HTTP
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_21 = """
term good_term_21 {
  destination-address:: UDON
  destination-port:: HTTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_21 = """
term good_term_21 {
  destination-address:: FOO
  destination-port:: QUIC
  protocol:: udp
  action:: accept
}
"""

GOOD_TERM_23 = """
term good_term_23 {
  action:: accept
}
"""

BAD_TERM_1 = """
term bad-term-1 {
  destination-address:: SOME_HOST
  protocol:: tcp
  action:: deny
  vpn:: good-vpn-4 policy-4
}
"""

TCP_ESTABLISHED_TERM = """
term tcp-established-term {
  source-address:: SOME_HOST
  source-port:: SMTP
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""

UDP_ESTABLISHED_TERM = """
term udp-established-term {
  source-address:: FOO
  source-port:: QUIC
  protocol:: udp
  option:: established
  action:: accept
}
"""

ICMP_RESPONSE_TERM = """
term icmp_response-term {
  protocol:: icmp
  icmp-type:: echo-reply
  action:: accept
}
"""

EXPIRED_TERM_1 = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

ICMP_TYPE_TERM_1 = """
term test-icmp {
  protocol:: icmp
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

# For testing when the number of terms is at the 8 term application limit
LONG_IPV6_ICMP_TERM = """
term accept-icmpv6-types {
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply neighbor-solicit
  icmp-type:: neighbor-advertisement router-advertisement packet-too-big
  icmp-type:: parameter-problem time-exceeded
  action:: accept
}
"""

# For testing when the number of terms goes over the 8 term application limit
LONG_IPV6_ICMP_TERM2 = """
term accept-icmpv6-types {
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply neighbor-solicit
  icmp-type:: neighbor-advertisement router-advertisement packet-too-big
  icmp-type:: parameter-problem time-exceeded destination-unreachable
  action:: accept
}
"""

ICMP_ALL_TERM = """
term accept-icmp-types {
  protocol:: icmp
  icmp-type:: echo-reply unreachable source-quench redirect alternate-address
  icmp-type:: echo-request router-advertisement router-solicitation
  icmp-type:: time-exceeded parameter-problem timestamp-request
  icmp-type:: timestamp-reply information-request information-reply
  icmp-type:: mask-request mask-reply conversion-error mobile-redirect
  action:: accept
}
"""

ICMP6_ALL_TERM = """
term accept-icmpv6-types {
  protocol:: icmpv6
  icmp-type:: destination-unreachable packet-too-big time-exceeded
  icmp-type:: parameter-problem echo-request echo-reply
  icmp-type:: multicast-listener-query multicast-listener-report
  icmp-type:: multicast-listener-done router-solicit router-advertisement
  icmp-type:: neighbor-solicit neighbor-advertisement redirect-message
  icmp-type:: router-renumbering icmp-node-information-query
  icmp-type:: icmp-node-information-response
  icmp-type:: inverse-neighbor-discovery-solicitation
  icmp-type:: inverse-neighbor-discovery-advertisement
  icmp-type:: version-2-multicast-listener-report
  icmp-type:: home-agent-address-discovery-request
  icmp-type:: home-agent-address-discovery-reply mobile-prefix-solicitation
  icmp-type:: mobile-prefix-advertisement certification-path-solicitation
  icmp-type:: certification-path-advertisement multicast-router-advertisement
  icmp-type:: multicast-router-solicitation multicast-router-termination
  action:: accept
}
"""

IPV6_ICMP_TERM = """
term test-ipv6_icmp {
  protocol:: icmpv6
  icmp-type:: destination-unreachable packet-too-big
  icmp-type:: time-exceeded time-exceeded
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

BAD_ICMP_TERM_1 = """
term test-icmp {
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

ICMP_ONLY_TERM_1 = """
term test-icmp {
  protocol:: icmp
  action:: accept
}
"""

OWNER_TERM = """
term owner-test {
  owner:: foo@google.com
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

DEFAULT_TERM_1 = """
term default-term-1 {
  action:: deny
}
"""

TIMEOUT_TERM = """
term timeout-term {
  protocol:: icmp
  icmp-type:: echo-request
  timeout:: 77
  action:: accept
}
"""

GLOBAL_ZONE_TERM = """
term global-zone-term {
  protocol:: icmp
  icmp-type:: echo-request
  source-zone:: szone2 szone1
  destination-zone:: dzone2 dzone1
  action:: accept
}
"""

PLATFORM_EXCLUDE_TERM = """
term platform-exclude-term {
  protocol:: tcp udp
  platform-exclude:: srx
  action:: accept
}
"""

PLATFORM_TERM = """
term platform-term {
  protocol:: tcp udp
  platform:: srx juniper
  action:: accept
}
"""

PLATFORM_EXCLUDE_ADDRESS_TERM = """
term platform-exclude-term {
  protocol:: tcp udp
  source-address:: FOO
  platform-exclude:: srx
  action:: accept
}
"""

SUPPORTED_TOKENS = frozenset({
    'action',
    'comment',
    'counter',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'destination_zone',
    'dscp_except',
    'dscp_match',
    'dscp_set',
    'source_zone',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'owner',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'timeout',
    'translated',
    'verbatim',
    'vpn'
})

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'count', 'log', 'dscp'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2

_IPSET = [nacaddr.IP('10.0.0.0/8'),
          nacaddr.IP('2001:4860:8000::/33')]
_IPSET2 = [nacaddr.IP('10.23.0.0/22'), nacaddr.IP('10.23.0.6/23', strict=False)]
_IPSET3 = [nacaddr.IP('10.23.0.0/23')]
_IPSET4 = [nacaddr.IP('10.0.0.0/20')]
_IPSET5 = [nacaddr.IP('10.0.0.0/24')]


class JuniperSRXTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testHeaderComment(self):
    pol = policy.ParsePolicy(GOOD_HEADER + ICMP_TYPE_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('This is a test acl with a comment', output, output)

  def testHeaderApplyGroups(self):
    pol = policy.ParsePolicy(GOOD_HEADER_5 + ICMP_TYPE_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('apply-groups [ tcp-test1 tcp-test2 ]', output,
                  output)

  def testHeaderApplyGroupsExcept(self):
    pol = policy.ParsePolicy(GOOD_HEADER_6 + ICMP_TYPE_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('apply-groups-except [ tcp-test1 tcp-test2 ]', output,
                  output)

  def testLongComment(self):
    expected_output = """
            /*
            This header is very very very very very very very very very very
            very very very very very very very very very very large
            */"""
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']
    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn(expected_output, output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testTermAndFilterName(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('policy good-term-1 {', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testVpnWithoutPolicy(self):
    self.naming.GetNetAddr.return_value = _IPSET

    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_3,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('ipsec-vpn good-vpn-3;', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testVpnWithPolicy(self):
    self.naming.GetNetAddr.return_value = _IPSET

    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_4,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('ipsec-vpn good-vpn-4;', output, output)
    self.assertIn('pair-policy policy-4;', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testVpnWithDrop(self):
    self.naming.GetNetAddr.return_value = _IPSET

    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + BAD_TERM_1,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertNotIn('ipsec-vpn good-vpn-4;', output, output)
    self.assertNotIn('pair-policy policy-4;', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testDefaultDeny(self):
    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + DEFAULT_TERM_1,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('deny;', output, output)

  def testIcmpTypes(self):
    pol = policy.ParsePolicy(GOOD_HEADER + ICMP_TYPE_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('application test-icmp-app;', output, output)
    self.assertIn('application test-icmp-app {', output, output)
    self.assertIn('term t1 protocol icmp icmp-type 0 inactivity-timeout 60',
                  output, output)
    self.assertIn('term t2 protocol icmp icmp-type 8 inactivity-timeout 60',
                  output, output)

  def testLongIcmpTypes(self):
    pol = policy.ParsePolicy(GOOD_HEADER + LONG_IPV6_ICMP_TERM, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))

    # Make sure that the application isn't split into an application set
    # due to ICMP term usage up to 8 terms.
    self.assertNotIn('application-set accept-icmpv6-types-app', output)
    self.assertIn('application accept-icmpv6-types-app;', output)

    # Use regex to check for there being a single application with exactly 8
    # terms in it.
    pattern = re.compile(
        r'application accept-icmpv6-types-app \{\s+(term t\d protocol icmp6 icmp6-type \d{1,3} inactivity-timeout 60;\s+){8}\}'
    )
    self.assertTrue(pattern.search(output), output)

  def testLongSplitIcmpTypes(self):
    pol = policy.ParsePolicy(GOOD_HEADER + LONG_IPV6_ICMP_TERM2, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))

    # Check the application was split into a set of many applications; 9 terms.
    pattern = re.compile(
        r'application-set accept-icmpv6-types-app \{\s+(application accept-icmpv6-types-app\d;\s+){9}\}')
    self.assertTrue(pattern.search(output), output)

    # Check that each of the 9 applications with 1 term each.
    pattern = re.compile(
        r'(application accept-icmpv6-types-app\d \{\s+(term t1 protocol icmp6 icmp6-type \d{1,3} inactivity-timeout 60;\s+)\}\s+){9}'
    )
    self.assertTrue(pattern.search(output), output)

  def testAllIcmpTypes(self):
    pol = policy.ParsePolicy(GOOD_HEADER + ICMP_ALL_TERM, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))

    # Check for split into application set of many applications; 18 terms.
    pattern = re.compile(
        r'application-set accept-icmp-types-app \{\s+(application accept-icmp-types-app\d{1,2};\s+){18}\}')
    self.assertTrue(pattern.search(output), output)

    # Check that each of the 18 applications have 1 term each.
    pattern = re.compile(
        r'(application accept-icmp-types-app\d{1,2} \{\s+(term t1 protocol icmp icmp-type \d{1,3} inactivity-timeout 60;\s+)\}\s+){18}'
    )
    self.assertTrue(pattern.search(output), output)

  def testAllIcmp6Types(self):
    pol = policy.ParsePolicy(GOOD_HEADER + ICMP6_ALL_TERM, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    # Check for 29 applications.
    pattern = re.compile(
        r'application-set accept-icmpv6-types-app \{\s+(application accept-icmpv6-types-app\d{1,2};\s+){29}\}'
    )
    self.assertTrue(pattern.search(output), output)

    # Check that each of the 4 applications have between 1 and 8 terms.
    pattern = re.compile(
        r'(application accept-icmpv6-types-app\d{1,2} \{\s+(term t1 protocol icmp6 icmp6-type \d{1,3} inactivity-timeout 60;\s+)\}\s+){29}'
    )
    self.assertTrue(pattern.search(output), output)

  def testLoggingBothAccept(self):
    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER
                                                   + GOOD_TERM_LOG_1,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('session-init;', output)
    self.assertIn('session-close;', output)

  def testLoggingBothDeny(self):
    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER
                                                   + GOOD_TERM_LOG_2,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('session-init;', output)
    self.assertIn('session-close;', output)

  def testLoggingTrueAccept(self):
    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER
                                                   + GOOD_TERM_LOG_3,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('session-close;', output)
    self.assertNotIn('session-init;', output)

  def testLoggingTrueDeny(self):
    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER
                                                   + GOOD_TERM_LOG_4,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('session-init;', output)
    self.assertNotIn('session-close;', output)

  def testCounterAccept(self):
    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER
                                                   + GOOD_TERM_COUNT_1,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('count;', output)

  def testCounterDeny(self):
    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER
                                                   + GOOD_TERM_COUNT_2,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('count;', output)

  def testOwnerTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER + OWNER_TERM, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('            /*\n'
                  '            Owner: foo@google.com\n'
                  '            */', output, output)

  def testBadICMP(self):
    pol = policy.ParsePolicy(GOOD_HEADER + BAD_ICMP_TERM_1, self.naming)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      junipersrx.JuniperSRX, pol, EXP_INFO)

  def testICMPProtocolOnly(self):
    pol = policy.ParsePolicy(GOOD_HEADER + ICMP_ONLY_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('protocol icmp;', output, output)

  def testMultipleProtocolGrouping(self):
    pol = policy.ParsePolicy(GOOD_HEADER + MULTIPLE_PROTOCOLS_TERM, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('application-set multi-proto-app {', output, output)
    self.assertIn('application multi-proto-app1;', output, output)
    self.assertIn('application multi-proto-app2;', output, output)
    self.assertIn('application multi-proto-app3;', output, output)
    self.assertIn('application multi-proto-app1 {', output, output)
    self.assertIn('term t1 protocol tcp;', output, output)
    self.assertIn('application multi-proto-app2 {', output, output)
    self.assertIn('term t2 protocol udp;', output, output)
    self.assertIn('application multi-proto-app3 {', output, output)
    self.assertIn('term t3 protocol icmp;', output, output)

  def testGlobalPolicyHeader(self):
    pol = policy.ParsePolicy(GOOD_HEADER_10 + MULTIPLE_PROTOCOLS_TERM,
                             self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertEqual(output.count('global {'), 2)
    self.assertNotIn('from-zone all to-zone all {', output)

  def testBadGlobalPolicyHeaderZoneBook(self):
    pol = policy.ParsePolicy(BAD_HEADER_3 + MULTIPLE_PROTOCOLS_TERM,
                             self.naming)
    self.assertRaises(junipersrx.UnsupportedFilterError, junipersrx.JuniperSRX,
                      pol, EXP_INFO)

  def testBadGlobalPolicyHeaderNameAll(self):
    pol = policy.ParsePolicy(BAD_HEADER_4 + MULTIPLE_PROTOCOLS_TERM,
                             self.naming)
    self.assertRaises(junipersrx.UnsupportedFilterError, junipersrx.JuniperSRX,
                      pol, EXP_INFO)

  def testBadHeaderType(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(BAD_HEADER + GOOD_TERM_1, self.naming)
    self.assertRaises(junipersrx.UnsupportedFilterError, junipersrx.JuniperSRX,
                      pol, EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testBadHeaderMultiAF(self):
    # test for multiple address faimilies in header
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(BAD_HEADER_1 + GOOD_TERM_1, self.naming)
    self.assertRaises(junipersrx.ConflictingTargetOptionsError,
                      junipersrx.JuniperSRX,
                      pol, EXP_INFO)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  @mock.patch.object(junipersrx.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    _ = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + EXPIRED_TERM_1,
                                                 self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s>%s is expired.',
        'expired_test', 'trust', 'untrust')

  @mock.patch.object(junipersrx.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    _ = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + EXPIRING_TERM %
                                                 exp_date.strftime('%Y-%m-%d'),
                                                 self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s>%s expires in '
        'less than two weeks.', 'is_expiring',
        'trust', 'untrust')

  def testTimeout(self):
    pol = policy.ParsePolicy(GOOD_HEADER + TIMEOUT_TERM, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('timeout 77', output, output)

  def testZoneGlobal(self):
    pol = policy.ParsePolicy(GOOD_HEADER_10 + GLOBAL_ZONE_TERM, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('from-zone', output, output)
    self.assertIn('szone1 szone2', output, output)
    self.assertIn('to-zone', output, output)
    self.assertIn('dzone1 dzone2', output, output)

  def testZoneNonGlobal(self):
    pol = policy.ParsePolicy(GOOD_HEADER + GLOBAL_ZONE_TERM, self.naming)
    self.assertRaises(junipersrx.UnsupportedFilterError,
                      junipersrx.JuniperSRX, pol, EXP_INFO)

  def testIcmpV6(self):
    pol = policy.ParsePolicy(GOOD_HEADER + IPV6_ICMP_TERM, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('protocol icmp6', output, output)
    self.assertIn('icmp6-type', output, output)

  def testReplaceStatement(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('replace: address-book', output, output)
    self.assertIn('replace: policies', output, output)
    self.assertIn('replace: applications', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testAdressBookBothAFs(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('replace: address-book {', output, output)
    self.assertIn('global {', output, output)
    self.assertIn('2001:4860:8000::/33', output, output)
    self.assertIn('10.0.0.0/8', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testAdressBookIPv4(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('replace: address-book {', output, output)
    self.assertIn('global {', output, output)
    self.assertNotIn('2001:4860:8000::/33', output, output)
    self.assertIn('10.0.0.0/8', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testAdressBookIPv6(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_4 + GOOD_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('replace: address-book {', output, output)
    self.assertIn('global {', output, output)
    self.assertIn('2001:4860:8000::/33', output, output)
    self.assertNotIn('10.0.0.0/8', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testAddressBookContainsSmallerPrefix(self):
    _IPSET2[0].parent_token = 'FOOBAR'
    _IPSET2[1].parent_token = 'SOME_HOST'
    _IPSET3[0].parent_token = 'FOOBAR'
    self.naming.GetNetAddr.side_effect = [_IPSET2, _IPSET3]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1 + GOOD_HEADER_2 +
                             GOOD_TERM_12, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('address FOOBAR_0 10.23.0.0/22;', output, output)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('SOME_HOST'),
        mock.call('FOOBAR')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('SMTP', 'tcp')] * 2)

  def testAddressBookContainsLargerPrefix(self):
    _IPSET2[0].parent_token = 'FOOBAR'
    _IPSET2[1].parent_token = 'SOME_HOST'
    _IPSET3[0].parent_token = 'FOOBAR'
    self.naming.GetNetAddr.side_effect = [_IPSET3, _IPSET2]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_12 + GOOD_HEADER +
                             GOOD_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('address FOOBAR_0 10.23.0.0/22;', output, output)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('FOOBAR'),
        mock.call('SOME_HOST')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('SMTP', 'tcp')] * 2)

  def testZoneAdressBookBothAFs(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_9 + GOOD_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('security-zone untrust {', output, output)
    self.assertIn('replace: address-book {', output, output)
    self.assertIn('2001:4860:8000::/33', output, output)
    self.assertIn('10.0.0.0/8', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testZoneAdressBookIPv4(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_7 + GOOD_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('security-zone untrust {', output, output)
    self.assertIn('replace: address-book {', output, output)
    self.assertNotIn('2001:4860:8000::/33', output, output)
    self.assertIn('10.0.0.0/8', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testZoneAdressBookIPv6(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_8 + GOOD_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('security-zone untrust {', output, output)
    self.assertIn('replace: address-book {', output, output)
    self.assertIn('2001:4860:8000::/33', output, output)
    self.assertNotIn('10.0.0.0/8', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def assertFalseUnorderedAddressBook(self, address_book):
    # This is very naive check that expects addresses to be exact as returned
    # from _OutOfOrderAddresses method. If you modify one please modify this one
    # as well.
    for line in address_book:
      if '10.0.0.0/8' in line:
        self.fail('Addresses in address book are out of order.')
      elif '1.0.0.0/8' in line:
        break

  def _OutOfOrderAddresses(self):
    x = nacaddr.IP('10.0.0.0/8')
    x.parent_token = 'test'
    y = nacaddr.IP('1.0.0.0/8')
    y.parent_token = 'out_of_order'

    return x, y

  def testAddressBookOrderingSuccess(self):
    self.naming.GetNetAddr.return_value = self._OutOfOrderAddresses()
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_2, self.naming)
    p = junipersrx.JuniperSRX(pol, EXP_INFO)

    self.assertFalseUnorderedAddressBook(p._GenerateAddressBook())

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testAddressBookOrderingAlreadyOrdered(self):
    y, x = self._OutOfOrderAddresses()
    self.naming.GetNetAddr.return_value = [x, y]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_2, self.naming)
    p = junipersrx.JuniperSRX(pol, EXP_INFO)

    self.assertFalseUnorderedAddressBook(p._GenerateAddressBook())

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def _AssertOrder(self, strings, expected_order):
    order = copy.copy(expected_order)
    matcher = order.pop(0)
    for line in strings:
      if matcher in line:
        if not order:
          return
        matcher = order.pop(0)

    self.fail('Strings weren\'t in expected order.\nExpected:\n  %s\n\nGot:\n%s'
              % ('\n  '.join(expected_order), '\n'.join(strings)))

  def testApplicationsOrderingSuccess(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.side_effect = [['80', '80'], ['25', '25']]

    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_2 + GOOD_TERM_1,
                             self.naming)
    p = junipersrx.JuniperSRX(pol, EXP_INFO)
    self._AssertOrder(p._GenerateApplications(),
                      ['application good-term-1-app1',
                       'application good-term-2-app1',
                       'application-set good-term-1-app',
                       'application-set good-term-2-app'])

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('SOME_HOST')] * 2)
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('SMTP', 'tcp')] * 2)

  def testApplicationsOrderingAlreadyOrdered(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.side_effect = [['25', '25'], ['80', '80']]

    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_1 + GOOD_TERM_2,
                             self.naming)
    p = junipersrx.JuniperSRX(pol, EXP_INFO)
    self._AssertOrder(p._GenerateApplications(),
                      ['application good-term-1-app1',
                       'application good-term-2-app1',
                       'application-set good-term-1-app',
                       'application-set good-term-2-app'])

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('SOME_HOST')] * 2)
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('SMTP', 'tcp')] * 2)

  def testDscpWithByte(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_10,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('dscp b111000;', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testDscpWithClass(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]

    srx = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_11,
                                                   self.naming), EXP_INFO)
    output = str(srx)
    self.assertIn('dscp af42;', output, output)
    self.assertIn('dscp [ af41-af42 5 ];', output, output)
    self.assertIn('dscp-except [ be ];', output, output)

    self.naming.GetNetAddr.assert_called_once_with('SOME_HOST')

  def testLargeTermSplitting(self):
    ips = list(nacaddr.IP('10.0.8.0/21').subnets(new_prefix=32))
    mo_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        mo_ips.append(nacaddr.IP(ip))
      counter += 1

    ips = list(nacaddr.IP('10.0.0.0/21').subnets(new_prefix=32))
    prodcolos_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        prodcolos_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetNetAddr.side_effect = [mo_ips, prodcolos_ips]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_14, self.naming)
    srx = junipersrx.JuniperSRX(pol, EXP_INFO)
    self.assertEqual(len(srx.policy.filters[0][1]), 4)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('FOOBAR'),
        mock.call('SOME_HOST')])
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testLargeTermSplittingV6(self):
    ips = list(nacaddr.IP('2620:0:1000:3103:eca0:2c09:6b32:e000/119'
                          ).subnets(new_prefix=128))
    mo_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        mo_ips.append(nacaddr.IP(ip))
      counter += 1

    ips = list(nacaddr.IP('2720:0:1000:3103:eca0:2c09:6b32:e000/119'
                          ).subnets(new_prefix=128))
    prodcolos_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        prodcolos_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetNetAddr.side_effect = [mo_ips, prodcolos_ips]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_14, self.naming)
    srx = junipersrx.JuniperSRX(pol, EXP_INFO)
    self.assertEqual(len(srx.policy.filters[0][1]), 4)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('FOOBAR'),
        mock.call('SOME_HOST')])
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testLargeTermSplitIgnoreV6(self):
    ips = list(nacaddr.IP('2620:0:1000:3103:eca0:2c09:6b32:e000/119'
                          ).subnets(new_prefix=128))
    mo_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        mo_ips.append(nacaddr.IP(ip))
      counter += 1

    ips = list(nacaddr.IP('2720:0:1000:3103:eca0:2c09:6b32:e000/119'
                          ).subnets(new_prefix=128))
    ips.append(nacaddr.IPv4('10.0.0.1/32'))
    prodcolos_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        prodcolos_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetNetAddr.side_effect = [mo_ips, prodcolos_ips]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_14, self.naming)
    srx = junipersrx.JuniperSRX(pol, EXP_INFO)
    self.assertEqual(len(srx.policy.filters[0][1]), 1)

  def testDuplicateTermsInDifferentZones(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.side_effect = [['25'], ['26']]

    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_2 + GOOD_HEADER_11 +
                             GOOD_TERM_2, self.naming)
    self.assertRaises(junipersrx.ConflictingApplicationSetsError,
                      junipersrx.JuniperSRX, pol, EXP_INFO)

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('SOME_HOST')] * 2)
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('SMTP', 'tcp')] * 2)

  def testBuildTokens(self):
    self.naming.GetServiceByProto.side_effect = [['25'], ['26']]
    pol1 = junipersrx.JuniperSRX(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_2,
                                                    self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    self.naming.GetServiceByProto.side_effect = [['25'], ['26']]

    pol1 = junipersrx.JuniperSRX(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_15, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testOptimizedGlobalAddressBook(self):
    foobar_ips = [nacaddr.IP('172.16.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.17.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.18.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.19.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.22.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.23.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.24.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.25.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.26.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.27.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.28.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.29.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.30.0.0/16', token='FOOBAR'),
                  nacaddr.IP('172.31.0.0/16', token='FOOBAR')]
    some_host_ips = [nacaddr.IP('172.20.0.0/16', token='SOME_HOST'),
                     nacaddr.IP('172.21.0.0/16', token='SOME_HOST'),
                     nacaddr.IP('10.0.0.0/8', token='SOME_HOST')]

    self.naming.GetNetAddr.side_effect = [foobar_ips, some_host_ips,
                                          some_host_ips]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_17 + GOOD_HEADER_2 +
                             GOOD_TERM_15, self.naming)
    srx = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('address FOOBAR_0 172.16.0.0/14', srx, srx)
    self.assertIn('address FOOBAR_1 172.22.0.0/15;', srx, srx)
    self.assertIn('address FOOBAR_2 172.24.0.0/13;', srx, srx)
    self.assertIn('address SOME_HOST_0 10.0.0.0/8;', srx, srx)
    self.assertIn('address SOME_HOST_1 172.20.0.0/15;', srx, srx)
    self.assertNotIn('/16', srx, srx)

  def testNakedExclude(self):
    small = [nacaddr.IP('10.0.0.0/24', 'SMALL', 'SMALL')]
    self.naming.GetNetAddr.side_effect = [small]

    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_18, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn(
        'address GOOD_TERM_18_SRC_EXCLUDE_2 10.0.1.0/24;', output, output)
    self.assertIn(
        'address GOOD_TERM_18_SRC_EXCLUDE_3 10.0.2.0/23;', output, output)
    self.assertIn(
        'address GOOD_TERM_18_SRC_EXCLUDE_4 10.0.4.0/22;', output, output)
    self.assertIn(
        'address GOOD_TERM_18_SRC_EXCLUDE_5 10.0.8.0/21;', output, output)
    self.assertNotIn('10.0.0.0', output)

  def testSourceExclude(self):
    large = [nacaddr.IP('10.0.0.0/20', 'LARGE', 'LARGE')]
    small = [nacaddr.IP('10.0.0.0/24', 'SMALL', 'SMALL')]
    self.naming.GetNetAddr.side_effect = [large, small]

    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_19, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn(
        'address GOOD_TERM_19_SRC_EXCLUDE_0 10.0.1.0/24;', output, output)
    self.assertIn(
        'address GOOD_TERM_19_SRC_EXCLUDE_1 10.0.2.0/23;', output, output)
    self.assertIn(
        'address GOOD_TERM_19_SRC_EXCLUDE_2 10.0.4.0/22;', output, output)
    self.assertIn(
        'address GOOD_TERM_19_SRC_EXCLUDE_3 10.0.8.0/21;', output, output)
    self.assertNotIn('10.0.0.0/24', output)

  def testPlatformExclude(self):
    large = [nacaddr.IP('10.0.0.0/20', 'LARGE', 'LARGE')]
    small = [nacaddr.IP('10.0.0.0/24', 'SMALL', 'SMALL')]
    self.naming.GetNetAddr.side_effect = [large, small]

    pol = policy.ParsePolicy(GOOD_HEADER + PLATFORM_EXCLUDE_TERM + GOOD_TERM_19,
                             self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('good_term_19', output,
                  output)
    self.assertNotIn('platform-exclude-term', output)

  def testPlatformTerm(self):
    large = [nacaddr.IP('10.0.0.0/20', 'LARGE', 'LARGE')]
    small = [nacaddr.IP('10.0.0.0/24', 'SMALL', 'SMALL')]
    self.naming.GetNetAddr.side_effect = [large, small]

    pol = policy.ParsePolicy(GOOD_HEADER + PLATFORM_TERM + GOOD_TERM_19,
                             self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('good_term_19', output,
                  output)
    self.assertIn('platform-term', output, output)

  def testPlatformExcludeWithSourceExclude(self):
    foo = [nacaddr.IP('192.1.0.0/20', 'FOO', 'FOO')]
    large = [nacaddr.IP('10.0.0.0/20', 'LARGE', 'LARGE')]
    small = [nacaddr.IP('10.0.0.0/24', 'SMALL', 'SMALL')]
    self.naming.GetNetAddr.side_effect = [foo, large, small]

    pol = policy.ParsePolicy(
        GOOD_HEADER + PLATFORM_EXCLUDE_ADDRESS_TERM + GOOD_TERM_19, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('address GOOD_TERM_19_SRC_EXCLUDE_0 10.0.1.0/24;', output,
                  output)
    self.assertIn('address GOOD_TERM_19_SRC_EXCLUDE_1 10.0.2.0/23;', output,
                  output)
    self.assertIn('address GOOD_TERM_19_SRC_EXCLUDE_2 10.0.4.0/22;', output,
                  output)
    self.assertIn('address GOOD_TERM_19_SRC_EXCLUDE_3 10.0.8.0/21;', output,
                  output)
    self.assertNotIn('10.0.0.0/24', output)
    self.assertNotIn('192.1.0.0/20', output)
    self.assertNotIn('platform-exclude-term', output)

  def testMixedVersionIcmp(self):
    pol = policy.ParsePolicy(GOOD_HEADER + ICMP_TYPE_TERM_1 + IPV6_ICMP_TERM,
                             self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('term t6 protocol icmp6 icmp6-type 129 '
                  'inactivity-timeout 60;', output)
    self.assertIn('term t1 protocol icmp icmp-type 0 '
                  'inactivity-timeout 60;', output)

  def testOptimizedApplicationset(self):
    some_host = [nacaddr.IP('10.0.0.1/32', token='SOMEHOST')]
    foo = [nacaddr.IP('10.0.0.2/32', token='FOO')]
    foobar = [nacaddr.IP('10.0.0.3/32', token='FOOBAR')]
    self.naming.GetNetAddr.side_effect = [some_host, foo, foobar,
                                          foobar, some_host]

    self.naming.GetServiceByProto.side_effect = [['25', '25'], ['80', '80'],
                                                 ['25', '25'], ['25', '25']]

    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_2 + GOOD_TERM_20 +
                             GOOD_TERM_12 + GOOD_HEADER_2 + GOOD_TERM_14,
                             self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertNotIn('dup-of-term-1-app', output)

  def testExpressPath(self):
    some_host = [nacaddr.IP('10.0.0.1/32', token='SOMEHOST')]
    self.naming.GetNetAddr.side_effect = [some_host, some_host]

    self.naming.GetServiceByProto.side_effect = [['25', '25'], ['25', '25']]

    pol = policy.ParsePolicy(GOOD_HEADER_14 + GOOD_TERM_2 + DEFAULT_TERM_1 +
                             GOOD_HEADER + GOOD_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertIn('services-offload;', output)
    self.assertIn('deny;', output)
    self.assertIn('permit;', output)

  def testDropEstablished(self):
    some_host = [nacaddr.IP('10.0.0.1/32', token='FOO')]
    self.naming.GetServiceByProto.side_effect = [['25', '25'], ['443', '443'],
                                                 ['25', '25'], ['443', '443']]
    self.naming.GetNetAddr.side_effect = [some_host, some_host, some_host,
                                          some_host]
    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1 + GOOD_TERM_21 +
                             DEFAULT_TERM_1 + GOOD_HEADER_2 +
                             TCP_ESTABLISHED_TERM + UDP_ESTABLISHED_TERM +
                             DEFAULT_TERM_1, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertNotIn('udp-established-term', output)
    self.assertNotIn('tcp-established-term', output)

  def testStatelessReply(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    ret = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_1 + ICMP_RESPONSE_TERM,
                             self.naming)

    _, terms = ret.filters[0]
    for term in terms:
      if term.protocol[0] == 'icmp':
        term.stateless_reply = True

    srx = junipersrx.JuniperSRX(ret, EXP_INFO)

    output = str(srx)
    self.assertIn('policy good-term-1 {', output, output)
    self.assertNotIn('policy icmp_response-term {', output, output)

  def testNoVerbose(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']
    pol = policy.ParsePolicy(GOOD_HEADER_NOVERBOSE + GOOD_TERM_1, self.naming)
    srx = junipersrx.JuniperSRX(pol, EXP_INFO)
    self.assertNotIn('This is a test acl with a comment', str(srx))
    self.assertNotIn('very very very', str(srx))

  def testDropUndefinedAddressbookTermsV4ForV6Render(self):
    # V4-only term should be dropped when rendering ACL as V6 - b/172933068
    udon = [nacaddr.IP('10.0.0.2/32', token='UDON')]
    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [udon]

    # GOOD_HEADER_4 specifies V6 rendering
    pol = policy.ParsePolicy(GOOD_HEADER_4 + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertNotIn('good_term_21', output)

  def testDeleteV4AddressEntriesForV6Render(self):
    # Confirm V4 address book entries are not generated when rendering as V6
    udon = [nacaddr.IP('10.0.0.2/32', token='UDON')]
    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [udon]

    # GOOD_HEADER_4 specifies V6 rendering
    pol = policy.ParsePolicy(GOOD_HEADER_4 + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertNotIn('10.0.0.2/32', output)

  def testDropUndefinedAddressbookTermsV6ForV4Render(self):
    # V6-only term should be dropped when rendering ACL as V4 - b/172933068
    udon = [nacaddr.IP('2001:4860:8000::5/128', token='UDON')]
    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [udon]

    # GOOD_HEADER_3 specifies V4 rendering
    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertNotIn('good_term_21', output)

  def testDeleteV6AddressEntriesForV4Render(self):
    # Confirm V6 address book entries are not generated when rendering as V4
    udon = [nacaddr.IP('2001:4860:8000::5/128', token='UDON')]
    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [udon]

    # GOOD_HEADER_3 specifies V4 rendering
    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    self.assertNotIn('2001:4860:8000::5/128', output)

  def testCreateV6AddressEntriesForMixedRender(self):
    # V6-only 1024+ IPs; MIXED rendering
    # Confirm that address set names used in policy are also created in
    # address book

    # TODO(nitb) Move multiple IP networks generation logic to separate method
    # and reuse in other tests
    overflow_ips = [
        nacaddr.IP('2001:4860:8000::5/128'),
        nacaddr.IP('3051:abd2:5400::9/128'),
        nacaddr.IP('aee2:37ba:3cc0::3/128'),
        nacaddr.IP('6f5d:abd2:1403::1/128'),
        nacaddr.IP('577e:5400:3051::6/128'),
        nacaddr.IP('af22:32d2:3f00::2/128')
    ]

    ips = list(
        nacaddr.IP('2620:0:1000:3103:eca0:2c09:6b32:e000/117').subnets(
            new_prefix=128))
    mo_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        mo_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [overflow_ips+mo_ips]

    # GOOD_HEADER = MIXED rendering
    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))

    # extract address-set-names referenced in policy blocks
    partial_pruned_acl = output.split('replace: policies')[1].split(
        'destination-address [ ')[1:]

    # verify that there are exactly 9 terms in the ACL by checking if
    # partial_pruned_acl contains exactly 9 elements
    self.assertEqual(len(partial_pruned_acl), 9)

    for text in partial_pruned_acl:
      address_set_name = text.split(' ];')[0]

      if address_set_name:
        address_set_count = output.count(address_set_name)
        # check if each addresssetname referenced in policy occurs more than
        # once i.e. is defined in the address book
        self.assertGreater(address_set_count, 1)

  def testCreateV6AddressEntriesForV6Render(self):
    # V6-only 1024+ IPs; V6 rendering
    overflow_ips = [
        nacaddr.IP('2001:4860:8000::5/128'),
        nacaddr.IP('3051:abd2:5400::9/128'),
        nacaddr.IP('aee2:37ba:3cc0::3/128'),
        nacaddr.IP('6f5d:abd2:1403::1/128'),
        nacaddr.IP('577e:5400:3051::6/128'),
        nacaddr.IP('af22:32d2:3f00::2/128')
    ]

    ips = list(
        nacaddr.IP('2620:0:1000:3103:eca0:2c09:6b32:e000/117').subnets(
            new_prefix=128))
    mo_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        mo_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [overflow_ips+mo_ips]

    # GOOD_HEADER_4 = V6 rendering
    pol = policy.ParsePolicy(GOOD_HEADER_4 + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))

    # extract address-set-names referenced in policy blocks
    partial_pruned_acl = output.split('replace: policies')[1].split(
        'destination-address [ ')[1:]

    # verify that there are exactly 9 terms in the ACL by checking if
    # partial_pruned_acl contains exactly 9 elements
    self.assertEqual(len(partial_pruned_acl), 9)

    for text in partial_pruned_acl:
      address_set_name = text.split(' ];')[0]

      if address_set_name:
        address_set_count = output.count(address_set_name)
        # check if each addresssetname referenced in policy occurs more than
        # once i.e. is defined in the address book
        self.assertGreater(address_set_count, 1)

  def testEmptyACLEmptyAddressBookV6IpsV4Render(self):
    # V6-only 1024+ IPs; V4 rendering
    overflow_ips = [
        nacaddr.IP('2001:4860:8000::5/128'),
        nacaddr.IP('3051:abd2:5400::9/128'),
        nacaddr.IP('aee2:37ba:3cc0::3/128'),
        nacaddr.IP('6f5d:abd2:1403::1/128'),
        nacaddr.IP('577e:5400:3051::6/128'),
        nacaddr.IP('af22:32d2:3f00::2/128')
    ]

    ips = list(
        nacaddr.IP('2620:0:1000:3103:eca0:2c09:6b32:e000/117').subnets(
            new_prefix=128))
    mo_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        mo_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [overflow_ips+mo_ips]

    # GOOD_HEADER_3 = V4 rendering
    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    address_set_count = output.count('address')

    # verify acl is empty
    self.assertNotIn('policy', output)

    # verify address book is empty
    self.assertEqual(address_set_count, 1)

  def testCreateV4AddressEntriesForMixedRender(self):
    # V4-only 1024+ IPs; MIXED rendering
    overflow_ips = [
        nacaddr.IP('23.2.3.3/32'),
        nacaddr.IP('54.2.3.4/32'),
        nacaddr.IP('76.2.3.5/32'),
        nacaddr.IP('132.2.3.6/32'),
        nacaddr.IP('197.2.3.7/32')
    ]

    ips = list(nacaddr.IP('10.0.8.0/21').subnets(new_prefix=32))
    mo_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        mo_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [overflow_ips+mo_ips]

    # GOOD_HEADER = MIXED rendering
    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))

    # extract address-set-names referenced in policy blocks
    partial_pruned_acl = output.split('replace: policies')[1].split(
        'destination-address [ ')[1:]

    # verify that there are exactly 3 terms in the ACL by checking if
    # partial_pruned_acl contains exactly 3 elements
    self.assertEqual(len(partial_pruned_acl), 3)

    for text in partial_pruned_acl:
      address_set_name = text.split(' ];')[0]

      if address_set_name:
        address_set_count = output.count(address_set_name)
        # check if each addresssetname referenced in policy occurs more than
        # once i.e. is defined in the address book
        self.assertGreater(address_set_count, 1)

  def testEmptyACLEmptyAddressBookV4IpsV6Render(self):
    # V4-only 1024+ IPs; V6 rendering
    overflow_ips = [
        nacaddr.IP('23.2.3.3/32'),
        nacaddr.IP('54.2.3.4/32'),
        nacaddr.IP('76.2.3.5/32'),
        nacaddr.IP('132.2.3.6/32'),
        nacaddr.IP('197.2.3.7/32')
    ]

    ips = list(nacaddr.IP('10.0.8.0/21').subnets(new_prefix=32))
    mo_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        mo_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [overflow_ips+mo_ips]

    # GOOD_HEADER_4 = V6 rendering
    pol = policy.ParsePolicy(GOOD_HEADER_4 + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))
    address_set_count = output.count('address')

    # verify acl is empty
    self.assertNotIn('policy', output)

    # verify address book is empty
    self.assertEqual(address_set_count, 1)

  def testCreateV4AddressEntriesForV4Render(self):
    # V4-only 1024+ IPs; V4 rendering
    overflow_ips = [
        nacaddr.IP('23.2.3.3/32'),
        nacaddr.IP('54.2.3.4/32'),
        nacaddr.IP('76.2.3.5/32'),
        nacaddr.IP('132.2.3.6/32'),
        nacaddr.IP('197.2.3.7/32')
    ]

    ips = list(nacaddr.IP('10.0.8.0/21').subnets(new_prefix=32))
    mo_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        mo_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [overflow_ips+mo_ips]

    # GOOD_HEADER_3 = V4 rendering
    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))

    # extract address-set-names referenced in policy blocks
    partial_pruned_acl = output.split('replace: policies')[1].split(
        'destination-address [ ')[1:]

    # verify that there are exactly 3 terms in the ACL by checking if
    # partial_pruned_acl contains exactly 3 elements
    self.assertEqual(len(partial_pruned_acl), 3)

    for text in partial_pruned_acl:
      address_set_name = text.split(' ];')[0]

      if address_set_name:
        address_set_count = output.count(address_set_name)
        # check if each addresssetname referenced in policy occurs more than
        # once i.e. is defined in the address book
        self.assertGreater(address_set_count, 1)

  def testCreateMixedAddressEntriesForMixedRender(self):
    # 513V6 and 512V4 IPs; MIXED rendering
    overflow_ips = [
        nacaddr.IP('2001:4860:8000::5/128')
    ]

    ips = list(nacaddr.IP('10.0.8.0/22').subnets(new_prefix=32))
    v4_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        v4_ips.append(nacaddr.IP(ip))
      counter += 1

    ips = list(
        nacaddr.IP('2620:0:1000:3103:eca0:2c09:6b32:e000/118').subnets(
            new_prefix=128))
    v6_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        v6_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [overflow_ips + v4_ips + v6_ips]

    # GOOD_HEADER = MIXED rendering
    pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))

    # extract address-set-names referenced in policy blocks
    partial_pruned_acl = output.split('replace: policies')[1].split(
        'destination-address [ ')[1:]

    # verify that there are exactly 6 terms in the ACL by checking if
    # partial_pruned_acl contains exactly 6 elements
    self.assertEqual(len(partial_pruned_acl), 6)

    for text in partial_pruned_acl:
      address_set_name = text.split(' ];')[0]

      if address_set_name:
        address_set_count = output.count(address_set_name)
        # check if each addresssetname referenced in policy occurs more than
        # once i.e. is defined in the address book
        self.assertGreater(address_set_count, 1)

  def testCreateV6AddressEntriesForV6Render2(self):
    # 513V6 and 512V4 IPs; V6 rendering
    overflow_ips = [
        nacaddr.IP('2001:4860:8000::5/128')
    ]

    ips = list(nacaddr.IP('10.0.8.0/22').subnets(new_prefix=32))
    v4_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        v4_ips.append(nacaddr.IP(ip))
      counter += 1

    ips = list(nacaddr.IP('2620:0:1000:3103:eca0:2c09:6b32:e000/118').subnets(new_prefix=128))
    v6_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        v6_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [overflow_ips + v4_ips + v6_ips]

    # GOOD_HEADER_4 = V6 rendering
    pol = policy.ParsePolicy(GOOD_HEADER_4 + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))

    # extract address-set-names referenced in policy blocks
    partial_pruned_acl = output.split('replace: policies')[1].split(
        'destination-address [ ')[1:]

    # verify that there are exactly 5 terms in the ACL by checking if
    # partial_pruned_acl contains exactly 5 elements
    self.assertEqual(len(partial_pruned_acl), 5)

    for text in partial_pruned_acl:
      address_set_name = text.split(' ];')[0]

      if address_set_name:
        address_set_count = output.count(address_set_name)
        # check if each addresssetname referenced in policy occurs more than
        # once i.e. is defined in the address book
        self.assertGreater(address_set_count, 1)

  def testCreateV4AddressEntriesForV4Render2(self):
    # 513V6 and 512V4 IPs; V4 rendering
    overflow_ips = [
        nacaddr.IP('2001:4860:8000::5/128')
    ]

    ips = list(nacaddr.IP('10.0.8.0/22').subnets(new_prefix=32))
    v4_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        v4_ips.append(nacaddr.IP(ip))
      counter += 1

    ips = list(nacaddr.IP('2620:0:1000:3103:eca0:2c09:6b32:e000/118').subnets(new_prefix=128))
    v6_ips = []
    counter = 0
    for ip in ips:
      if counter % 2 == 0:
        v6_ips.append(nacaddr.IP(ip))
      counter += 1

    self.naming.GetServiceByProto.side_effect = [['25', '25']]
    self.naming.GetNetAddr.side_effect = [overflow_ips + v4_ips + v6_ips]

    # GOOD_HEADER_3 = V4 rendering
    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_21, self.naming)
    output = str(junipersrx.JuniperSRX(pol, EXP_INFO))

    # extract address-set-names referenced in policy blocks

    partial_pruned_acl = output.split('replace: policies')[1].split(
        'destination-address [ ')[1:]

    # verify that there is only one term in the ACL by checking if
    # partial_pruned_acl contains only one element
    self.assertEqual(len(partial_pruned_acl), 1)

    for text in partial_pruned_acl:
      address_set_name = text.split(' ];')[0]

      if address_set_name:
        address_set_count = output.count(address_set_name)
        # check if each addresssetname referenced in policy occurs more than
        # once i.e. is defined in the address book
        self.assertGreater(address_set_count, 1)

  def testEmptyApplications(self):
    self.naming.GetNetAddr.return_value = _IPSET

    # GOOD_HEADER_3 doesn't matter, any valid header should do
    pol = policy.ParsePolicy(GOOD_HEADER_3 + GOOD_TERM_23,
                             self.naming)
    p = junipersrx.JuniperSRX(pol, EXP_INFO)
    output = p._GenerateApplications()

    pattern = re.compile(r'delete: applications;')
    self.assertTrue(pattern.search(str(''.join(output))), ''.join(output))


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


# tests/lib/k8s_test.py

# Copyright 2015 Google Inc. All Rights Reserved.
#
# 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.
"""Unittest for K8s NetworkPolicy rendering module."""

from unittest import mock
from absl.testing import absltest
from absl.testing import parameterized

from capirca.lib import aclgenerator
from capirca.lib import k8s
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy
import yaml

GOOD_HEADER = """
header {
  comment:: "The general policy comment."
  target:: k8s
}
"""

GOOD_HEADER_INGRESS = """
header {
  comment:: "The general policy comment."
  target:: k8s INGRESS
}
"""

GOOD_HEADER_EGRESS = """
header {
  comment:: "The general policy comment."
  target:: k8s EGRESS
}
"""

GOOD_TERM = """
term good-term-1 {
  owner:: myself
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_CUSTOM_NAME = """
term %s {
  owner:: myself
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_CUSTOM_PROTO = """
term custom-proto-term {
  owner:: myself
  comment:: "custom proto term"
  source-address:: CORP_EXTERNAL
  protocol:: %s
  action:: accept
}
"""

GOOD_TERM_PROTO_ALL = """
term good-term-2 {
  owner:: myself
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  destination-port:: DNS
  protocol:: udp tcp sctp
  action:: accept
}
"""

GOOD_TERM_ALLOW_ALL_TCP = """
term good-term-3 {
  owner:: myself
  comment:: "DNS access from corp."
  source-address:: CORP_EXTERNAL
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_EGRESS = """
term good-term-4 {
  comment:: "DNS access from corp."
  destination-address:: CORP_EXTERNAL
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_TERM_EXPIRED = """
term good-term-expired {
  comment:: "Management access from corp."
  expiration:: 2001-01-01
  source-address:: CORP_EXTERNAL
  destination-tag:: ssh-servers
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_EXCLUDE_SOURCE = """
term good-term-exclude-source {
  comment:: "term with source exclusions"
  source-address:: ANY_IPS
  source-exclude:: TEST_IPS
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_EXCLUDE_DEST = """
term good-term-exclude-destination {
  comment:: "term with destination exclusions"
  destination-address:: ANY_IPS
  destination-exclude:: TEST_IPS
  protocol:: tcp
  action:: accept
}
"""

DEFAULT_DENY = """
term default-deny {
  comment:: "default_deny."
  action:: deny
}
"""

BAD_TERM_DENY = """
term bad-term-1 {
  comment:: "explicit deny"
  source-address:: CORP_EXTERNAL
  protocol:: tcp
  action:: deny
}
"""

BAD_TERM_INVALID_SOURCE_EXCLUDE = """
term bad-term-2 {
  comment:: "source exclude without source address"
  source-exclude:: CORP_EXTERNAL
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_NO_ADDR = """
term bad-term-3 {
  comment:: "ingress no source"
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_INGRESS_DESTINATION = """
term bad-term-4 {
  comment:: "source exclude without source address"
  source-address:: CORP_EXTERNAL
  destination-address:: CORP_EXTERNAL
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_SOURCE_PORT = """
term bad-term-5 {
  comment:: "source port restriction"
  source-address:: CORP_EXTERNAL
  source-port:: DNS
  protocol:: udp
  action:: accept
}
"""

BAD_TERM_EMPTY_SOURCE = """
term bad-term-6 {
  comment:: "empty source address after flattening"
  source-address:: CORP_EXTERNAL
  source-exclude:: CORP_EXTERNAL
  protocol:: tcp
  action:: accept
}
"""

BAD_TERM_EMPTY_DEST = """
term bad-term-7 {
  comment:: "empty destination address after flattening"
  destination-address:: CORP_EXTERNAL
  destination-exclude:: CORP_EXTERNAL
  protocol:: tcp
  action:: accept
}
"""

VALID_TERM_NAMES = [
    'gcp-to-gcp',
    'default-deny',
    'google-web',
    'zo6hmxkfibardh6tgbiy7ua6',
    'http.frontend.web.com',
]

INVALID_TERM_NAMES = [
    'CAPS-ARE-NOT-VALID',
    '_underscores_',
    'mIxEdCaSe',
    'an-otherwise-valid-term-ending-in-a-dash-',
]

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'destination_tag',
    'expiration',
    'stateless_reply',
    'name',
    'owner',
    'protocol',
    'source_address',
    'source_address_exclude',
    'translated',
    'platform',
    'platform_exclude',
}

SUPPORTED_SUB_TOKENS = {'action': {'accept', 'deny'}}

SUPPORTED_PROTOS = ['tcp', 'udp', 'sctp']

UNSUPPORTED_PROTOS = ['igmp', 'pim', 'ah']

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2

TEST_IPS = [nacaddr.IP('10.2.3.4/32'), nacaddr.IP('2001:4860:8000::5/128')]

TEST_INCLUDE_IPS = [nacaddr.IP('10.2.3.4/32'), nacaddr.IP('10.4.3.2/32')]

TEST_EXCLUDE_IPS = [nacaddr.IP('10.4.3.2/32')]

TEST_INCLUDE_RANGE = [nacaddr.IP('10.128.0.0/9')]

TEST_EXCLUDE_RANGE = [nacaddr.IP('10.240.0.0/16')]

ANY_IPS = [nacaddr.IP('0.0.0.0/0'), nacaddr.IP('::/0')]

ANY_IPV4 = [nacaddr.IP('0.0.0.0/0')]

ANY_IPV6 = [nacaddr.IP('::/0')]

TEST_IPV4_ONLY = [nacaddr.IP('10.2.3.4/32')]

TEST_IPV6_ONLY = [nacaddr.IP('2001:4860:8000::5/128')]


class K8sTest(parameterized.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testGenericTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    expected = {
        'apiVersion':
            k8s.K8s._API_VERSION,
        'kind':
            k8s.K8s._RESOURCE_KIND,
        'items': [{
            'apiVersion': k8s.Term._API_VERSION,
            'kind': k8s.Term._RESOURCE_KIND,
            'metadata': {
                'name': 'good-term-1',
                'annotations': {
                    'owner': 'myself',
                    'comment': 'DNS access from corp.'
                },
            },
            'spec': {
                'podSelector': {},
                'policyTypes': ['Ingress'],
                'ingress': [{
                    'from': [{
                        'ipBlock': {
                            'cidr': '10.2.3.4/32'
                        }
                    }, {
                        'ipBlock': {
                            'cidr': '2001:4860:8000::5/128'
                        }
                    }],
                    'ports': [{
                        'port': 53,
                        'protocol': 'UDP'
                    }, {
                        'port': 53,
                        'protocol': 'TCP'
                    }],
                }]
            },
        }]
    }

    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)

    policy_list = yaml.safe_load(str(acl))
    self.assertDictEqual(expected, policy_list)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testGenericEgressTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    expected = {
        'apiVersion':
            k8s.K8s._API_VERSION,
        'kind':
            k8s.K8s._RESOURCE_KIND,
        'items': [{
            'apiVersion': k8s.Term._API_VERSION,
            'kind': k8s.Term._RESOURCE_KIND,
            'metadata': {
                'name': 'good-term-4-e',
                'annotations': {
                    'comment': 'DNS access from corp.'
                },
            },
            'spec': {
                'podSelector': {},
                'policyTypes': ['Egress'],
                'egress': [{
                    'to': [{
                        'ipBlock': {
                            'cidr': '10.2.3.4/32'
                        }
                    }, {
                        'ipBlock': {
                            'cidr': '2001:4860:8000::5/128'
                        }
                    }],
                    'ports': [{
                        'port': 53,
                        'protocol': 'UDP'
                    }, {
                        'port': 53,
                        'protocol': 'TCP'
                    }],
                }]
            },
        }]
    }

    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM_EGRESS, self.naming),
        EXP_INFO)

    policy_list = yaml.safe_load(str(acl))
    self.assertDictEqual(expected, policy_list)

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testAllProtosTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53'], ['53']]

    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_PROTO_ALL, self.naming),
        EXP_INFO)

    policy_list = yaml.safe_load(str(acl))
    policies = policy_list['items']
    self.assertLen(policies, 1)
    net_policy = policies[0]
    self.assertLen(net_policy['spec']['ingress'], 1)
    ingress_rule = net_policy['spec']['ingress'][0]
    self.assertLen(ingress_rule['ports'], 3)
    unique_protos = {
        port_selector['protocol'] for port_selector in ingress_rule['ports']
    }
    self.assertSetEqual({'UDP', 'TCP', 'SCTP'}, unique_protos)

  def testPortRangeTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['0-1024']

    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming), EXP_INFO)
    policy_list = yaml.safe_load(str(acl))
    policies = policy_list['items']

    self.assertLen(policies, 1)
    net_policy = policies[0]
    self.assertLen(net_policy['spec']['ingress'], 1)
    ingress_rule = net_policy['spec']['ingress'][0]
    self.assertLen(ingress_rule['ports'], 2)
    expected = {'endPort': 1024, 'port': 0}
    for port_selector in ingress_rule['ports']:
      self.assertEqual(port_selector, {**port_selector, **expected})

  def testAllowAllTcpTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['53']
    expected_ingress_ports = [{'protocol': 'TCP'}]

    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_ALLOW_ALL_TCP, self.naming),
        EXP_INFO)

    policy_list = yaml.safe_load(str(acl))
    policies = policy_list['items']
    self.assertLen(policies, 1)
    net_policy = policies[0]
    self.assertLen(net_policy['spec']['ingress'], 1)
    ingress_rule = net_policy['spec']['ingress'][0]
    self.assertLen(ingress_rule['ports'], 1)
    self.assertSequenceEqual(ingress_rule['ports'], expected_ingress_ports)

  def testDefaultDenyTerm(self):
    expected = {
        'apiVersion': k8s.Term._API_VERSION,
        'kind': k8s.Term._RESOURCE_KIND,
        'metadata': {
            'name': 'default-deny',
            'annotations': {
                'comment': 'default_deny.'
            },
        },
        'spec': {
            'podSelector': {},
            'policyTypes': ['Ingress']
        },
    }

    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER + DEFAULT_DENY, self.naming), EXP_INFO)

    policy_list = yaml.safe_load(str(acl))
    policies = policy_list['items']
    self.assertDictEqual(expected, policies[0])

  def testDefaultDenyEgressTerm(self):
    expected = {
        'apiVersion': k8s.Term._API_VERSION,
        'kind': k8s.Term._RESOURCE_KIND,
        'metadata': {
            'name': 'default-deny-e',
            'annotations': {
                'comment': 'default_deny.'
            },
        },
        'spec': {
            'podSelector': {},
            'policyTypes': ['Egress']
        },
    }

    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + DEFAULT_DENY, self.naming),
        EXP_INFO)

    policy_list = yaml.safe_load(str(acl))
    policies = policy_list['items']
    self.assertLen(policies, 1)
    self.assertDictEqual(expected, policies[0])

  def testBadDenyTerm(self):
    self.assertRaisesRegex(
        k8s.K8sNetworkPolicyError, 'not support explicit deny', k8s.K8s,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_DENY, self.naming), EXP_INFO)

  def testBadSourceExclusionTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        k8s.K8sNetworkPolicyError, 'missing required field', k8s.K8s,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_INVALID_SOURCE_EXCLUDE,
                           self.naming), EXP_INFO)

  def testBadIngressNoAddressTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        k8s.K8sNetworkPolicyError, 'missing required field.+source', k8s.K8s,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_NO_ADDR, self.naming),
        EXP_INFO)

  def testBadEgressNoAddressTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.assertRaisesRegex(
        k8s.K8sNetworkPolicyError, 'missing required field.+destination',
        k8s.K8s,
        policy.ParsePolicy(GOOD_HEADER_EGRESS + BAD_TERM_NO_ADDR,
                           self.naming), EXP_INFO)

  @parameterized.named_parameters(
      {
          'testcase_name': 'IPv4',
          'ip_block_cidr': TEST_INCLUDE_RANGE,
          'ip_block_exclude': TEST_EXCLUDE_RANGE,
      }, {
          'testcase_name': 'IPv6',
          'ip_block_cidr': ANY_IPV6,
          'ip_block_exclude': TEST_IPV6_ONLY,
      }, {
          'testcase_name': 'MultiExclude',
          'ip_block_cidr': TEST_INCLUDE_IPS,
          'ip_block_exclude': TEST_EXCLUDE_RANGE + TEST_EXCLUDE_IPS,
      })
  def testGoodSourceAddressExcludeTerm(self, ip_block_cidr, ip_block_exclude):
    expected_peer_specs = []
    expected_peer_spec_except = [str(ex) for ex in ip_block_exclude[::-1]]
    for ip in ip_block_cidr:
      expected_peer_specs.append(
          {'ipBlock': {
              'cidr': str(ip),
              'except': expected_peer_spec_except
          }})

    expected = {
        'apiVersion': k8s.Term._API_VERSION,
        'kind': k8s.Term._RESOURCE_KIND,
        'metadata': {
            'name': 'good-term-exclude-source',
            'annotations': {
                'comment': 'term with source exclusions'
            },
        },
        'spec': {
            'ingress': [{
                'from': expected_peer_specs,
                'ports': [{
                    'protocol': 'TCP'
                }],
            }],
            'podSelector': {},
            'policyTypes': ['Ingress'],
        },
    }
    self.naming.GetNetAddr.side_effect = [ip_block_cidr, ip_block_exclude]
    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_EXCLUDE_SOURCE, self.naming),
        EXP_INFO)

    policy_list = yaml.safe_load(str(acl))
    policies = policy_list['items']
    self.assertDictEqual(expected, policies[0])

  @parameterized.named_parameters(
      {
          'testcase_name': 'IPv4',
          'ip_block_cidr': TEST_INCLUDE_RANGE,
          'ip_block_exclude': TEST_EXCLUDE_RANGE,
      }, {
          'testcase_name': 'IPv6',
          'ip_block_cidr': ANY_IPV6,
          'ip_block_exclude': TEST_IPV6_ONLY,
      }, {
          'testcase_name': 'MultiExclude',
          'ip_block_cidr': TEST_INCLUDE_IPS,
          'ip_block_exclude': TEST_EXCLUDE_RANGE + TEST_EXCLUDE_IPS,
      })
  def testGoodDestAddressExcludeTerm(self, ip_block_cidr, ip_block_exclude):
    expected_peer_specs = []
    expected_peer_spec_except = [str(ex) for ex in ip_block_exclude[::-1]]
    for ip in ip_block_cidr:
      expected_peer_specs.append(
          {'ipBlock': {
              'cidr': str(ip),
              'except': expected_peer_spec_except
          }})

    expected = {
        'apiVersion': k8s.Term._API_VERSION,
        'kind': k8s.Term._RESOURCE_KIND,
        'metadata': {
            'name': 'good-term-exclude-destination-e',
            'annotations': {
                'comment': 'term with destination exclusions'
            },
        },
        'spec': {
            'egress': [{
                'to': expected_peer_specs,
                'ports': [{
                    'protocol': 'TCP'
                }],
            }],
            'podSelector': {},
            'policyTypes': ['Egress'],
        },
    }
    self.naming.GetNetAddr.side_effect = [ip_block_cidr, ip_block_exclude]
    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM_EXCLUDE_DEST,
                           self.naming), EXP_INFO)

    policy_list = yaml.safe_load(str(acl))
    policies = policy_list['items']
    self.assertDictEqual(expected, policies[0])

  def testBadSourceAddressExcludeTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY
    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_EMPTY_SOURCE, self.naming),
        EXP_INFO)

    self.assertEqual(str(acl), '')

  def testBadDestinationAddressExcludeTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPV4_ONLY

    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER_EGRESS + BAD_TERM_EMPTY_DEST,
                           self.naming), EXP_INFO)

    self.assertEqual(str(acl), '')

  def testBadSourcePortTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53']]

    self.assertRaisesRegex(
        k8s.K8sNetworkPolicyError, 'not support source port', k8s.K8s,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_SOURCE_PORT, self.naming),
        EXP_INFO)

  def testBadIngressDestinationTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53']]

    self.assertRaisesRegex(
        k8s.K8sNetworkPolicyError,
        '[Ii]ngress rules cannot include.+destination', k8s.K8s,
        policy.ParsePolicy(GOOD_HEADER + BAD_TERM_INGRESS_DESTINATION,
                           self.naming), EXP_INFO)

  def testBadEgressSourceTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    self.assertRaisesRegex(
        k8s.K8sNetworkPolicyError, '[Ee]gress rules cannot include.+source',
        k8s.K8s, policy.ParsePolicy(GOOD_HEADER_EGRESS + GOOD_TERM,
                                    self.naming), EXP_INFO)

  def testValidTermNames(self):
    for name in VALID_TERM_NAMES:
      self.naming.GetNetAddr.return_value = TEST_IPS
      self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
      pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_CUSTOM_NAME % name,
                               self.naming)
      acl = k8s.K8s(pol, EXP_INFO)
      self.assertIsNotNone(str(acl))

  def testInvalidTermNames(self):
    for name in INVALID_TERM_NAMES:
      self.naming.GetNetAddr.return_value = TEST_IPS
      self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
      pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_CUSTOM_NAME % name,
                               self.naming)
      self.assertRaisesRegex(k8s.K8sNetworkPolicyError,
                             'name %s is not valid' % name, k8s.K8s, pol,
                             EXP_INFO)

  def testSkipExpiredTerm(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_EXPIRED, self.naming),
        EXP_INFO)
    self.assertEqual(str(acl), '')

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testSkipStatelessReply(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    # Add stateless_reply to terms, there is no current way to include it in the
    # term definition.
    ret = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM, self.naming)
    _, terms = ret.filters[0]
    for term in terms:
      term.stateless_reply = True

    acl = k8s.K8s(ret, EXP_INFO)
    self.assertEqual(str(acl), '')

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'),
         mock.call('DNS', 'tcp')])

  def testValidTermProtos(self):
    for proto in SUPPORTED_PROTOS:
      self.naming.GetNetAddr.return_value = TEST_IPS
      self.naming.GetServiceByProto.return_value = ['53']
      pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_CUSTOM_PROTO % proto,
                               self.naming)
      acl = k8s.K8s(pol, EXP_INFO)
      self.assertIsNotNone(str(acl))

  def testInvalidTermProtos(self):
    for proto in UNSUPPORTED_PROTOS:
      self.naming.GetNetAddr.return_value = TEST_IPS
      self.naming.GetServiceByProto.return_value = ['53']
      pol = policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_CUSTOM_PROTO % proto,
                               self.naming)
      self.assertRaises(aclgenerator.UnsupportedFilterError, k8s.K8s, pol,
                        EXP_INFO)

  def testMultipleTerms(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['53']

    acl = k8s.K8s(
        policy.ParsePolicy(GOOD_HEADER + GOOD_TERM + GOOD_TERM_ALLOW_ALL_TCP,
                           self.naming), EXP_INFO)

    policy_list = yaml.safe_load(str(acl))
    self.assertLen(policy_list['items'], 2)


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


# tests/lib/nacaddr_test.py

# Copyright 2007 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for nacaddr.py module."""

from absl.testing import absltest

from capirca.lib import nacaddr


class NacaddrUnitTest(absltest.TestCase):
  """Unit Test for nacaddr.py.

     nacaddr class extends ipaddr by adding .text fields to allow
     comments for each of the IPv4 and IPv6 classes.
  """

  def setUp(self):
    super().setUp()
    self.addr1 = nacaddr.IPv4(u'10.0.0.0/8', 'The 10 block')
    self.addr2 = nacaddr.IPv6('DEAD:BEEF:BABE:FACE:DEAF:FEED:C0DE:F001/64',
                              'An IPv6 Address', strict=False)

  def testCollapsing(self):
    ip1 = nacaddr.IPv4(u'1.1.0.0/24', 'foo')
    ip2 = nacaddr.IPv4(u'1.1.1.0/24', 'foo')
    ip3 = nacaddr.IPv4(u'1.1.2.0/24', 'baz')
    ip4 = nacaddr.IPv4(u'1.1.3.0/24')
    ip5 = nacaddr.IPv4(u'1.1.4.0/24')

    # stored in no particular order b/c we want CollapseAddr to call [].sort
    # and we want that sort to call nacaddr.IP.__cmp__() on our array members
    ip6 = nacaddr.IPv4(u'1.1.0.0/22')

    # check that addreses are subsumed properlly.
    collapsed = nacaddr.CollapseAddrList([ip1, ip2, ip3, ip4, ip5, ip6])
    self.assertEqual(len(collapsed), 2)
    # test that the comments are collapsed properlly, and that comments aren't
    # added to addresses that have no comments.
    self.assertListEqual([collapsed[0].text, collapsed[1].text],
                         ['foo, baz', ''])
    self.assertListEqual(collapsed, [nacaddr.IPv4(u'1.1.0.0/22'),
                                     nacaddr.IPv4(u'1.1.4.0/24')])

    # test that two addresses are supernet'ed properlly
    collapsed = nacaddr.CollapseAddrList([ip1, ip2])
    self.assertEqual(len(collapsed), 1)
    self.assertEqual(collapsed[0].text, 'foo')
    self.assertListEqual(collapsed, [nacaddr.IPv4(u'1.1.0.0/23')])

    ip_same1 = ip_same2 = nacaddr.IPv4(u'1.1.1.1/32')
    self.assertListEqual(nacaddr.CollapseAddrList([ip_same1, ip_same2]),
                         [ip_same1])
    ip1 = nacaddr.IPv6(u'::2001:1/100', strict=False)
    ip2 = nacaddr.IPv6(u'::2002:1/120', strict=False)
    ip3 = nacaddr.IPv6(u'::2001:1/96', strict=False)
    # test that ipv6 addresses are subsumed properlly.
    collapsed = nacaddr.CollapseAddrList([ip1, ip2, ip3])
    self.assertListEqual(collapsed, [ip3])

  def testNacaddrV4Comment(self):
    self.assertEqual(self.addr1.text, 'The 10 block')

  def testNacaddrV6Comment(self):
    self.assertEqual(self.addr2.text, 'An IPv6 Address')

  def testSupernetting(self):
    self.assertEqual(self.addr1.Supernet().text, 'The 10 block')
    self.assertEqual(self.addr2.Supernet().text, 'An IPv6 Address')
    self.assertEqual(self.addr1.Supernet().prefixlen, 7)
    self.assertEqual(self.addr2.Supernet().prefixlen, 63)

    token_ip = nacaddr.IP('1.1.1.0/24', token='FOO_TOKEN')
    self.assertEqual(token_ip.Supernet().token, 'FOO_TOKEN')
    self.assertEqual(nacaddr.IP('0.0.0.0/0').Supernet(),
                     nacaddr.IP('0.0.0.0/0'))
    self.assertEqual(nacaddr.IP('::0/0').Supernet(), nacaddr.IP('::0/0'))

    self.assertRaises(nacaddr.PrefixlenDiffInvalidError,
                      nacaddr.IP('1.1.1.0/24').Supernet, 25)
    self.assertRaises(nacaddr.PrefixlenDiffInvalidError,
                      nacaddr.IP('::1/64', strict=False).Supernet, 65)

  def testAddressListExclusion(self):
    a1 = nacaddr.IPv4('1.1.1.0/24')
    a2 = nacaddr.IPv4('10.0.0.0/24')
    b1 = nacaddr.IPv4('1.1.1.1/32')
    b2 = nacaddr.IPv4('10.0.0.25/32')
    b3 = nacaddr.IPv4('192.168.0.0/16')

    expected = [nacaddr.IPv4('1.1.1.0/32'), nacaddr.IPv4('1.1.1.2/31'),
                nacaddr.IPv4('1.1.1.4/30'), nacaddr.IPv4('1.1.1.8/29'),
                nacaddr.IPv4('1.1.1.16/28'), nacaddr.IPv4('1.1.1.32/27'),
                nacaddr.IPv4('1.1.1.64/26'), nacaddr.IPv4('1.1.1.128/25'),
                nacaddr.IPv4('10.0.0.0/28'), nacaddr.IPv4('10.0.0.16/29'),
                nacaddr.IPv4('10.0.0.24/32'), nacaddr.IPv4('10.0.0.26/31'),
                nacaddr.IPv4('10.0.0.28/30'), nacaddr.IPv4('10.0.0.32/27'),
                nacaddr.IPv4('10.0.0.64/26'), nacaddr.IPv4('10.0.0.128/25')]
    self.assertListEqual(nacaddr.AddressListExclude([a1, a2], [b1, b2, b3]),
                         expected)
    # [1,2,3] + [4,5,6] = [1,2,3,4,5,6].  this is basically the same test as
    # above but i think it's a little more readable
    expected_two = list(a1.address_exclude(b1))
    expected_two.extend(a2.address_exclude(b2))
    self.assertListEqual(nacaddr.AddressListExclude([a1, a2], [b1, b2, b3]),
                         sorted(expected_two))

  def testComplexAddressListExcludesion(self):
    # this is a big fugly test. there was a bug in AddressListExclude
    # which manifested itself when more than one member of the excludes
    # list was a part of the same superset token.
    #
    # for example, it used to be like so:
    #  excludes = ['1.1.1.1/32', '1.1.1.2/32']
    #  superset = ['1.1.1.0/30']
    #
    # '1.1.1.0/30'.AddressExclude('1.1.1.1/32') ->
    #    ['1.1.1.0/32', '1.1.1.2/32', '1.1.1.3/32']
    # '1.1.1.0/30'.AddressExclude('1.1.1.2/32') ->
    #    ['1.1.1.0/32', '1.1.1.1/32', '1.1.1.3/32']
    #
    # yet combining those two results gives you
    #   ['1.1.1.0/32', '1.1.1.1/32', '1.1.1.2/32' '1.1.1.3/32'], or
    #   '1.1.1.0/30', which clearly isn't right.

    excludes = [nacaddr.IPv4('10.0.0.0/23'), nacaddr.IPv4('10.1.0.0/16')]
    superset = [nacaddr.IPv4('0.0.0.0/0')]

    expected = [nacaddr.IPv4('0.0.0.0/5'), nacaddr.IPv4('8.0.0.0/7'),
                nacaddr.IPv4('10.0.2.0/23'), nacaddr.IPv4('10.0.4.0/22'),
                nacaddr.IPv4('10.0.8.0/21'), nacaddr.IPv4('10.0.16.0/20'),
                nacaddr.IPv4('10.0.32.0/19'), nacaddr.IPv4('10.0.64.0/18'),
                nacaddr.IPv4('10.0.128.0/17'), nacaddr.IPv4('10.2.0.0/15'),
                nacaddr.IPv4('10.4.0.0/14'), nacaddr.IPv4('10.8.0.0/13'),
                nacaddr.IPv4('10.16.0.0/12'), nacaddr.IPv4('10.32.0.0/11'),
                nacaddr.IPv4('10.64.0.0/10'), nacaddr.IPv4('10.128.0.0/9'),
                nacaddr.IPv4('11.0.0.0/8'), nacaddr.IPv4('12.0.0.0/6'),
                nacaddr.IPv4('16.0.0.0/4'), nacaddr.IPv4('32.0.0.0/3'),
                nacaddr.IPv4('64.0.0.0/2'), nacaddr.IPv4('128.0.0.0/1')]

    self.assertListEqual(nacaddr.AddressListExclude(superset, excludes),
                         expected)

  def testAddressListExcludeCaseOne(self):
    # Small block eliminated by large block, and an extra block that stays.
    # For both IP versions.
    superset = [nacaddr.IPv4('200.0.0.0/24'), nacaddr.IPv4('10.1.0.0/24'),
                nacaddr.IPv6('200::/56'), nacaddr.IPv6('10:1::/56')]
    excludes = [nacaddr.IPv6('10::/16'), nacaddr.IPv4('10.0.0.0/8')]
    expected = [nacaddr.IPv4('200.0.0.0/24'), nacaddr.IPv6('200::/56')]
    self.assertListEqual(nacaddr.AddressListExclude(superset, excludes),
                         expected)

  def testAddressListExcludeCaseTwo(self):
    # Two blocks out of the middle of a large block.
    superset = [nacaddr.IPv4('200.0.0.0/24'), nacaddr.IPv4('10.0.0.0/8'),
                nacaddr.IPv6('200::/56'), nacaddr.IPv6('10::/16')]
    excludes = [nacaddr.IPv6('10:8000::/18'), nacaddr.IPv6('10:4000::/18'),
                nacaddr.IPv4('10.128.0.0/10'), nacaddr.IPv4('10.64.0.0/10')]
    expected = [nacaddr.IPv4('10.0.0.0/10'), nacaddr.IPv4('10.192.0.0/10'),
                nacaddr.IPv4('200.0.0.0/24'),
                nacaddr.IPv6('10::/18'), nacaddr.IPv6('10:c000::/18'),
                nacaddr.IPv6('200::/56')]
    self.assertListEqual(nacaddr.AddressListExclude(superset, excludes),
                         expected)

  def testAddressListExcludeCaseThree(self):
    # Two blocks off both ends of a large block.
    superset = [nacaddr.IPv4('200.0.0.0/24'), nacaddr.IPv4('10.0.0.0/8'),
                nacaddr.IPv6('200::/56'), nacaddr.IPv6('10::/16')]
    excludes = [nacaddr.IPv6('10::/18'), nacaddr.IPv6('10:c000::/18'),
                nacaddr.IPv4('10.0.0.0/10'), nacaddr.IPv4('10.192.0.0/10')]
    expected = [nacaddr.IPv4('10.64.0.0/10'), nacaddr.IPv4('10.128.0.0/10'),
                nacaddr.IPv4('200.0.0.0/24'),
                nacaddr.IPv6('10:4000::/18'), nacaddr.IPv6('10:8000::/18'),
                nacaddr.IPv6('200::/56')]
    self.assertListEqual(nacaddr.AddressListExclude(superset, excludes),
                         expected)

  def testAddressListExcludeCaseFour(self):
    # IPv6 does not affect IPv4
    superset = [nacaddr.IPv4('0.0.0.0/0')]
    excludes = [nacaddr.IPv6('::/0')]
    expected = [nacaddr.IPv4('0.0.0.0/0')]
    self.assertListEqual(nacaddr.AddressListExclude(superset, excludes),
                         expected)

  def testAddressListExcludeCaseFive(self):
    # IPv6 does not affect IPv4
    superset = [nacaddr.IPv6('::/0')]
    excludes = [nacaddr.IPv4('0.0.0.0/0')]
    expected = [nacaddr.IPv6('::/0')]
    self.assertListEqual(nacaddr.AddressListExclude(superset, excludes),
                         expected)

  def testAddressListExcludeCaseSix(self):
    # IPv6 does not affect IPv4
    superset = [nacaddr.IPv6('0::ffff:0.0.0.0/96')]
    excludes = [nacaddr.IPv4('0.0.0.0/0')]
    expected = [nacaddr.IPv6('0::ffff:0.0.0.0/96')]
    self.assertListEqual(nacaddr.AddressListExclude(superset, excludes),
                         expected)

  def testCollapseAddrListPreserveTokens(self):
    addr_list = [nacaddr.IPv4('10.0.1.7/32', token='BIZ'),
                 nacaddr.IPv4('192.168.1.10/32', token='ALSOUNDERSUPER'),
                 nacaddr.IPv4('10.0.0.6/32', token='FOO'),
                 nacaddr.IPv4('10.0.0.9/32', token='BAR'),
                 nacaddr.IPv4('10.0.0.8/32', token='FOO'),
                 nacaddr.IPv4('10.0.0.7/32', token='BAR'),
                 nacaddr.IPv4('192.168.1.1/24', token='SUPER', strict=False),
                 nacaddr.IPv4('10.0.1.6/32', token='BIZ'),
                 nacaddr.IPv4('192.168.1.7/31',
                              token='UNDERSUPER',
                              strict=False)
                 ]
    expected = [nacaddr.IPv4('10.0.0.7/32', token='BAR'),
                nacaddr.IPv4('10.0.0.9/32', token='BAR'),
                nacaddr.IPv4('10.0.1.6/31', token='BIZ'),
                nacaddr.IPv4('10.0.0.6/32', token='FOO'),
                nacaddr.IPv4('10.0.0.8/32', token='FOO'),
                nacaddr.IPv4('192.168.1.1/24', token='SUPER', strict=False)]
    collapsed = nacaddr.CollapseAddrListPreserveTokens(addr_list)
    self.assertListEqual(collapsed, expected)

  def testIsSupernet(self):
    addrs1 = [nacaddr.IPv4('10.0.1.7/32'), nacaddr.IPv4('10.0.1.2/32')]
    addrs2 = [nacaddr.IPv4('10.0.1.0/24')]
    addrs3 = [nacaddr.IPv4('10.0.1.7/32'), nacaddr.IPv4('10.1.1.2/32')]
    addrs4 = [nacaddr.IPv4('192.168.1.1/32', nacaddr.IPv4('172.0.0.1/32'))]
    addrs5 = [nacaddr.IPv4('192.168.1.1/24', strict=False),
              nacaddr.IPv4('10.0.1.0/24')]
    self.assertTrue(nacaddr.IsSuperNet(addrs2, addrs1))
    self.assertFalse(nacaddr.IsSuperNet(addrs2, addrs3))
    self.assertFalse(nacaddr.IsSuperNet(addrs2, addrs4))
    self.assertFalse(nacaddr.IsSuperNet(addrs2, addrs5))
    self.assertTrue(nacaddr.IsSuperNet(addrs5, addrs2))

  def testSafeCollapsing(self):
    test_data = [([nacaddr.IPv4('10.0.0.0/8'),
                   nacaddr.IPv4('10.0.0.0/10')],
                  [nacaddr.IPv4('10.0.0.0/9')],
                  [nacaddr.IPv4('10.0.0.0/8'),
                   nacaddr.IPv4('10.0.0.0/10')]),

                 ([nacaddr.IPv4('192.168.0.0/27'),
                   nacaddr.IPv4('192.168.0.0/24')],
                  [nacaddr.IPv4('192.168.1.0/24')],
                  [nacaddr.IPv4('192.168.0.0/24')]),

                 ([nacaddr.IPv6('10::/56'),
                   nacaddr.IPv6('10::/128')],
                  [nacaddr.IPv6('10::/64')],
                  [nacaddr.IPv6('10::/56'),
                   nacaddr.IPv6('10::/128')]),

                 ([nacaddr.IPv6('10::/128'),
                   nacaddr.IPv6('10::/56')],
                  [nacaddr.IPv6('8::/64')],
                  [nacaddr.IPv6('10::/56')])
                 ]
    for addresses, complement_addresses, result in test_data:
      self.assertEqual(nacaddr.CollapseAddrList(addresses,
                                                complement_addresses), result)


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


# tests/lib/naming_test.py

# Copyright 2007 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for naming.py module."""

from absl.testing import absltest

from capirca.lib import nacaddr
from capirca.lib import naming


class NamingUnitTest(absltest.TestCase):
  """Unit Test for naming.py.

     The Naming class allows us to specify if we want to use arrays of text
     instead of files.  Most of the tests below create an empty Naming class.
     To populate the class with data, we simply pass our test data in arrays
     to the ParseList method, or in some cases, pass an io.BytesIO stream.
  """

  def setUp(self):
    super().setUp()
    self.defs = naming.Naming(None)
    servicedata = []
    servicedata.append('SVC1 = 80/tcp 81/udp 82/tcp')
    servicedata.append('SVC2 = 80/tcp 81/udp 82/tcp SVC2')
    servicedata.append('SVC3 = 80/tcp 81/udp')
    servicedata.append('SVC4 = 80/tcp # some service')
    servicedata.append('TCP_90 = 90/tcp')
    servicedata.append('SVC5 = TCP_90')
    servicedata.append('SVC6 = SVC1 SVC5')
    networkdata = []
    networkdata.append('NET1 = 10.1.0.0/8 # network1')
    networkdata.append('NET2 = 10.2.0.0/16 # network2.0')
    networkdata.append('       NET1')
    networkdata.append('9OCLOCK = 1.2.3.4/32 # 9 is the time')
    networkdata.append('FOOBAR = 9OCLOCK')
    networkdata.append('FOO_V6 = ::FFFF:FFFF:FFFF:FFFF')
    networkdata.append('BAR_V6 = ::1/128')
    networkdata.append('BAZ = FOO_V6')
    networkdata.append('      BAR_V6')
    networkdata.append('BING = NET1 # foo')
    networkdata.append('       FOO_V6')

    self.defs.ParseServiceList(servicedata)
    self.defs.ParseNetworkList(networkdata)

  def testCommentedServices(self):
    self.assertEqual(self.defs.GetService('SVC4'), ['80/tcp'])
    self.assertListEqual(self.defs.GetServiceByProto('SVC4', 'tcp'),
                         ['80'])

  def testBadGetRequest(self):
    """Test proper handling of a non-existant service request."""
    self.assertRaises(naming.UndefinedServiceError, self.defs.GetService, 'FOO')
    self.assertRaises(naming.UndefinedServiceError, self.defs.GetServiceByProto,
                      'FOO', 'tcp')

  def testGetServiceRecursion(self):
    """Ensure we don't slip into recursion hell when object contains itself."""
    self.assertListEqual(self.defs.GetService('SVC2'),
                         ['80/tcp', '81/udp', '82/tcp'])

  def testGetService(self):
    """Verify proper results from a service lookup request."""
    self.assertListEqual(self.defs.GetService('SVC1'),
                         ['80/tcp', '81/udp', '82/tcp'])

  def testBadProtocol(self):
    """Test proper handling of a non-existant service request."""
    self.assertListEqual(self.defs.GetServiceByProto('SVC1', 'fud'), [])

  def testGetServiceByProto(self):
    self.assertListEqual(self.defs.GetServiceByProto('SVC1', 'tcp'),
                         ['80', '82'])

  def testGetServiceByProtoWithoutProtocols(self):
    """Ensure services with protocol are not returned when type is specified."""
    self.assertListEqual(self.defs.GetServiceByProto('SVC3', 'tcp'), ['80'])

  def testNetworkComment(self):
    self.assertEqual(self.defs.GetNetAddr('NET1')[0].text, 'network1')

  def testNestedNetworkComment(self):
    self.assertEqual(self.defs.GetNetAddr('NET2')[1].text, 'network1')

  def testUndefinedAddress(self):
    self.assertRaises(naming.UndefinedAddressError, self.defs.GetNetAddr, 'FOO')

  def testNamespaceCollisionError(self):
    badservicedata = []
    badservicedata.append('SVC1 = 80/tcp')
    badservicedata.append('SVC1 = 81/udp')
    testdefs = naming.Naming(None)
    self.assertRaises(naming.NamespaceCollisionError,
                      testdefs.ParseServiceList, badservicedata)

  def testNetworkAddress(self):
    self.assertListEqual(self.defs.GetNetAddr('NET1'),
                         [nacaddr.IPv4('10.0.0.0/8')])

  def testInet6Address(self):
    self.assertListEqual(self.defs.GetNetAddr('BAZ'),
                         [nacaddr.IPv6('::FFFF:FFFF:FFFF:FFFF'),
                          nacaddr.IPv6('::1/128')])

  def testMixedAddresses(self):
    self.assertListEqual(self.defs.GetNetAddr('BING'),
                         [nacaddr.IPv4('10.0.0.0/8'),
                          nacaddr.IPv6('::FFFF:FFFF:FFFF:FFFF')])
    # same thing but letting nacaddr decide which v4 or v6.
    self.assertListEqual(self.defs.GetNetAddr('BING'),
                         [nacaddr.IP('10.0.0.0/8'),
                          nacaddr.IP('::FFFF:FFFF:FFFF:FFFF')])

  def testNestedServices(self):
    self.assertListEqual(self.defs.GetServiceByProto('SVC6', 'tcp'),
                         ['80', '82', '90'])

  def testServiceParents(self):
    """SVC6 contains SVC5 which contains TCP_90 which contains 90/tcp."""
    self.assertListEqual(self.defs.GetServiceParents('90/tcp'),
                         ['TCP_90', 'SVC5', 'SVC6'])

  def testNetParents(self):
    """BIN & NET2 contain NET1, BING & BAZ contain FOO_V6."""
    self.assertListEqual(sorted(self.defs.GetNetParents('NET1')),
                         ['BING', 'NET2'])
    self.assertListEqual(sorted(self.defs.GetNetParents('FOO_V6')),
                         ['BAZ', 'BING'])

  def testGetIpParents(self):
    """Ensure GetIpParents returns proper results."""
    self.assertListEqual(self.defs.GetIpParents('10.11.12.13/32'),
                         ['BING', 'NET1', 'NET2'])

  def testUndefinedTokenNesting(self):
    bad_servicedata = ['FOO = 7/tcp BAR']
    bad_networkdata = ['NETGROUP = 10.0.0.0/8 FOOBAR']
    baddefs = naming.Naming(None)
    baddefs.ParseServiceList(bad_servicedata)
    baddefs.ParseNetworkList(bad_networkdata)
    self.assertRaises(naming.UndefinedServiceError,
                      baddefs._CheckUnseen, 'services')
    self.assertRaises(naming.UndefinedAddressError,
                      baddefs._CheckUnseen, 'networks')

  def testParseNetFile(self):
    filedefs = naming.Naming(None)
    data = ['FOO = 127.0.0.1 # some network\n']
    filedefs._ParseFile(data, 'networks')
    self.assertEqual(filedefs.GetNetAddr('FOO'), [nacaddr.IPv4('127.0.0.1')])

  def testParseServiceFile(self):
    filedefs = naming.Naming(None)
    data = ['HTTP = 80/tcp\n']
    filedefs._ParseFile(data, 'services')
    self.assertEqual(filedefs.GetService('HTTP'), ['80/tcp'])

  def testServiceIncorrectSyntax(self):
    badservicedata = []
    badservicedata.append('SVC1 = 80//tcp 80/udp')
    badservicedata.append('SVC2 = 81/tcp')
    testdefs = naming.Naming(None)
    self.assertRaises(naming.NamingSyntaxError,
                      testdefs.ParseServiceList, badservicedata)

  def testGetNetChildrenSingle(self):
    expected = ['NET1']
    self.assertEqual(expected, self.defs.GetNetChildren('NET2'))

  def testGetNetChildrenMulti(self):
    expected = ['FOO_V6', 'BAR_V6']
    self.assertEqual(expected, self.defs.GetNetChildren('BAZ'))

  def testGetNetChildrenQueryNotExist(self):
    self.assertEqual([], self.defs.GetNetChildren('IDONOTEXIST'))

  def testGetNetChildrenNoChild(self):
    self.assertEqual([], self.defs.GetNetChildren('NET1'))


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


# tests/lib/nftables_test.py

# Copyright 2023 Google Inc. All Rights Reserved.
#
# 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.
"""Unittest for Nftables rendering module."""

import datetime
import re
from unittest import mock
from absl import logging
from absl.testing import absltest
from absl.testing import parameterized
from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import nftables
from capirca.lib import policy


class DictObj:
  """Helper class to use a dictionary of dictionaries to form an object.

  We can then specifically test using it.
  """

  def __init__(self, in_dict: dict):
    assert isinstance(in_dict, dict)
    for key, val in in_dict.items():
      if isinstance(val, (list, tuple)):
        setattr(
            self, key, [DictObj(x) if isinstance(x, dict) else x for x in val]
        )
      else:
        setattr(self, key, DictObj(val) if isinstance(val, dict) else val)


# "logging" is not a token.
SUPPORTED_TOKENS = frozenset({
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_type',
    'name',  # obj attribute, not token
    'option',
    'protocol',
    'platform',
    'platform_exclude',
    'source_interface',  # input interface
    'source_address',
    'source_address_exclude',
    'source_port',
    'destination_interface',  # ouput interface
    'translated',  # obj attribute, not token
    'stateless_reply',
})

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny'},
    'option': {'established', 'tcp-established'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}
# IP address data, to be loaded onto policy and test rendering.
TEST_IPV4_ONLY = [nacaddr.IP('10.2.3.4/32')]
TEST_IPV6_ONLY = [nacaddr.IP('2001:4860:8000::5/128')]
TEST_IPS = [nacaddr.IP('10.2.3.4/32'), nacaddr.IP('2001:4860:8000::5/128')]

HEADER_TEMPLATE = """
header {
  target:: nftables %s
}
"""

HEAD_OVERRIDE_DEFAULT_ACTION = """
header {
  target:: nftables inet output ACCEPT
}
"""

HEADER_COMMENT = """
header {
  comment:: "Noverbose + custom priority policy example"
  target:: nftables inet output ACCEPT
}
"""

HEADER_MIXED_AF = """
header {
  target:: nftables mixed output
}
"""

HEADER_IPV4_AF = """
header {
  target:: nftables inet output
}
"""

HEADER_IPV6_AF = """
header {
  target:: nftables inet6 output
}
"""

HEADER_NOVERBOSE = """
header {
  target:: nftables mixed output noverbose
}
"""

GOOD_HEADER_1 = """
header {
  target:: nftables inet6 INPUT
}
"""

GOOD_HEADER_2 = """
header {
  target:: nftables mixed output accept
}
"""

GOOD_HEADER_3 = """
header {
  target:: nftables inet input
}
"""

DENY_TERM = """
term deny-term {
  comment:: "Dual-stack IPv4/v6 deny all"
  action:: deny
}
"""

# Input interface name test term.
SOURCE_INTERFACE_TERM = """
term src-interface-term {
  source-interface:: eth123
  protocol:: tcp
  action:: accept
}
"""

# Output interface name test term.
DESTINATION_INTERFACE_TERM = """
term dst-interface-term {
  destination-interface:: eth123
  protocol:: tcp
  action:: accept
}
"""

BAD_INTERFACE_TERM = """
term dst-interface-term {
  source-interface:: eth123
  destination-interface:: eth123
  protocol:: tcp
  action:: accept
}
"""

ESTABLISHED_OPTION_TERM = """
term established-term {
  protocol:: udp
  option:: established
  action:: accept
}
"""

TCP_ESTABLISHED_OPTION_TERM = """
term tcp-established-term {
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""

ICMP_TERM = """
term good-icmp {
  protocol:: icmp
  action:: accept
}
"""

ICMP_SINGLE_TYPE = """
term good-icmp-single-type {
  comment:: "IPv4 ICMP accept single type"
  icmp-type:: router-solicit
  protocol:: icmp
  action:: accept
}
"""

ICMPV6_TERM = """
term good-icmpv6 {
  protocol:: icmpv6
  action:: accept
}
"""

ICMPV6_SINGLE_TYPE = """
term good-icmpv6-single-type {
  comment:: "IPv6 ICMP accept single type"
  icmp-type:: router-solicit
  protocol:: icmpv6
  action:: accept
}
"""

ICMPV6_MULTI_TERM = """
term good-icmpv6-type {
  comment:: "IPv6 ICMP accept many types"
  icmp-type:: router-solicit router-advertisement neighbor-advertisement neighbor-solicit
  protocol:: icmpv6
  action:: accept
}
"""

COMMENT_TERM = """
term good-icmpv6-type {
  comment:: "This term has a comment"
  protocol:: tcp
  action:: accept
}
"""

NOCOMMENT_TERM = """
term good-icmpv6-type {
  protocol:: tcp
  action:: accept
}
"""

LOGGING_TERM = """
term log-packets {
  logging:: true
  action:: accept
}
"""

COUNTER_TERM = """
term count-packets {
  counter:: thisnameisignored
  action:: accept
}
"""

COUNT_AND_LOG_TERM = """
term count-and-log-packets {
  logging:: true
  counter:: thisnameisignored
  action:: accept
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
   protocol:: tcp
   action:: accept
   destination-port:: SSH
   destination-address:: TEST_NET
}
"""

IPV6_ONLY_TERM = """
term ip6-only {
  destination-address:: TEST_IPV6_ONLY
  action:: accept
}
"""

IPV6_SRCIP = """
term ip6-src-addr {
  source-address:: TEST_IPV6_ONLY
  action:: deny
}
"""

IPV4_SRCIP = """
term ip4-src-addr {
  source-address:: TEST_IPV4_ONLY
  action:: deny
}
"""

ALL_SRCIP = """
term all-src-addr {
  comment:: "All IP address families. v4/v6"
  source-address:: TEST_IPS
  action:: deny
}
"""

EXCLUDE = {'ip6': [nacaddr.IP('::/3'), nacaddr.IP('::/0')]}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


def IPhelper(addresses):
  """Helper for string to nacaddr.IP conversion for parametized tests."""
  normalized = []
  if not addresses:
    # if empty list of addresses.
    return addresses
  else:
    for addr in addresses:
      normalized.append(nacaddr.IP(addr))
    return normalized


class NftablesTest(parameterized.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)
    self.dummyterm = nftables.Term('', '', '')

  @parameterized.parameters(('ip protocol tcp', ' ip protocol tcp'), ('', ''))
  def testAdd(self, statement, expected_output):
    result = nftables.Add(statement)
    self.assertEqual(result, expected_output)

  @parameterized.parameters((2, 'chain acl_name', '  chain acl_name'))
  def testTabSpacer(self, num_spaces, statement, expected_output):
    result = nftables.TabSpacer(num_spaces, statement)
    self.assertEqual(result, expected_output)

  @parameterized.parameters(
      (
          'ip',
          ['200.1.1.3/32', '9782:b30a:e5c6:1aa4:29ff:e57c:44a0:1b84'],
          ['200.1.1.3/32', '2606:4700:4700::1111'],
          ['ip saddr 200.1.1.3/32 ip daddr 200.1.1.3/32'],
      ),
      (
          'ip',
          ['200.1.1.3/32', '200.1.1.4/32'],
          ['200.1.1.3/32', '200.1.1.4/32'],
          [
              'ip saddr { 200.1.1.3/32, 200.1.1.4/32 } ip daddr { 200.1.1.3/32,'
              ' 200.1.1.4/32 }'
          ],
      ),
      (
          'ip6',
          ['8.8.8.8', '9782:b30a:e5c6:1aa4:29ff:e57c:44a0:1b84'],
          ['200.1.1.3/32', '2606:4700:4700::1111'],
          [
              'ip6 saddr 9782:b30a:e5c6:1aa4:29ff:e57c:44a0:1b84/128 ip6 daddr'
              ' 2606:4700:4700::1111/128'
          ],
      ),
      (
          'ip6',
          ['2606:4700:4700::1111', '2606:4700:4700::1112'],
          ['2606:4700:4700::1111', '2606:4700:4700::1112'],
          [
              'ip6 saddr { 2606:4700:4700::1111/128, 2606:4700:4700::1112/128 }'
              ' ip6 daddr { 2606:4700:4700::1111/128,'
              ' 2606:4700:4700::1112/128 }'
          ],
      ),
  )
  def test_AddrStatement(self, af, src_addr, dst_addr, expected):
    # Necessary object format.
    src_obj = IPhelper(src_addr)
    dst_obj = IPhelper(dst_addr)
    result = self.dummyterm._AddrStatement(af, src_obj, dst_obj)
    self.assertEqual(result, expected)

  @parameterized.parameters(
      (
          ['nd-router-advert', 'nd-neighbor-solicit', 'nd-neighbor-advert'],
          '{ nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert }',
      ),
      (['200.1.1.3/32'], '200.1.1.3/32'),
      (['1.1.1.1', '8.8.8.8'], '{ 1.1.1.1, 8.8.8.8 }'),
      (['tcp', 'udp', 'icmp'], '{ tcp, udp, icmp }'),
      (['80', '443'], '{ 80, 443 }'),
      ('53', '53'),
  )
  def testCreateAnonymousSet(self, input_data, expected):
    result = self.dummyterm.CreateAnonymousSet(input_data)
    self.assertEqual(result, expected)

  @parameterized.parameters(
      (
          '',
          [
              'ip6 saddr 2606:4700:4700::1111/128 ip6 daddr {'
              ' 2001:4860:4860::8844/128, 2001:4860:4860::8888/128 }'
          ],
          ['tcp sport 80 tcp dport 80'],
          (
              'ct state { ESTABLISHED, RELATED } log prefix'
              ' "combo_cnt_log_established" counter'
          ),
          'accept',
          '',
          [
              'ip6 saddr 2606:4700:4700::1111/128 ip6 daddr {'
              ' 2001:4860:4860::8844/128, 2001:4860:4860::8888/128 } tcp sport'
              ' 80 tcp dport 80 ct state { ESTABLISHED, RELATED } log prefix'
              ' "combo_cnt_log_established" counter accept'
          ],
      ),
      (
          '',
          ['ip daddr 8.8.8.8/32'],
          ['tcp sport 53 tcp dport 53'],
          'ct state new',
          'accept',
          'comment "this is a term with a comment"',
          [
              'ip daddr 8.8.8.8/32 tcp sport 53 tcp dport 53 ct state new'
              ' accept comment "this is a term with a comment"'
          ],
      ),
  )
  def testGroupExpressions(
      self,
      int_str,
      address_expr,
      porst_proto_expr,
      opt,
      verdict,
      comment,
      expected_output,
  ):
    result = self.dummyterm.GroupExpressions(
        int_str, address_expr, porst_proto_expr, opt, verdict, comment
    )
    self.assertEqual(result, expected_output)

  def testBadInterfaceTerm(self):
    pol = policy.ParsePolicy(
        GOOD_HEADER_1 + GOOD_TERM_1 + BAD_INTERFACE_TERM, self.naming
    )
    with self.assertRaises(nftables.TermError):
      nftables.Nftables.__init__(
          nftables.Nftables.__new__(nftables.Nftables), pol, EXP_INFO
      )

  def testDuplicateTerm(self):
    pol = policy.ParsePolicy(
        GOOD_HEADER_1 + GOOD_TERM_1 + GOOD_TERM_1, self.naming
    )
    with self.assertRaises(nftables.TermError):
      nftables.Nftables.__init__(
          nftables.Nftables.__new__(nftables.Nftables), pol, EXP_INFO
      )

  @parameterized.parameters(
      ([(80, 80)], '80'), ([(1024, 65535)], '1024-65535'), ([], '')
  )
  def testGroup(self, data, expected_output):
    """Test _Group function we use in Ports."""
    result = self.dummyterm._Group(data)
    self.assertEqual(result, expected_output)

  @parameterized.parameters(
      ('ip', ['tcp'], [], [], [], ['ip protocol tcp']),
      (
          'ip',
          ['tcp'],
          [(3198, 3199)],
          [(80, 80), (443, 443)],
          [],
          ['tcp sport 3198-3199 tcp dport { 80, 443 }'],
      ),
      (
          'ip',
          ['tcp'],
          [],
          [(80, 80), (443, 443)],
          [],
          ['tcp dport { 80, 443 }'],
      ),
      ('ip', ['tcp, udp'], [], [], [], ['ip protocol tcp, udp']),
      ('ip6', ['tcp'], [], [], [], ['meta l4proto tcp']),
      (
          'ip6',
          ['tcp'],
          [(3198, 3199)],
          [(80, 80), (443, 443)],
          [],
          ['tcp sport 3198-3199 tcp dport { 80, 443 }'],
      ),
      ('ip6', ['tcp', 'udp'], [], [], [], ['meta l4proto { tcp, udp }']),
  )
  def testPortsAndProtocols(self, af, proto, src_p, dst_p, icmp_type, expected):
    result = self.dummyterm.PortsAndProtocols(
        af, proto, src_p, dst_p, icmp_type
    )
    self.assertEqual(result, expected)

  @parameterized.parameters(
      'chain_name input 0 inet extraneous_target_option',
      'ip6 OUTPUT 300 400 mixed input', # pylint: disable=implicit-str-concat
      'ip forwarding',
      'ip7 0 spaghetti',
      'ip6 prerouting',
      'chain_name',
      '',
  )
  def testBadHeader(self, case):
    logging.info('Testing bad header case %s.', case)
    header = HEADER_TEMPLATE % case
    pol = policy.ParsePolicy(header + GOOD_TERM_1, self.naming)
    with self.assertRaises(nftables.HeaderError):
      nftables.Nftables.__init__(
          nftables.Nftables.__new__(nftables.Nftables), pol, EXP_INFO
      )

  @parameterized.parameters((HEADER_NOVERBOSE, False), (HEADER_COMMENT, True))
  def testVerboseHeader(self, header_to_use, expected_output):
    pol = policy.ParsePolicy(header_to_use + GOOD_TERM_1, self.naming)
    data = nftables.Nftables(pol, EXP_INFO)
    for _, _, _, _, _, _, verbose, _ in data.nftables_policies:
      result = verbose
    self.assertEqual(result, expected_output)

  def testGoodHeader(self):
    nftables.Nftables(
        policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1, self.naming), EXP_INFO
    )
    nft = str(
        nftables.Nftables(
            policy.ParsePolicy(
                GOOD_HEADER_1 + GOOD_TERM_1 + GOOD_HEADER_2 + IPV6_SRCIP,
                self.naming,
            ),
            EXP_INFO,
        )
    )
    self.assertIn('type filter hook input', nft)

  def testStatefulFirewall(self):
    nftables.Nftables(
        policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1, self.naming), EXP_INFO
    )
    nft = str(
        nftables.Nftables(
            policy.ParsePolicy(
                GOOD_HEADER_1 + GOOD_TERM_1 + GOOD_HEADER_2 + IPV6_SRCIP,
                self.naming,
            ),
            EXP_INFO,
        )
    )
    self.assertIn('ct state established,related accept', nft)

  def testICMPv6type(self):
    nftables.Nftables(
        policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1, self.naming), EXP_INFO
    )
    nft = str(
        nftables.Nftables(
            policy.ParsePolicy(GOOD_HEADER_1 + ICMPV6_MULTI_TERM, self.naming),
            EXP_INFO,
        )
    )
    self.assertIn(
        (
            'icmpv6 type { nd-router-solicit, nd-router-advert,'
            ' nd-neighbor-advert, nd-neighbor-solicit } accept'
        ),
        nft,
    )

  def testOverridePolicyHeader(self):
    expected_output = 'accept'

    pol = policy.ParsePolicy(
        HEAD_OVERRIDE_DEFAULT_ACTION + GOOD_TERM_1, self.naming
    )
    data = nftables.Nftables(pol, EXP_INFO)
    for _, _, _, _, _, default_policy, _, _ in data.nftables_policies:
      result = default_policy
    self.assertEqual(result, expected_output)

  @parameterized.parameters(
      (['127.0.0.1', '8.8.8.8'], {'ip': ['127.0.0.1/32', '8.8.8.8/32']}),
      (
          ['0.0.0.0/8', '2001:db8::/32'],
          {'ip': ['0.0.0.0/8'], 'ip6': ['2001:db8::/32']},
      ),
  )
  def testAddressClassifier(self, addr_to_classify, expected_output):
    result = nftables.Term._AddressClassifier(self, IPhelper(addr_to_classify))
    self.assertEqual(result, expected_output)

  @parameterized.parameters(
      ('ip6', ['multicast-listener-query'], ['mld-listener-query']),
      (
          'ip6',
          ['echo-request', 'multicast-listener-query'],
          ['echo-request', 'mld-listener-query'],
      ),
      (
          'ip6',
          ['router-solicit', 'multicast-listener-done', 'router-advertisement'],
          ['nd-router-solicit', 'mld-listener-done', 'nd-router-advert'],
      ),
      ('ip4', ['echo-request', 'echo-reply'], ['echo-request', 'echo-reply']),
  )
  def testMapICMPtypes(self, af, icmp_types, expected_output):
    result = self.dummyterm.MapICMPtypes(af, icmp_types)
    self.assertEqual(result, expected_output)

  @parameterized.parameters(
      (
          {
              'name': 'tcp_established',
              'option': ['tcp-established', 'established'],
              'icmp_type': None,
              'counter': None,
              'logging': [],
              'protocol': ['tcp', 'icmp'],
              'action': ['deny'],
          },
          '',
      ),
      (
          {
              'name': 'icmpv6_noconttrack',
              'option': [],
              'icmp_type': ['router-solicit'],
              'counter': None,
              'logging': [],
              'protocol': ['icmpv6'],
              'action': ['accept'],
          },
          '',
      ),
      (
          {
              'name': 'dont_render_tcp_established',
              'option': ['tcp-established', 'established'],
              'icmp_type': None,
              'counter': None,
              'logging': [],
              'protocol': ['icmp'],
              'action': ['accept'],
          },
          'ct state new',
      ),
      (
          {
              'name': 'blank_option_donothing',
              'option': [],
              'icmp_type': None,
              'counter': None,
              'logging': [],
              'protocol': ['icmp'],
              'action': ['accept'],
          },
          'ct state new',
      ),
      (
          {
              'name': 'syslog',
              'option': [],
              'icmp_type': None,
              'counter': None,
              'logging': ['syslog'],
              'protocol': ['tcp'],
              'action': ['accept'],
          },
          'ct state new log prefix "syslog"',
      ),
      (
          {
              'name': 'logging_disabled',
              'option': [],
              'icmp_type': None,
              'counter': None,
              'logging': ['disable'],
              'protocol': ['tcp'],
              'action': ['accept'],
          },
          'ct state new',
      ),
      (
          {
              'name': 'combo_logging_tcp_established',
              'option': ['tcp-established'],
              'icmp_type': None,
              'counter': None,
              'logging': ['true'],
              'protocol': ['tcp'],
              'action': ['accept'],
          },
          'ct state new log prefix "combo_logging_tcp_established"',
      ),
      (
          {
              'name': 'combo_cnt_log_established',
              'option': ['tcp-established'],
              'icmp_type': None,
              'counter': 'whatever-name-you-want',
              'logging': ['true'],
              'protocol': ['tcp'],
              'action': ['deny'],
          },
          'log prefix "combo_cnt_log_established" counter',
      ),
  )
  def testOptionsHandler(self, term_dict, expected_output):
    term = DictObj(term_dict)
    result = self.dummyterm._OptionsHandler(term)
    self.assertEqual(result, expected_output)

  def testBuildTokens(self):
    self.naming.GetServiceByProto.side_effect = [['25'], ['26']]
    pol1 = nftables.Nftables(
        policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1, self.naming), EXP_INFO
    )
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  @parameterized.parameters(
      (
          ESTABLISHED_OPTION_TERM,
          (
              'WARNING: Term established-term is a established term and will'
              ' not be rendered.'
          ),
      ),
      (
          TCP_ESTABLISHED_OPTION_TERM,
          (
              'WARNING: Term tcp-established-term is a tcp-established term and'
              ' will not be rendered.'
          ),
      ),
  )
  def testSkippedTerm(self, termdata, messagetxt):
    with self.assertLogs() as ctx:
      # run a policy object expected to be skipped and logged.
      nft = nftables.Nftables(
          policy.ParsePolicy(GOOD_HEADER_1 + termdata, self.naming), EXP_INFO
      )
    # self.assertEqual(len(ctx.records), 2)
    record = ctx.records[1]
    self.assertEqual(record.message, messagetxt)

  @parameterized.parameters(
      (HEADER_MIXED_AF + ICMPV6_TERM, 'ip protocol icmp'),
      (HEADER_IPV4_AF + ICMPV6_TERM, 'meta l4proto icmpv6'),
      (HEADER_IPV6_AF + ICMP_TERM, 'ip protocol icmp'),
  )
  def testRulesetGeneratorICMPmismatch(self, pol_data, doesnotcontain):
    # This test ensures that ICMPv6 only term isn't rendered in a mixed header.
    nftables.Nftables(policy.ParsePolicy(pol_data, self.naming), EXP_INFO)
    nft = str(
        nftables.Nftables(policy.ParsePolicy(pol_data, self.naming), EXP_INFO)
    )
    self.assertNotIn(doesnotcontain, nft)

  def testRulesetGeneratorUniqueChain(self):
    # This test is intended to verify that on mixed address family rulesets
    # no duplicate instance of a simple deny is rendered within a mixed chain.
    expected_term_rule = 'drop comment "Dual-stack IPv4/v6 deny all"'
    count = 0
    nftables.Nftables(
        policy.ParsePolicy(HEADER_MIXED_AF + DENY_TERM, self.naming), EXP_INFO
    )
    nft = str(
        nftables.Nftables(
            policy.ParsePolicy(HEADER_MIXED_AF + DENY_TERM, self.naming),
            EXP_INFO,
        )
    )
    matching_lines = re.findall(expected_term_rule, nft)
    for match in matching_lines:
      count += 1
    self.assertEqual(count, 1)

  @parameterized.parameters(
      (GOOD_HEADER_1 + GOOD_TERM_2, 'inet6'),
      (GOOD_HEADER_1 + ICMPV6_TERM, 'inet6'),
      (GOOD_HEADER_1 + COMMENT_TERM, 'mixed'),
      (GOOD_HEADER_2 + GOOD_TERM_2, 'mixed'),
      (GOOD_HEADER_3 + GOOD_TERM_2, 'inet'),
      (GOOD_HEADER_3 + ICMP_TERM, 'inet'),
  )
  def testRulesetGeneratorAF(self, policy_data: str, expected_inet: str):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.return_value = ['22']

    nft = nftables.Nftables(
        policy.ParsePolicy(policy_data, self.naming), EXP_INFO
    )
    for header, terms in nft.policy.filters:
      filter_options = header.FilterOptions('nftables')
      nf_af, nf_hook, _, _, verbose = nft._ProcessHeader(filter_options)
      for term in terms:
        term_object = nftables.Term(term, nf_af, nf_hook, verbose)

        # Checks for address family consistency within terms
        ruleset_list = term_object.RulesetGenerator(term)
        self.assertNotEmpty(ruleset_list)
        for ruleset in ruleset_list:
          if expected_inet == 'inet':
            self.assertNotIn(str(TEST_IPV6_ONLY), ruleset)
          elif expected_inet == 'inet6':
            self.assertNotIn(str(TEST_IPV4_ONLY), ruleset)

          for rule in ruleset.split('\n'):
            if rule.startswith('ip '):
              self.assertNotIn('meta l4proto', rule)
              self.assertNotIn('icmpv6', rule)
            if rule.startswith('ip6 '):
              self.assertNotIn('ip protocol', rule)
              self.assertNotIn('icmp', rule)

  @parameterized.parameters(
      (
          GOOD_HEADER_1 + SOURCE_INTERFACE_TERM,
          TEST_IPS,
          '    iifname eth123 meta l4proto',
      ),
      (
          GOOD_HEADER_1 + DESTINATION_INTERFACE_TERM,
          TEST_IPS,
          '    oifname eth123 meta l4proto',
      ),
      (GOOD_HEADER_1 + LOGGING_TERM, TEST_IPS, 'log prefix "log-packets"'),
      (GOOD_HEADER_1 + COUNTER_TERM, TEST_IPS, 'counter'),
      (
          GOOD_HEADER_1 + COUNT_AND_LOG_TERM,
          TEST_IPS,
          'log prefix "count-and-log-packets" counter',
      ),
      (
          HEADER_MIXED_AF + IPV6_ONLY_TERM,
          TEST_IPS,
          'ip6 daddr 2001:4860:8000::5/128 ct state new accept',
      ),
      (
          HEADER_MIXED_AF + ALL_SRCIP,
          TEST_IPS,
          'ip saddr 10.2.3.4/32 drop comment "All IP address families. v4/v6"',
      ),
      (GOOD_HEADER_3 + ICMP_SINGLE_TYPE, TEST_IPS, 'icmp type router-solicit'),
      (
          GOOD_HEADER_1 + ICMPV6_SINGLE_TYPE,
          TEST_IPS,
          'icmpv6 type nd-router-solicit',
      ),
  )
  def testRulesetGenerator(self, policy_data: str, IPs, contains: str):
    self.naming.GetNetAddr.return_value = IPs
    nft = str(
        nftables.Nftables(
            policy.ParsePolicy(policy_data, self.naming), EXP_INFO
        )
    )
    self.assertIn(contains, nft)


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


# tests/lib/nsxv_functtest.py

# Copyright 2015 The Capirca Project Authors All Rights Reserved.
#
# 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.
#
"""Functional test class for nsxv.py."""

import copy
import optparse
from absl.testing import absltest
from xml.etree import ElementTree as ET

from capirca.lib import naming
from capirca.lib import nsxv
from capirca.lib import policy
from capirca.tests.lib import nsxv_mocktest


class NsxvFunctionalTest(absltest.TestCase):
  """Functional testing for NSXV."""

  def setUp(self):
    """Call before every test case."""
    super().setUp()
    parser = optparse.OptionParser()
    parser.add_option(
        '-d',
        '--def',
        dest='definitions',
        help='definitions directory',
        default='../def')
    (FLAGS, args) = _parser.parse_args()
    self.defs = naming.Naming(FLAGS.definitions)

  def tearDown(self):
    super().tearDown()
    pass

  def runTest(self):
    pass

  def test_nsxv_policy(self):
    pol = policy.ParsePolicy(nsxv_mocktest.POLICY, self.defs)
    exp_info = 2
    nsx = copy.deepcopy(pol)
    fw = nsxv.Nsxv(nsx, exp_info)
    output = str(fw)

    # parse the xml
    root = ET.fromstring(output)
    # check section name
    section_name = {'id': '1007', 'name': 'POLICY_NAME'}
    self.assertEqual(root.attrib, section_name)
    # check name and action
    self.assertEqual(root.find('./rule/name').text, 'reject-imap-requests')
    self.assertEqual(root.find('./rule/action').text, 'reject')

    # check IPV4 destination
    exp_destaddr = ['200.1.1.4/31']

    for destination in root.findall('./rule/destinations/destination'):
      self.assertEqual((destination.find('type').text), 'Ipv4Address')
      value = (destination.find('value').text)
      if value not in exp_destaddr:
        self.fail('IPv4Address destination not found in test_nsxv_str()')

    # check protocol
    protocol = int(root.find('./rule/services/service/protocol').text)
    self.assertEqual(protocol, 6)

    # check destination port
    destination_port = root.find('./rule/services/service/destinationPort').text
    self.assertEqual(destination_port, '143')

  def test_nsxv_nosectiondid(self):
    pol = policy.ParsePolicy(nsxv_mocktest.POLICY_NO_SECTION_ID, self.defs)
    exp_info = 2
    nsx = copy.deepcopy(pol)
    fw = nsxv.Nsxv(nsx, exp_info)
    output = str(fw)
    # parse the xml
    root = ET.fromstring(output)
    # check section name
    section_name = {'name': 'POLICY_NO_SECTION_ID_NAME'}
    self.assertEqual(root.attrib, section_name)
    # check name and action
    self.assertEqual(root.find('./rule/name').text, 'accept-icmp')
    self.assertEqual(root.find('./rule/action').text, 'allow')

    # check protocol
    protocol = int(root.find('./rule/services/service/protocol').text)
    self.assertEqual(protocol, 1)

  def test_nsxv_nofiltertype(self):
    pol = policy.ParsePolicy(nsxv_mocktest.POLICY_NO_FILTERTYPE, self.defs)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError, nsxv.Nsxv(pol, 2))

  def test_nsxv_incorrectfiltertype(self):
    pol = policy.ParsePolicy(nsxv_mocktest.POLICY_INCORRECT_FILTERTYPE,
                             self.defs)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError, nsxv.Nsxv(pol, 2))

  def test_nsxv_optionkywd(self):
    pol = policy.ParsePolicy(nsxv_mocktest.POLICY_OPTION_KYWD, self.defs)
    self.assertRaises(nsxv.NsxvAclTermError, str(nsxv.Nsxv(pol, 2)))

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


# tests/lib/nsxv_mocktest.py

# Copyright 2015 The Capirca Project Authors All Rights Reserved.
#
# 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.
#
"""Nsxv Mock Test terms for nsxv module."""



INET_TERM = """\
  term permit-mail-services {
    destination-address:: MAIL_SERVERS
    protocol:: tcp
    destination-port:: MAIL_SERVICES
    action:: accept
  }
  """

INET6_TERM = """\
  term test-icmpv6 {
    protocol:: icmpv6
    icmp-type:: echo-request echo-reply
    action:: accept
  }
  """

INET_FILTER = """\
  header {
    comment:: "Sample inet NSXV filter"
    target:: nsxv INET_FILTER_NAME inet
  }

  term allow-ntp-request {
    comment::"Allow ntp request"
    source-address:: NTP_SERVERS
    source-port:: NTP
    destination-address:: INTERNAL
    destination-port:: NTP
    protocol:: udp
    action:: accept
  }
  """

INET6_FILTER = """\
  header {
    comment:: "Sample inet6 NSXV filter"
    target:: nsxv INET6_FILTER_NAME inet6
  }

  term test-icmpv6 {
    #destination-address:: WEB_SERVERS
    protocol:: icmpv6
    icmp-type:: echo-request echo-reply
    action:: accept
  }
  """

MIXED_FILTER = """\
  header {
    comment:: "Sample mixed NSXV filter"
    target:: nsxv MIXED_FILTER_NAME mixed 1009
  }

  term accept-to-honestdns {
    comment:: "Allow name resolution using honestdns."
    destination-address:: GOOGLE_DNS
    destination-port:: DNS
    protocol:: udp
    action:: accept
  }
  """

POLICY = """\
  header {
    comment:: "Sample NSXV filter"
    target:: nsxv POLICY_NAME inet 1007
  }

  term reject-imap-requests {
    destination-address:: MAIL_SERVERS
    destination-port:: IMAP
    protocol:: tcp
    action:: reject-with-tcp-rst
  }
  """

POLICY_NO_SECTION_ID = """\
  header {
    comment:: "NSXV filter without section id"
    target:: nsxv POLICY_NO_SECTION_ID_NAME inet
  }
  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

POLICY_NO_FILTERTYPE = """\
  header {
    comment:: "Sample NSXV filter"
    target:: nsxv POLICY_NO_FILTERTYPE_NAME
  }
  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

POLICY_INCORRECT_FILTERTYPE = """\
  header {
    comment:: "Sample NSXV filter"
    target:: nsxv POLICY_INCORRECT_FILTERTYPE_NAME inet1
  }
  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

POLICY_OPTION_KYWD = """\
  header {
    comment:: "Sample NSXV filter"
    target:: nsxv POLICY_OPTION_KYWD_NAME inet 1009
  }
  term accept-bgp-replies {
    comment:: "Allow inbound replies to BGP requests."
    source-port:: BGP
    protocol:: tcp
    option:: tcp-established
    action:: accept
  }
  """


# tests/lib/nsxv_test.py

# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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.
"""UnitTest class for nsxv.py."""

from absl.testing import absltest
from unittest import mock
from xml.etree import ElementTree as ET

from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import nsxv
from capirca.lib import policy


INET_TERM = """\
  term permit-mail-services {
    destination-address:: MAIL_SERVERS
    protocol:: tcp
    destination-port:: MAIL_SERVICES
    action:: accept
  }
  """

INET6_TERM = """\
  term test-icmpv6 {
    protocol:: icmpv6
    icmp-type:: echo-request echo-reply
    action:: accept
  }
  """

INET_FILTER = """\
  header {
    comment:: "Sample inet NSXV filter"
    target:: nsxv INET_FILTER_NAME inet
  }

  term allow-ntp-request {
    comment::"Allow ntp request"
    source-address:: NTP_SERVERS
    source-port:: NTP
    destination-address:: INTERNAL
    destination-port:: NTP
    protocol:: udp
    action:: accept
  }
  """

INET_FILTER_2 = """\
  header {
    comment:: "Sample inet NSXV filter"
    target:: nsxv INET_FILTER2_NAME inet
  }

  term allow-ntp-request {
    comment::"Allow ntp request"
    source-address:: NTP_SERVERS
    source-port:: NTP
    destination-address:: INTERNAL
    destination-port:: NTP
    protocol:: udp
    policer:: batman
    action:: accept
  }
  """

INET_FILTER_WITH_ESTABLISHED = """\
  header {
    comment:: "Sample inet NSXV filter"
    target:: nsxv INET_FILTER_WITH_ESTABLISHED_NAME inet
  }

  term allow-ntp-request {
    comment::"Allow ntp request"
    source-address:: NTP_SERVERS
    source-port:: NTP
    destination-address:: INTERNAL
    destination-port:: NTP
    protocol:: udp
    option:: tcp-established
    policer:: batman
    action:: accept
  }
  """
MIXED_HEADER = """\
  header {
    comment:: "Sample mixed NSXV filter"
    target:: nsxv MIXED_HEADER_NAME mixed
  }

"""

INET_HEADER = """\
  header {
    comment:: "Sample mixed NSXV filter"
    target:: nsxv INET_HEADER_NAME inet
  }

"""

MIXED_FILTER_INET_ONLY = MIXED_HEADER + INET_TERM

INET_FILTER_NO_SOURCE = INET_HEADER + INET_TERM

INET6_FILTER = """\
  header {
    comment:: "Sample inet6 NSXV filter"
    target:: nsxv INET6_FILTER_NAME inet6
  }

  term test-icmpv6 {
    #destination-address:: WEB_SERVERS
    protocol:: icmpv6
    icmp-type:: echo-request echo-reply
    action:: accept
  }
  """

MIXED_FILTER = """\
  header {
    comment:: "Sample mixed NSXV filter"
    target:: nsxv MIXED_FILTER_NAME mixed
  }

  term accept-to-honestdns {
    comment:: "Allow name resolution using honestdns."
    destination-address:: GOOGLE_DNS
    destination-port:: DNS
    protocol:: udp
    action:: accept
  }
  """

POLICY = """\
  header {
    comment:: "Sample NSXV filter"
    target:: nsxv POLICY_NAME inet
  }

  term reject-imap-requests {
    destination-address:: MAIL_SERVERS
    destination-port:: IMAP
    protocol:: tcp
    action:: reject-with-tcp-rst
  }
  """

POLICY_WITH_SECURITY_GROUP = """\
  header {
    comment:: "Sample filter with Security Group"
    target:: nsxv POLICY_WITH_SECURITY_GROUP_NAME inet 1010 securitygroup \
    securitygroup-Id
  }

  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

HEADER_WITH_SECTIONID = """\
  header {
    comment:: "Sample NSXV filter1"
    target:: nsxv HEADER_WITH_SECTIONID_NAME inet 1009
  }
  """

HEADER_WITH_SECURITYGROUP = """\
  header {
    comment:: "Sample NSXV filter2"
    target:: nsxv HEADER_WITH_SECURITYGROUP_NAME inet6 securitygroup \
    securitygroup-Id1
  }
  """

BAD_HEADER = """\
  header {
    comment:: "Sample NSXV filter3"
    target:: nsxv BAD_HEADER_NAME inet 1011 securitygroup
  }
  """

BAD_HEADER_1 = """\
  header {
    comment:: "Sample NSXV filter4"
    target:: nsxv BAD_HEADER_1_NAME 1012
  }
  """

BAD_HEADER_2 = """\
  header {
    comment:: "Sample NSXV filter5"
    target:: nsxv BAD_HEADER_2_NAME inet securitygroup
  }
  """

BAD_HEADER_3 = """\
  header {
    comment:: "Sample NSXV filter6"
    target:: nsxv BAD_HEADER_3_NAME
  }
  """

BAD_HEADER_4 = """\
  header {
    comment:: "Sample NSXV filter7"
    target:: nsxv BAD_HEADER_3_NAME inet 1234 securitygroup securitygroup \
    securitygroupId1
  }
  """

TERM = """\
  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

MIXED_TO_V4 = """\
  term mixed_to_v4 {
    source-address:: GOOGLE_DNS
    destination-address:: INTERNAL
    protocol:: tcp udp
    action:: accept
  }
  """

V4_TO_MIXED = """\
  term v4_to_mixed {
    source-address:: INTERNAL
    destination-address:: GOOGLE_DNS
    protocol:: tcp udp
    action:: accept
  }
  """

MIXED_TO_V6 = """\
  term mixed_to_v6 {
    source-address:: GOOGLE_DNS
    destination-address:: SOME_HOST
    action:: accept
  }
  """

V6_TO_MIXED = """\
  term v6_to_mixed {
    source-address:: SOME_HOST
    destination-address:: GOOGLE_DNS
    action:: accept
  }
  """

MIXED_TO_MIXED = """\
  term mixed_to_mixed {
    source-address:: GOOGLE_DNS
    destination-address:: GOOGLE_DNS
    action:: accept
  }
  """

MIXED_TO_ANY = """\
  term mixed_to_any {
    source-address:: GOOGLE_DNS
    action:: accept
  }
  """

ANY_TO_MIXED = """\
  term any_to_mixed {
    destination-address:: GOOGLE_DNS
    action:: accept
  }
  """

V4_TO_V4 = """\
  term v4_to_v4 {
    source-address:: NTP_SERVERS
    destination-address:: INTERNAL
    action:: accept
  }
  """

V6_TO_V6 = """\
  term v6_to_v6 {
    source-address:: SOME_HOST
    destination-address:: SOME_HOST
    action:: accept
  }
  """

V4_TO_V6 = """\
  term v4_to_v6 {
    source-address:: INTERNAL
    destination-address:: SOME_HOST
    action:: accept
  }
  """

V6_TO_V4 = """\
  term v6_to_v4 {
    source-address:: SOME_HOST
    destination-address:: INTERNAL
    action:: accept
  }
  """

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'option',
    'platform',
    'platform_exclude',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2
_PLATFORM = 'nsxv'


class TermTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testInitForinet(self):
    """Test for Term._init_."""
    inet_term = nsxv.Term(INET_TERM, 'inet')
    self.assertEqual(inet_term.af, 4)
    self.assertEqual(inet_term.filter_type, 'inet')

  def testInitForinet6(self):
    """Test for Term._init_."""
    inet6_term = nsxv.Term(INET6_TERM, 'inet6', None, 6)
    self.assertEqual(inet6_term.af, 6)
    self.assertEqual(inet6_term.filter_type, 'inet6')

  def testServiceToStr(self):
    """Test for Term._ServiceToStr."""

    proto = 6
    icmp_types = []
    dports = [(1024, 65535)]
    spots = [(123, 123)]
    nsxv_term = nsxv.Term(INET_TERM, 'inet')
    service = nsxv_term._ServiceToString(proto, spots, dports, icmp_types)
    self.assertEqual(service, '<service><protocol>6</protocol><sourcePort>'
                     '123</sourcePort><destinationPort>1024-65535'
                     '</destinationPort></service>')

  def testStrForinet(self):
    """Test for Term._str_."""
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']

    pol = policy.ParsePolicy(INET_FILTER, self.naming, False)
    af = 4
    for _, terms in pol.filters:
      nsxv_term = nsxv.Term(terms[0], af)
      rule_str = nsxv.Term.__str__(nsxv_term)
    # parse xml rule and check if the values are correct
    root = ET.fromstring(rule_str)
    # check name and action
    self.assertEqual(root.find('name').text, 'allow-ntp-request')
    self.assertEqual(root.find('action').text, 'allow')

    # check source address
    exp_sourceaddr = ['10.0.0.1', '10.0.0.2']
    source_address = root.findall('./sources/source')
    self.assertNotEqual(len(source_address), 0)
    for source in source_address:
      self.assertEqual((source.find('type').text), 'Ipv4Address')
      value = (source.find('value').text)
      if value not in exp_sourceaddr:
        self.fail('IPv4Address source address not found in test_str_forinet()')

    # check destination address
    exp_destaddr = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
    destination_address = root.findall('./destinations/destination')
    self.assertNotEqual(len(destination_address), 0)
    for destination in destination_address:
      self.assertEqual((destination.find('type').text), 'Ipv4Address')
      value = (destination.find('value').text)
      if value not in exp_destaddr:
        self.fail('IPv4Address destination not found in test_str_forinet()')

    # check protocol
    protocol = int(root.find('./services/service/protocol').text)
    self.assertEqual(protocol, 17)

    # check source port
    source_port = root.find('./services/service/sourcePort').text
    self.assertEqual(source_port, '123')

    # check destination port
    destination_port = root.find('./services/service/destinationPort').text
    self.assertEqual(destination_port, '123')

    # check notes
    notes = root.find('notes').text
    self.assertEqual(notes, 'Allow ntp request')

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('NTP', 'udp')] * 2)

  def testStrForinet6(self):
    """Test for Term._str_."""
    pol = policy.ParsePolicy(INET6_FILTER, self.naming, False)
    af = 6
    filter_type = 'inet6'
    for _, terms in pol.filters:
      nsxv_term = nsxv.Term(terms[0], filter_type, None, af)
      rule_str = nsxv.Term.__str__(nsxv_term)

    # parse xml rule and check if the values are correct
    root = ET.fromstring(rule_str)
    # check name and action
    self.assertEqual(root.find('name').text, 'test-icmpv6')
    self.assertEqual(root.find('action').text, 'allow')

    # check protocol and sub protocol
    exp_subprotocol = [128, 129]
    for service in root.findall('./services/service'):
      protocol = int(service.find('protocol').text)
      self.assertEqual(protocol, 58)

      sub_protocol = int(service.find('subProtocol').text)
      if sub_protocol not in exp_subprotocol:
        self.fail('subProtocol not matched in test_str_forinet6()')

  def testTranslatePolicy(self):
    """Test for Nsxv.test_TranslatePolicy."""
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']

    pol = policy.ParsePolicy(INET_FILTER, self.naming, False)
    translate_pol = nsxv.Nsxv(pol, EXP_INFO)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'inet')
      self.assertEqual(filter_list, ['inet'])
      self.assertEqual(len(terms), 1)

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('NTP', 'udp')] * 2)

  def testTranslatePolicyMixedFilterInetOnly(self):
    """Test for Nsxv.test_TranslatePolicy. Testing Mixed filter with inet."""
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(MIXED_FILTER_INET_ONLY, self.naming, False)
    translate_pol = nsxv.Nsxv(pol, EXP_INFO)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'mixed')
      self.assertEqual(filter_list, ['mixed'])
      self.assertEqual(len(terms), 1)
      self.assertIn('10.0.0.0/8', str(terms[0]))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('MAIL_SERVERS')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('MAIL_SERVICES', 'tcp')] * 1)

  def testTranslatePolicyMixedFilterInet6Only(self):
    """Test for Nsxv.test_TranslatePolicy. Testing Mixed filter with inet6."""
    self.naming.GetNetAddr.return_value = [nacaddr.IP('2001:4860:4860::8844')]

    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(MIXED_FILTER_INET_ONLY, self.naming, False)
    translate_pol = nsxv.Nsxv(pol, EXP_INFO)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'mixed')
      self.assertEqual(filter_list, ['mixed'])
      self.assertEqual(len(terms), 1)
      self.assertIn('2001:4860:4860::8844', str(terms[0]))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('MAIL_SERVERS')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('MAIL_SERVICES', 'tcp')] * 1)

  def testTranslatePolicyMixedFilterInetMixed(self):
    """Test for Nsxv.test_TranslatePolicy. Testing Mixed filter with mixed."""
    self.naming.GetNetAddr.return_value = [
        nacaddr.IP('2001:4860:4860::8844'),
        nacaddr.IP('10.0.0.0/8')
    ]
    self.naming.GetServiceByProto.return_value = ['25']

    pol = policy.ParsePolicy(MIXED_FILTER_INET_ONLY, self.naming, False)
    translate_pol = nsxv.Nsxv(pol, EXP_INFO)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'mixed')
      self.assertEqual(filter_list, ['mixed'])
      self.assertEqual(len(terms), 1)
      self.assertIn('2001:4860:4860::8844', str(terms[0]))
      self.assertIn('10.0.0.0/8', str(terms[0]))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('MAIL_SERVERS')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('MAIL_SERVICES', 'tcp')] * 1)

  def testTranslatePolicyWithEstablished(self):
    """Test for Nsxv.test_TranslatePolicy."""
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']

    pol = policy.ParsePolicy(INET_FILTER_WITH_ESTABLISHED, self.naming, False)
    translate_pol = nsxv.Nsxv(pol, EXP_INFO)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'inet')
      self.assertEqual(filter_list, ['inet'])
      self.assertEqual(len(terms), 1)

      self.assertNotIn('<sourcePort>123</sourcePort><destinationPort>123',
                       str(terms[0]))

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('NTP', 'udp')] * 2)

  def testNsxvStr(self):
    """Test for Nsxv._str_."""
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'),
         nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]
    self.naming.GetServiceByProto.return_value = ['53']

    pol = policy.ParsePolicy(MIXED_FILTER, self.naming, False)
    target = nsxv.Nsxv(pol, EXP_INFO)

    # parse the output and seperate sections and comment
    section_tokens = str(target).split('<section')
    sections = []

    for sec in section_tokens:
      section = sec.replace('name=', '<section name=')
      sections.append(section)
    # parse the xml
    # Checking comment tag
    comment = sections[0]
    if 'Id' not in comment:
      self.fail('Id missing in xml comment in test_nsxv_str()')
    if 'Date' not in comment:
      self.fail('Date missing in xml comment in test_nsxv_str()')
    if 'Revision' not in comment:
      self.fail('Revision missing in xml comment in test_nsxv_str()')

    root = ET.fromstring(sections[1])
    # check section name
    section_name = {'name': 'MIXED_FILTER_NAME'}
    self.assertEqual(root.attrib, section_name)
    # check name and action
    self.assertEqual(root.find('./rule/name').text, 'accept-to-honestdns')
    self.assertEqual(root.find('./rule/action').text, 'allow')

    # check IPV4 and IPV6 destinations
    exp_ipv4dest = ['8.8.4.4', '8.8.8.8']
    exp_ipv6dest = ['2001:4860:4860::8844', '2001:4860:4860::8888']

    destination_address = root.findall('./rule/destinations/destination')
    self.assertNotEqual(len(destination_address), 0)
    for destination in destination_address:
      addr_type = destination.find('type').text
      value = (destination.find('value').text)

      if 'Ipv4Address' in addr_type:
        if value not in exp_ipv4dest:
          self.fail('IPv4Address not found in test_nsxv_str()')
      else:
        if value not in exp_ipv6dest:
          self.fail('IPv6Address not found in test_nsxv_str()')

    # check protocol
    protocol = int(root.find('./rule/services/service/protocol').text)
    self.assertEqual(protocol, 17)

    # check destination port
    destination_port = root.find('./rule/services/service/destinationPort').text
    self.assertEqual(destination_port, '53')

    # check notes
    notes = root.find('./rule/notes').text
    self.assertEqual(notes, 'Allow name resolution using honestdns.')

    self.naming.GetNetAddr.assert_called_once_with('GOOGLE_DNS')
    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'udp')

  def testNsxvStrWithSecurityGroup(self):
    """Test for Nsxv._str_."""
    pol = policy.ParsePolicy(POLICY_WITH_SECURITY_GROUP, self.naming, False)
    target = nsxv.Nsxv(pol, EXP_INFO)

    # parse the output and seperate sections and comment
    section_tokens = str(target).split('<section')
    sections = []

    for sec in section_tokens:
      section = sec.replace('id=', '<section id=')
      sections.append(section)
    # parse the xml
    # Checking comment tag
    comment = sections[0]
    if 'Id' not in comment:
      self.fail('Id missing in xml comment in test_nsxv_str()')
    if 'Date' not in comment:
      self.fail('Date missing in xml comment in test_nsxv_str()')
    if 'Revision' not in comment:
      self.fail('Revision missing in xml comment in test_nsxv_str()')

    root = ET.fromstring(sections[1])
    # check section name
    section_name = {'id': '1010', 'name': 'POLICY_WITH_SECURITY_GROUP_NAME'}
    self.assertEqual(root.attrib, section_name)
    # check name and action
    self.assertEqual(root.find('./rule/name').text, 'accept-icmp')
    self.assertEqual(root.find('./rule/action').text, 'allow')

    # check protocol
    protocol = int(root.find('./rule/services/service/protocol').text)
    self.assertEqual(protocol, 1)

    # check appliedTo
    applied_to = root.find('./rule/appliedToList/appliedTo/value').text
    self.assertEqual(applied_to, 'securitygroup-Id')

  def testBuildTokens(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']
    pol1 = nsxv.Nsxv(policy.ParsePolicy(INET_FILTER, self.naming), 2)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])

  def testBuildWarningTokens(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']

    pol1 = nsxv.Nsxv(policy.ParsePolicy(INET_FILTER_2, self.naming), 2)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])

  def testParseFilterOptionsCase1(self):
    pol = nsxv.Nsxv(policy.ParsePolicy(HEADER_WITH_SECTIONID + TERM,
                                       self.naming, False), EXP_INFO)
    for (header, _, _, _) in pol.nsxv_policies:
      filter_options = header.FilterOptions(_PLATFORM)
      pol._ParseFilterOptions(filter_options)
      self.assertEqual(nsxv.Nsxv._FILTER_OPTIONS_DICT['filter_type'], 'inet')
      self.assertEqual(nsxv.Nsxv._FILTER_OPTIONS_DICT['section_id'], '1009')
      self.assertIsNone(nsxv.Nsxv._FILTER_OPTIONS_DICT['applied_to'])

  def testParseFilterOptionsCase2(self):
    pol = nsxv.Nsxv(policy.ParsePolicy(HEADER_WITH_SECURITYGROUP + INET6_TERM,
                                       self.naming, False), EXP_INFO)
    for (header, _, _, _) in pol.nsxv_policies:
      filter_options = header.FilterOptions(_PLATFORM)
      pol._ParseFilterOptions(filter_options)
      self.assertEqual(nsxv.Nsxv._FILTER_OPTIONS_DICT['filter_type'], 'inet6')
      self.assertEqual(nsxv.Nsxv._FILTER_OPTIONS_DICT['section_id'], 0)
      self.assertEqual(nsxv.Nsxv._FILTER_OPTIONS_DICT['applied_to'],
                       'securitygroup-Id1')

  def testBadHeaderCase(self):
    pol = policy.ParsePolicy(BAD_HEADER + INET6_TERM, self.naming, False)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError,
                      nsxv.Nsxv, pol, EXP_INFO)

  def testBadHeaderCase1(self):
    pol = policy.ParsePolicy(BAD_HEADER_1 + TERM, self.naming, False)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError,
                      nsxv.Nsxv, pol, EXP_INFO)

  def testBadHeaderCase2(self):
    pol = policy.ParsePolicy(BAD_HEADER_2 + INET6_TERM, self.naming, False)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError,
                      nsxv.Nsxv, pol, EXP_INFO)

  def testBadHeaderCase3(self):
    pol = policy.ParsePolicy(BAD_HEADER_3 + INET6_TERM, self.naming, False)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError,
                      nsxv.Nsxv, pol, EXP_INFO)

  def testBadHeaderCase4(self):
    pol = policy.ParsePolicy(BAD_HEADER_4 + INET6_TERM, self.naming, False)
    self.assertRaises(nsxv.UnsupportedNsxvAccessListError,
                      nsxv.Nsxv, pol, EXP_INFO)

  def testMixedToV4(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            MIXED_TO_V4)
    self.verify_address_type(source_addr, 'Ipv4Address')
    self.verify_address_type(dest_addr, 'Ipv4Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('GOOGLE_DNS'), mock.call('INTERNAL')])

  def testV4ToMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')],
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            V4_TO_MIXED)
    self.verify_address_type(source_addr, 'Ipv4Address')
    self.verify_address_type(dest_addr, 'Ipv4Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('INTERNAL'), mock.call('GOOGLE_DNS')])

  def testMixedToV6(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')],
        [nacaddr.IP('2001:4860:8000::/33')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            MIXED_TO_V6)
    self.verify_address_type(source_addr, 'Ipv6Address')
    self.verify_address_type(dest_addr, 'Ipv6Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('GOOGLE_DNS'), mock.call('SOME_HOST')])

  def testV6ToMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('2001:4860:8000::/33')],
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            V6_TO_MIXED)
    self.verify_address_type(source_addr, 'Ipv6Address')
    self.verify_address_type(dest_addr, 'Ipv6Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('SOME_HOST'), mock.call('GOOGLE_DNS')])

  def testMixedToMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')],
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            MIXED_TO_MIXED)
    self.verify_mixed_address_types(source_addr)
    self.verify_mixed_address_types(dest_addr)

    self.naming.GetNetAddr.assert_has_calls([mock.call('GOOGLE_DNS')] * 2)

  def testMixedToAny(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            MIXED_TO_ANY)
    self.verify_mixed_address_types(source_addr)
    self.assertEqual(len(dest_addr), 0)

    self.naming.GetNetAddr.assert_has_calls([mock.call('GOOGLE_DNS')])

  def testAnyToMixed(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('8.8.4.4'), nacaddr.IP('8.8.8.8'),
         nacaddr.IP('2001:4860:4860::8844'),
         nacaddr.IP('2001:4860:4860::8888')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            ANY_TO_MIXED)
    self.assertEqual(len(source_addr), 0)
    self.verify_mixed_address_types(dest_addr)

    self.naming.GetNetAddr.assert_has_calls([mock.call('GOOGLE_DNS')])

  def testV4ToV4(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            V4_TO_V4)
    self.verify_address_type(source_addr, 'Ipv4Address')
    self.verify_address_type(dest_addr, 'Ipv4Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])

  def testV6ToV6(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('2001:4860:8000::/33')],
        [nacaddr.IP('2001:4860:8000::/33')]]

    source_addr, dest_addr = self.get_source_dest_addresses(MIXED_HEADER +
                                                            V6_TO_V6)
    self.verify_address_type(source_addr, 'Ipv6Address')
    self.verify_address_type(dest_addr, 'Ipv6Address')
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('SOME_HOST')] * 2)

  def testV4ToV6(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')],
        [nacaddr.IP('2001:4860:8000::/33')]]

    root = self.get_xml_root(MIXED_HEADER + V4_TO_V6)
    rule = root.findall('./rule')
    # No term(rule) will be rendered in this case
    self.assertEqual(len(rule), 0)
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('INTERNAL'), mock.call('SOME_HOST')])

  def testV6ToV4(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('2001:4860:8000::/33')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]

    root = self.get_xml_root(MIXED_HEADER + V6_TO_V4)
    rule = root.findall('./rule')
    # No term(rule) will be rendered in this case
    self.assertEqual(len(rule), 0)
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('SOME_HOST'), mock.call('INTERNAL')])

  def get_xml_root(self, data):
    pol = policy.ParsePolicy(data, self.naming, False)
    target = nsxv.Nsxv(pol, EXP_INFO)

    # parse the output and seperate sections and comment
    section_tokens = str(target).split('<section')
    sections = []

    for sec in section_tokens:
      section = sec.replace('name=', '<section name=')
      sections.append(section)
    # parse the xml
    root = ET.fromstring(sections[1])
    return root

  def get_source_dest_addresses(self, data):
    root = self.get_xml_root(data)
    source_addr = root.findall('./rule/sources/source')
    dest_addr = root.findall('./rule/destinations/destination')

    return source_addr, dest_addr

  def verify_address_type(self, address, type_):
    self.assertNotEqual(len(address), 0)
    for addr in address:
      addr_type = addr.find('type').text
      self.assertEqual(addr_type, type_)

  def verify_mixed_address_types(self, address):
    self.assertNotEqual(len(address), 0)
    ipv4_address = False
    ipv6_address = False
    for addr in address:
      addr_type = addr.find('type').text
      if addr_type == 'Ipv4Address':
        ipv4_address = True
      if addr_type == 'Ipv6Address':
        ipv6_address = True

    self.assertTrue(ipv4_address)
    self.assertTrue(ipv6_address)


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


# tests/lib/nsxv_unittest.py

# Copyright 2015 The Capirca Project Authors All Rights Reserved.
#
# 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.
#
"""UnitTest class for nsxv.py."""

import optparse
from absl.testing import absltest
from xml.etree import ElementTree as ET

from capirca.lib import naming
from capirca.lib import nsxv
from capirca.lib import policy
from capirca.tests.lib import nsxv_mocktest


class TermTest(absltest.TestCase):

  def setUp(self):
    """Call before every test case."""
    super().setUp()
    parser = optparse.OptionParser()
    parser.add_option(
        '-d',
        '--def',
        dest='definitions',
        help='definitions directory',
        default='../def')
    (FLAGS, args) = parser.parse_args()
    self.defs = naming.Naming(FLAGS.definitions)

  def tearDown(self):
    super().setUp()
    pass

  def runTest(self):
    pass

  def test_init_forinet(self):
    """Test for Term._init_."""
    inet_term = nsxv.Term(nsxv_mocktest.INET_TERM, 'inet')
    self.assertEqual(inet_term.af, 4)
    self.assertEqual(inet_term.filter_type, 'inet')

  def test_init_forinet6(self):
    """Test for Term._init_."""
    inet6_term = nsxv.Term(nsxv_mocktest.INET6_TERM, 'inet6', 6)
    self.assertEqual(inet6_term.af, 6)
    self.assertEqual(inet6_term.filter_type, 'inet6')

  def test_ServiceToStr(self):
    """Test for Term._ServiceToStr."""

    proto = 6
    icmp_types = []
    dports = [(1024, 65535)]
    spots = [(123, 123)]
    nsxv_term = nsxv.Term(nsxv_mocktest.INET_TERM, 'inet')
    service = nsxv_term._ServiceToString(proto, spots, dports, icmp_types)
    self.assertEqual(
        service, '<service><protocol>6</protocol><sourcePort>123'
        '</sourcePort><destinationPort>1024-65535'
        '</destinationPort></service>')

  def test_str_forinet(self):
    """Test for Term._str_."""
    pol = policy.ParsePolicy(nsxv_mocktest.INET_FILTER, self.defs, False)
    af = 4
    for _, terms in pol.filters:
      nsxv_term = nsxv.Term(terms[0], af)
      rule_str = nsxv.Term.__str__(nsxv_term)
    # parse xml rule and check if the values are correct
    root = ET.fromstring(rule_str)
    # check name and action
    self.assertEqual(root.find('name').text, 'allow-ntp-request')
    self.assertEqual(root.find('action').text, 'allow')

    # check source address
    exp_sourceaddr = ['10.0.0.1', '10.0.0.2']
    for destination in root.findall('./sources/source'):
      self.assertEqual((destination.find('type').text), 'Ipv4Address')
      value = (destination.find('value').text)
      if value not in exp_sourceaddr:
        self.fail('IPv4Address source address not found in test_str_forinet()')

    # check destination address
    exp_destaddr = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
    for destination in root.findall('./destinations/destination'):
      self.assertEqual((destination.find('type').text), 'Ipv4Address')
      value = (destination.find('value').text)
      if value not in exp_destaddr:
        self.fail('IPv4Address destination not found in test_str_forinet()')

    # check protocol
    protocol = int(root.find('./services/service/protocol').text)
    self.assertEqual(protocol, 17)

    # check source port
    source_port = root.find('./services/service/sourcePort').text
    self.assertEqual(source_port, '123')

    # check destination port
    destination_port = root.find('./services/service/destinationPort').text
    self.assertEqual(destination_port, '123')

    # check notes
    notes = root.find('notes').text
    self.assertEqual(notes, 'Allow ntp request')

  def test_str_forinet6(self):
    """Test for Term._str_."""
    pol = policy.ParsePolicy(nsxv_mocktest.INET6_FILTER, self.defs, False)
    af = 6
    filter_type = 'inet6'
    for _, terms in pol.filters:
      nsxv_term = nsxv.Term(terms[0], filter_type, af)
      rule_str = nsxv.Term.__str__(nsxv_term)

    # parse xml rule and check if the values are correct
    root = ET.fromstring(rule_str)
    # check name and action
    self.assertEqual(root.find('name').text, 'test-icmpv6')
    self.assertEqual(root.find('action').text, 'allow')

    # check protocol and sub protocol
    exp_subprotocol = [128, 129]
    for service in root.findall('./services/service'):
      protocol = int(service.find('protocol').text)
      self.assertEqual(protocol, 58)

      sub_protocol = int(service.find('subProtocol').text)
      if sub_protocol not in exp_subprotocol:
        self.fail('subProtocol not matched in test_str_forinet6()')

  def test_TranslatePolicy(self):
    """Test for Nsxv.test_TranslatePolicy."""
    # exp_info default is 2
    exp_info = 2
    pol = policy.ParsePolicy(nsxv_mocktest.INET_FILTER, self.defs, False)
    translate_pol = nsxv.Nsxv(pol, exp_info)
    nsxv_policies = translate_pol.nsxv_policies
    for (_, filter_name, filter_list, terms) in nsxv_policies:
      self.assertEqual(filter_name, 'inet')
      self.assertEqual(filter_list, ['inet'])
      self.assertEqual(len(terms), 1)

  def test_nsxv_str(self):
    """Test for Nsxv._str_."""
    # exp_info default is 2
    exp_info = 2
    pol = policy.ParsePolicy(nsxv_mocktest.MIXED_FILTER, self.defs, False)
    target = nsxv.Nsxv(pol, exp_info)

    # parse the xml and check the values
    root = ET.fromstring(str(target))
    # check section name
    section_name = {'id': '1009', 'name': 'MIXED_FILTER_NAME'}
    self.assertEqual(root.attrib, section_name)
    # check name and action
    self.assertEqual(root.find('./rule/name').text, 'accept-to-honestdns')
    self.assertEqual(root.find('./rule/action').text, 'allow')

    # check IPV4 and IPV6 destinations
    exp_ipv4dest = ['8.8.4.4', '8.8.8.8']
    exp_ipv6dest = ['2001:4860:4860::8844', '2001:4860:4860::8888']

    for destination in root.findall('./rule/destinations/destination'):
      obj_type = destination.find('type').text
      value = (destination.find('value').text)

      if 'Ipv4Address' in obj_type:
        if value not in exp_ipv4dest:
          self.fail('IPv4Address not found in test_nsxv_str()')
      else:
        if value not in exp_ipv6dest:
          self.fail('IPv6Address not found in test_nsxv_str()')

    # check protocol
    protocol = int(root.find('./rule/services/service/protocol').text)
    self.assertEqual(protocol, 17)

    # check destination port
    destination_port = root.find('./rule/services/service/destinationPort').text
    self.assertEqual(destination_port, '53')

    # check notes
    notes = root.find('./rule/notes').text
    self.assertEqual(notes, 'Allow name resolution using honestdns.')

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


# tests/lib/openconfig_test.py

# Copyright 2021 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for OpenConfig rendering module."""

import json
from absl.testing import absltest
from unittest import mock

from absl.testing import parameterized
from capirca.lib import aclgenerator
from capirca.lib import openconfig
from capirca.lib import gcp
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy

GOOD_HEADER = """
header {
  comment:: "The general policy comment."
  target:: openconfig inet
}
"""

GOOD_SADDR = """
term good-term-1 {
  comment:: "Allow source address."
  source-address:: CORP_EXTERNAL
  action:: accept
}
"""

GOOD_DADDR = """
term good-term-1 {
  comment:: "Allow destination address."
  destination-address:: CORP_EXTERNAL
  action:: accept
}
"""

GOOD_SPORT = """
term good-term-1 {
  comment:: "Allow TCP 53 source."
  source-port:: DNS
  protocol:: tcp
  action:: accept
}
"""

GOOD_DPORT = """
term good-term-1 {
  comment:: "Allow TCP 53 dest."
  destination-port:: DNS
  protocol:: tcp
  action:: accept
}
"""

GOOD_MULTI_PROTO_DPORT = """
term good-term-1 {
  comment:: "Allow TCP & UDP 53."
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_EVERYTHING = """
term good-term-1 {
  comment:: "Allow TCP & UDP 53 with saddr/daddr."
  destination-address:: CORP_EXTERNAL
  source-address:: CORP_EXTERNAL
  destination-port:: DNS
  protocol:: udp tcp
  action:: accept
}
"""

GOOD_JSON_SADDR = """
[
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv4": {
      "config": {
        "source-address": "10.2.3.4/32"
      }
    }
  }
]
"""

GOOD_JSON_V6_SADDR = """
 [
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv6": {
      "config": {
        "source-address": "2001:4860:8000::5/128"
      }
    }
  }
]
"""

GOOD_JSON_DADDR = """
[
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv4": {
      "config": {
        "destination-address": "10.2.3.4/32"
      }
    }
  }
]
"""

GOOD_JSON_V6_DADDR = """
 [
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv6": {
      "config": {
        "destination-address": "2001:4860:8000::5/128"
      }
    }
  }
]
"""

GOOD_JSON_MIXED_DADDR = """
[
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv4": {
      "config": {
        "destination-address": "10.2.3.4/32"
      }
    }
  },
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv6": {
      "config": {
        "destination-address": "2001:4860:8000::5/128"
      }
    }
  }
]
"""

GOOD_JSON_SPORT = """
[
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv4": {
      "config": {
        "protocol": 6
    },
    "transport": {
      "config": {
        "source-port": 53}
      }
    }
  }
]
"""

GOOD_JSON_DPORT = """
[
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv4": {
      "config": {
        "protocol": 6
    },
    "transport": {
      "config": {
        "destination-port": 53}
      }
    }
  }
]
"""

GOOD_JSON_MULTI_PROTO_DPORT = """
[
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv4": {
      "config": {
        "protocol": 17
    },
    "transport": {
      "config": {
        "destination-port": 53}
      }
    }
  },
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv4": {
      "config": {
        "protocol": 6
      },
      "transport": {
        "config": {
          "destination-port": 53}
        }
      }
  }
]
"""

GOOD_JSON_EVERYTHING = """
 [
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv4": {
      "config": {
        "destination-address": "10.2.3.4/32",
        "protocol": 17,
        "source-address": "10.2.3.4/32"
      },
      "transport": {
        "config": {
          "destination-port": 53
        }
      }
    }
  },
  {
    "actions": {
      "config": {
        "forwarding-action": "ACCEPT"
      }
    },
    "ipv4": {
      "config": {
        "destination-address": "10.2.3.4/32",
        "protocol": 6,
        "source-address": "10.2.3.4/32"
      },
      "transport": {
        "config": {
          "destination-port": 53
        }
      }
    }
  }
]
"""
GOOD_HEADER_INET6 = """
header {
  comment:: "The general policy comment."
  target:: openconfig inet6
}
"""

GOOD_HEADER_MIXED = """
header {
  comment:: "The general policy comment."
  target:: openconfig mixed
}
"""

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2

TEST_IPS = [nacaddr.IP('10.2.3.4/32'),
            nacaddr.IP('2001:4860:8000::5/128')]


_TERM_SOURCE_TAGS_LIMIT = 30
_TERM_TARGET_TAGS_LIMIT = 70
_TERM_PORTS_LIMIT = 256


class OpenConfigTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def _StripAclHeaders(self, acl):
    return '\n'.join([line for line in str(acl).split('\n')
                      if not line.lstrip().startswith('#')])

  def testSaddr(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER + GOOD_SADDR, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_SADDR)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')

  def testDaddr(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER + GOOD_DADDR, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_DADDR)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')

  def testSport(self):
    self.naming.GetNetAddr.return_value = TEST_IPS
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER + GOOD_SPORT, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_SPORT)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp')])

  def testDport(self):
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER + GOOD_DPORT, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_DPORT)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp')])

  def testEverything(self):
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER + GOOD_EVERYTHING, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_EVERYTHING)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'udp'),
        mock.call('DNS', 'tcp')])

  def testV6Saddr(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER_INET6 + GOOD_SADDR, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_V6_SADDR)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')

  def testV6Daddr(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER_INET6 + GOOD_DADDR, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_V6_DADDR)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')

  def testMixedDaddr(self):
    self.naming.GetNetAddr.return_value = TEST_IPS

    acl = openconfig.OpenConfig(policy.ParsePolicy(
        GOOD_HEADER_MIXED + GOOD_DADDR, self.naming), EXP_INFO)
    expected = json.loads(GOOD_JSON_MIXED_DADDR)
    self.assertEqual(expected, json.loads(str(acl)))

    self.naming.GetNetAddr.assert_called_once_with('CORP_EXTERNAL')



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


# tests/lib/packetfilter_test.py

# Copyright 2013 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for packetfilter rendering module."""

import datetime
from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import packetfilter
from capirca.lib import policy


GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: packetfilter test-filter mixed
}
"""

GOOD_HEADER_STATELESS = """
header {
  comment:: "this is a stateless test acl"
  target:: packetfilter test-filter mixed nostate
}
"""

GOOD_HEADER_INET4 = """
header {
  comment:: "this is a test acl"
  target:: packetfilter test-filter
}
"""

GOOD_HEADER_INET6 = """
header {
  comment:: "this is a test acl"
  target:: packetfilter test-filter inet6
}
"""

GOOD_HEADER_DIRECTIONAL = """
header {
  comment:: "this is a test acl"
  target:: packetfilter test-filter out mixed
}
"""

GOOD_HEADER_DIRECTIONAL_STATELESS = """
header {
  comment:: "this is a test acl"
  target:: packetfilter test-filter out mixed nostate
}
"""

GOOD_TERM_ICMP = """
term good-term-icmp {
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_ICMP_TYPES = """
term good-term-icmp-types {
  protocol:: icmp
  icmp-type:: echo-reply unreachable time-exceeded
  action:: deny
}
"""

GOOD_TERM_ICMPV6 = """
term good-term-icmpv6 {
  protocol:: icmpv6
  action:: accept
}
"""

GOOD_TERM_ICMPV6_TYPES = """
term good-term-icmpv6-types {
  protocol:: icmpv6
  icmp-type:: echo-reply
  action:: deny
}
"""

BAD_TERM_ICMP = """
term test-icmp {
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

BAD_TERM_ACTION = """
term bad-term-action {
  protocol:: icmp
  action:: reject-with-tcp-rst
}
"""

GOOD_TERM_TCP = """
term good-term-tcp {
  comment:: "Test term 1"
  destination-address:: PROD_NETWORK
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

DENY_TERM_TCP = """
term deny-term-tcp {
  protocol:: tcp
  action:: deny
}
"""

GOOD_TERM_LOG = """
term good-term-log {
  protocol:: tcp
  logging:: true
  action:: accept
}
"""

EXPIRED_TERM = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRED_TERM2 = """
term expired_test2 {
  expiration:: 2015-01-01
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

NEXT_TERM = """
term next {
  action:: next
}
"""

NEXT_LOG_TERM = """
term next-log {
  logging:: true
  action:: next
}
"""

PORTRANGE_TERM = """
term portrange {
  protocol:: tcp
  action:: accept
  destination-port:: HIGH_PORTS
}
"""

FLAGS_TERM = """
term flags {
  protocol:: tcp
  action:: accept
  option:: syn fin
}
"""

INVALID_FLAGS_TERM = """
term invalid-flags {
  protocol:: udp
  action:: accept
  option:: syn fin
}
"""

MULTILINE_COMMENT = """
term multiline-comment {
  comment:: "This is a
multiline comment"
  protocol:: tcp
  action:: accept
}
"""

TCP_STATE_TERM = """
term tcp-established-only {
  protocol:: tcp
  option:: established
  action:: accept
}
"""

TCP_GOOD_ESTABLISHED_TERM = """
term tcp-established-good {
  protocol:: tcp
  option:: established
  action:: accept
}
"""

TCP_BAD_ESTABLISHED_TERM = """
term tcp-established-bad {
  protocol:: tcp
  option:: established syn
  action:: accept
}
"""

UDP_ESTABLISHED_TERM = """
term udp-established {
  protocol:: udp
  option:: established
  action:: accept
}
"""

MULTIPLE_NAME_TERM = """
term multiple-name {
  protocol:: tcp
  destination-address:: PROD_NETWORK
  destination-port:: SMTP
  source-address:: CORP_INTERNAL
  action:: accept
}
"""

LONG_NAME_TERM_DNS_TCP = """
term multiple-name-dns-tcp {
  protocol:: tcp
  destination-address:: PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME
  destination-port:: DNS
  action:: accept
}
"""

LONG_NAME_TERM_DNS_UDP = """
term multiple-name-dns-udp {
  protocol:: udp
  destination-address:: PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME
  destination-port:: DNS
  action:: accept
}
"""

NON_SHORTENED_LONG_NAME_TERM_DNS_UDP = """
term multiple-name-dns-udp {
  protocol:: udp
  destination-address:: PROD_NETWORK_EXTREAMLY_LONG_VER
  destination-port:: DNS
  action:: accept
}
"""

DUPLICATE_DIFFERENT_LONG_NAME_TERM = """
term multiple-name {
  protocol:: tcp
  destination-address:: PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME
  destination-port:: SMTP
  source-address:: PROD_NETWORK_EXTREAMLY_LONG_VERY_GOOD_NAME
  action:: accept
}
"""

BAD_PROTO_TERM = """
term bad-proto {
  protocol:: hopopt
  action:: accept
}
"""

GOOD_WARNING_TERM = """
term good-warning {
  protocol:: tcp
  policer:: batman
  action:: accept
}
"""

SOURCE_INTERFACE_TERM = """
term src-intf {
  source-interface:: lo0
  action:: accept
}
"""

DESTINATION_INTERFACE_TERM = """
term dest-intf {
  destination-interface:: lo0
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'destination_interface',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'source_interface',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'syn',
               'ack',
               'fin',
               'rst',
               'urg',
               'psh',
               'all',
               'established',
               'tcp-established'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class PacketFilterTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testTcp(self):
    ip = nacaddr.IP('10.0.0.0/8')
    ip.parent_token = 'PROD_NETWORK'
    self.naming.GetNetAddr.return_value = [ip]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-tcp', result,
                  'did not find comment for good-term-tcp')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { <PROD_NETWORK> } port '
        '{ 25 }', result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWORK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testLog(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_LOG, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-log', result,
                  'did not find comment for good-term-log')
    self.assertIn(
        'pass quick log proto { tcp } from { any } to { any } flags S/SA '
        'keep state\n', result,
        'did not find actual term for good-term-log')

  def testIcmp(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-icmp', result,
                  'did not find comment for good-term-icmp')
    self.assertIn(
        'pass quick proto { icmp } from { any } to { any } keep state\n',
        result, 'did not find actual term for good-term-icmp')

  def testIcmpTypes(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMP_TYPES, self.naming), EXP_INFO)
    acl_v6_header = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_INET6 + GOOD_TERM_ICMP_TYPES, self.naming), EXP_INFO)
    result = str(acl)
    result_v6_header = str(acl_v6_header)
    self.assertIn('# term good-term-icmp-types', result,
                  'did not find comment for good-term-icmp-types')
    self.assertIn('# term good-term-icmp-types', result_v6_header,
                  'did not find comment for good-term-icmp-types'
                  'in V6 header')
    self.assertIn(
        'block drop quick proto { icmp } from { any } to { any } '
        'icmp-type { 0, 3, 11 }', result,
        'did not find actual term for good-term-icmp-types')
    self.assertIn(
        'block drop quick proto { icmp } from { any } to { any } '
        'icmp-type { 0, 3, 11 }', result,
        'did not find actual term for good-term-icmp-types'
        'in the acl with V6 header')

  def testIcmpv6(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMPV6, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-icmpv6', result,
                  'did not find comment for good-term-icmpv6')
    self.assertIn(
        'pass quick proto { ipv6-icmp } from { any } to { any } keep state\n',
        result,
        'did not find actual term for good-term-icmpv6')

  def testIcmpv6Types(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMPV6_TYPES, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-icmpv6-types', result,
                  'did not find comment for good-term-icmpv6-types')
    self.assertIn(
        'block drop quick proto { ipv6-icmp } from { any } to { any } '
        'icmp6-type { 129 }', result,
        'did not find actual term for good-term-icmpv6-types')

  def testBadIcmp(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + BAD_TERM_ICMP, self.naming), EXP_INFO)
    self.assertRaises(aclgenerator.UnsupportedFilterError, str, acl)

  @mock.patch.object(packetfilter.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + EXPIRED_TERM, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and '
        'will not be rendered.', 'expired_test',
        'test-filter')

  @mock.patch.object(packetfilter.logging, 'warning')
  def testExpiredTerm2(self, mock_warn):
    packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + EXPIRED_TERM2, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and '
        'will not be rendered.', 'expired_test2',
        'test-filter')

  @mock.patch.object(packetfilter.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + EXPIRING_TERM % exp_date.strftime('%Y-%m-%d'),
        self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring',
        'test-filter')

  def testMultiprotocol(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + MULTIPLE_PROTOCOLS_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term multi-proto', result,
                  'did not find comment for multi-proto')
    self.assertIn(
        'pass quick proto { tcp udp icmp } from { any } '
        'to { any } keep state\n',
        result,
        'did not find actual term for multi-proto')

  def testNextTerm(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + NEXT_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term next', result,
                  'did not find comment for next')
    self.assertIn(
        'pass from { any } to { any } flags S/SA keep state\n', result,
        'did not find actual term for next-term')

  def testNextLogTerm(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + NEXT_LOG_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term next-log', result,
                  'did not find comment for next-log')
    self.assertIn(
        'pass log from { any } to { any } flags S/SA keep state\n', result,
        'did not find actual term for next-log-term')

  def testPortRange(self):
    self.naming.GetServiceByProto.return_value = ['12345-12354']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + PORTRANGE_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term portrange', result,
                  'did not find comment for portrange')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { any } '
        'port { 12345:12354 }', result,
        'did not find actual term for portrange')

    self.naming.GetServiceByProto.assert_called_once_with(
        'HIGH_PORTS', 'tcp')

  def testFlags(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + FLAGS_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term flags', result,
                  'did not find comment for flags')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { any } '
        'flags SF/SF', result,
        'did not find actual term for flags')

  def testInvalidFlags(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + INVALID_FLAGS_TERM, self.naming), EXP_INFO)
    self.assertRaises(aclgenerator.UnsupportedFilterError, str, acl)

  def testMultilineComment(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + MULTILINE_COMMENT, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term multiline-comment', result,
                  'did not find comment for multiline-comment')
    self.assertIn('# This is a\n# multiline comment', result,
                  'did not find multiline comment for multiline-comment')

  def testStateless(self):
    ip = nacaddr.IP('10.0.0.0/8')
    ip.parent_token = 'PROD_NETWORK'
    self.naming.GetNetAddr.return_value = [ip]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_STATELESS + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-tcp', result,
                  'did not find comment for good-term-tcp')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { <PROD_NETWORK> } port '
        '{ 25 } no state', result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWORK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testInet4(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_INET4 + GOOD_TERM_LOG, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-log', result,
                  'did not find comment for good-term-log')
    self.assertIn(
        'pass quick log inet proto { tcp } from { any } to { any } flags S/SA '
        'keep state\n', result,
        'did not find actual term for good-term-log')

  def testInet6(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_INET6 + GOOD_TERM_LOG, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-log', result,
                  'did not find comment for good-term-log')
    self.assertIn(
        'pass quick log inet6 proto { tcp } from { any } to { any } flags S/SA '
        'keep state\n', result,
        'did not find actual term for good-term-log')

  def testDirectional(self):
    ip = nacaddr.IP('10.0.0.0/8')
    ip.parent_token = 'PROD_NETWORK'
    self.naming.GetNetAddr.return_value = [ip]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_DIRECTIONAL + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-tcp', result,
                  'did not find comment for good-term-tcp')
    self.assertIn(
        'pass out quick proto { tcp } from { any } to { <PROD_NETWORK> } port '
        '{ 25 }', result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWORK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testDirectionalSourceInterface(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_DIRECTIONAL + SOURCE_INTERFACE_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term src-intf', result,
                  'did not find comment for src-intf')
    self.assertIn(
        'pass in quick on lo0 from { any } to { any } flags S/SA keep state',
        result,
        'did not find actual term for src-intf')

  def testDirectionalDestinationInterface(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_DIRECTIONAL + DESTINATION_INTERFACE_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term dest-intf', result,
                  'did not find comment for dest-intf')
    self.assertIn(
        'pass out quick on lo0 from { any } to { any } flags S/SA keep state',
        result,
        'did not find actual term for dest-intf')

  def testMultipleHeader(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_STATELESS + GOOD_TERM_LOG + GOOD_HEADER_INET6
        + GOOD_TERM_ICMP,
        self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'pass quick log proto { tcp } from { any } to { any } no state',
        result,
        'did not find actual term for good-term-log')
    self.assertIn(
        'pass quick inet6 proto { icmp } from { any } to { any } no state',
        result,
        'did not find actual term for good-term-icmp')

  def testDirectionalStateless(self):
    ip = nacaddr.IP('10.0.0.0/8')
    ip.parent_token = 'PROD_NETWORK'
    self.naming.GetNetAddr.return_value = [ip]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_DIRECTIONAL_STATELESS + GOOD_TERM_TCP, self.naming),
                                    EXP_INFO)
    result = str(acl)
    self.assertIn('# term good-term-tcp', result,
                  'did not find comment for good-term-tcp')
    self.assertIn(
        'pass out quick proto { tcp } from { any } to { <PROD_NETWORK> } port '
        '{ 25 } no state', result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWORK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testStatelessEstablished(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_STATELESS + TCP_STATE_TERM, self.naming),
                                    EXP_INFO)
    result = str(acl)
    self.assertIn('# term tcp-established-only', result,
                  'did not find comment for tcp-established-only')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { any } flags A/A no state',
        result,
        'did not find actual term for tcp-established-only')

  def testBadFlags(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + TCP_BAD_ESTABLISHED_TERM, self.naming), EXP_INFO)
    self.assertRaises(aclgenerator.UnsupportedFilterError, str, acl)

  # While "UDP stateless established" seems to be a strange combination it
  # actually makes sense:  e.g., the state or nostate header is a global
  # header directive and indicates whether we do matching on established by
  # flags or proper connection tracking, and pf's permissiveness allows things
  # like:
  #   proto { udp, tcp } flags A/A no state'
  # whereby the flags only apply to TCP protocol matches.  However, the
  # following is invalid:
  #   proto { udp } flags A/A no state'
  # check to make sure we don't output the latter for things like:
  #   target:: packetfilter nostate
  #   term foo { protocol:: udp option:: established }
  def testUdpStatelessEstablished(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_STATELESS + UDP_ESTABLISHED_TERM, self.naming),
                                    EXP_INFO)
    result = str(acl)
    self.assertIn('# term udp-established', result,
                  'did not find comment for udp-established')
    self.assertIn(
        'pass quick proto { udp } from { any } to { any } no state',
        result,
        'did not find actual term for udp-established')

  def testStatefulBlock(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + DENY_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('# term deny-term-tcp', result,
                  'did not find comment for udp-established')
    self.assertIn(
        'block drop quick proto { tcp } from { any } to { any } flags S/SA',
        result,
        'did not find actual term for deny-term-tcp')

  def testTcpEstablished(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + TCP_GOOD_ESTABLISHED_TERM, self.naming),
                                    EXP_INFO)
    result = str(acl)
    self.assertIn('# term tcp-established-good', result,
                  'did not find comment for tcp-established-good')
    self.assertIn(
        'pass quick proto { tcp } from { any } to { any } flags A/A keep state',
        result,
        'did not find actual term for udp-established')

  def testTableCreation(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK'
    corp_internal_one = nacaddr.IP('100.96.0.1/11', strict=False)
    corp_internal_one.parent_token = 'CORP_INTERNAL'
    corp_internal_two = nacaddr.IP('172.16.0.0/16')
    corp_internal_two.parent_token = 'CORP_INTERNAL'
    self.naming.GetNetAddr.side_effect = [
        [prod_network],
        [corp_internal_one, corp_internal_two]]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + MULTIPLE_NAME_TERM, self.naming),
                                    EXP_INFO)
    result = str(acl)
    self.assertIn(
        'table <PROD_NETWORK> {10.0.0.0/8}', result,
        'did not find PROD_NETWORKtable in header')
    self.assertIn(
        'table <CORP_INTERNAL> {100.96.0.0/11,\\\n'
        '172.16.0.0/16}', result,
        'did not find CORP_INTERNAL table in header')
    self.assertIn(
        'pass quick proto { tcp } from { <CORP_INTERNAL> } to '
        '{ <PROD_NETWORK> } port { 25 } flags S/SA keep state', result,
        'did not find actual term for multiple-name')

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK'),
        mock.call('CORP_INTERNAL')])
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testTableNameShortened(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    self.naming.GetNetAddr.return_value = [prod_network]
    self.naming.GetServiceByProto.return_value = ['53']

    acl = packetfilter.PacketFilter(
        policy.ParsePolicy(GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_TCP,
                           self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'table <PROD_NETWORK_EXTREAMLY_LONG_VER> {10.0.0.0/8}', result,
        'did not find shortened name in header.')
    self.assertIn(
        'pass out quick proto { tcp } from { any } to '
        '{ <PROD_NETWORK_EXTREAMLY_LONG_VER> } '
        'port { 53 } flags S/SA keep state', result,
        'did not find actual term for multiple-name')

    self.naming.GetNetAddr.assert_called_once_with(
        'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME')
    self.naming.GetServiceByProto.assert_called_once_with('DNS', 'tcp')

  def testTableDuplicateShortNameError(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    prod_network_two = nacaddr.IP('172.0.0.0/8')
    prod_network_two.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_GOOD_NAME'
    self.naming.GetNetAddr.side_effect = [
        [prod_network], [prod_network_two]]
    self.naming.GetServiceByProto.return_value = ['25']

    self.assertRaises(
        packetfilter.DuplicateShortenedTableNameError,
        packetfilter.PacketFilter.__init__,
        packetfilter.PacketFilter.__new__(packetfilter.PacketFilter),
        policy.ParsePolicy(
            GOOD_HEADER_DIRECTIONAL + DUPLICATE_DIFFERENT_LONG_NAME_TERM,
            self.naming),
        EXP_INFO)
    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'),
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_GOOD_NAME')])
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testTableSameLongNameSameFilter(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    self.naming.GetNetAddr.return_value = [prod_network]
    self.naming.GetServiceByProto.return_value = ['53']

    acl = packetfilter.PacketFilter(
        policy.ParsePolicy(
            GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_TCP +
            LONG_NAME_TERM_DNS_UDP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'table <PROD_NETWORK_EXTREAMLY_LONG_VER> {10.0.0.0/8}', result,
        'did not find shortened name in header.')
    self.assertIn(
        'pass out quick proto { tcp } from { any } to '
        '{ <PROD_NETWORK_EXTREAMLY_LONG_VER> } '
        'port { 53 } flags S/SA keep state', result,
        'did not find actual TCP term for multiple-name')
    self.assertIn(
        'pass out quick proto { udp } from { any } to '
        '{ <PROD_NETWORK_EXTREAMLY_LONG_VER> } '
        'port { 53 } keep state', result,
        'did not find actual UDP for multiple-name')

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'),
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME')])
    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'),
        mock.call('DNS', 'udp')])

  def testTableSameLongNameDiffFilter(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    self.naming.GetNetAddr.return_value = [prod_network]
    self.naming.GetServiceByProto.return_value = ['53']

    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_TCP +
        GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_UDP,
        self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'table <PROD_NETWORK_EXTREAMLY_LONG_VER> {10.0.0.0/8}', result,
        'did not find shortened name in header.')
    self.assertIn(
        'pass out quick proto { tcp } from { any } to '
        '{ <PROD_NETWORK_EXTREAMLY_LONG_VER> } '
        'port { 53 } flags S/SA keep state', result,
        'did not find actual TCP term for multiple-name')
    self.assertIn(
        'pass out quick proto { udp } from { any } to '
        '{ <PROD_NETWORK_EXTREAMLY_LONG_VER> } '
        'port { 53 } keep state', result,
        'did not find actual UDP for multiple-name')

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'),
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME')])
    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'),
        mock.call('DNS', 'udp')])

  def testTableDiffObjectsShortenedAndNonShortened(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    prod_network_two = nacaddr.IP('172.0.0.0/8')
    prod_network_two.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VER'
    self.naming.GetNetAddr.side_effect = [
        [prod_network], [prod_network_two]]
    self.naming.GetServiceByProto.return_value = ['53']

    self.assertRaises(
        packetfilter.DuplicateShortenedTableNameError,
        packetfilter.PacketFilter.__init__,
        packetfilter.PacketFilter.__new__(packetfilter.PacketFilter),
        policy.ParsePolicy(
            GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_TCP +
            NON_SHORTENED_LONG_NAME_TERM_DNS_UDP,
            self.naming),
        EXP_INFO)
    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'),
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VER')])
    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'),
        mock.call('DNS', 'udp')])

  def testTableDuplicateShortNameErrorDiffFilter(self):
    prod_network = nacaddr.IP('10.0.0.0/8')
    prod_network.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'
    prod_network_two = nacaddr.IP('172.0.0.0/8')
    prod_network_two.parent_token = 'PROD_NETWORK_EXTREAMLY_LONG_VER'
    self.naming.GetNetAddr.side_effect = [
        [prod_network], [prod_network_two]]
    self.naming.GetServiceByProto.return_value = ['53']

    self.assertRaises(
        packetfilter.DuplicateShortenedTableNameError,
        packetfilter.PacketFilter.__init__,
        packetfilter.PacketFilter.__new__(packetfilter.PacketFilter),
        policy.ParsePolicy(
            GOOD_HEADER_DIRECTIONAL + LONG_NAME_TERM_DNS_TCP +
            GOOD_HEADER_DIRECTIONAL + NON_SHORTENED_LONG_NAME_TERM_DNS_UDP,
            self.naming),
        EXP_INFO)
    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VERY_NO_GOOD_NAME'),
        mock.call('PROD_NETWORK_EXTREAMLY_LONG_VER')])
    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'),
        mock.call('DNS', 'udp')])

  def testTermNameConflict(self):
    self.assertRaises(
        packetfilter.DuplicateTermError,
        packetfilter.PacketFilter.__init__,
        packetfilter.PacketFilter.__new__(packetfilter.PacketFilter),
        policy.ParsePolicy(
            GOOD_HEADER_DIRECTIONAL + GOOD_TERM_ICMP + GOOD_TERM_ICMP,
            self.naming),
        EXP_INFO)

  def testBadProtoError(self):
    acl = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + BAD_PROTO_TERM, self.naming), EXP_INFO)
    self.assertRaises(packetfilter.UnsupportedProtoError, str, acl)

  def testBuildTokens(self):
    ip = nacaddr.IP('10.0.0.0/8')
    ip.parent_token = 'PROD_NETWORK'
    self.naming.GetNetAddr.return_value = [ip]
    self.naming.GetServiceByProto.return_value = ['25']

    pol1 = packetfilter.PacketFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_TCP, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = packetfilter.PacketFilter(
        policy.ParsePolicy(GOOD_HEADER + GOOD_WARNING_TERM,
                           self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


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


# tests/lib/paloaltofw_test.py

# Copyright 2012 Google Inc. All Rights Reserved.
#
# 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.
"""Unit test for Palo Alto Firewalls acl rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import paloaltofw
from capirca.lib import policy

GOOD_HEADER_1 = """
header {
  comment:: "This is a test acl with a comment"
  target:: paloalto from-zone trust to-zone untrust
}
"""

GOOD_HEADER_2 = """
header {
  comment:: "This is a test acl with a comment"
  target:: paloalto from-zone all to-zone all
}
"""

GOOD_HEADER_INET6 = """
header {
  comment:: "This is a test acl with a comment"
  target:: paloalto from-zone trust to-zone untrust inet6
}
"""

GOOD_HEADER_MIXED = """
header {
  comment:: "This is a test acl with a comment"
  target:: paloalto from-zone trust to-zone untrust mixed
}
"""

GOOD_HEADER_TERM_PREFIXES_1 = """
header {
  comment:: "This is a test acl with a comment"
  target:: paloalto from-zone trust to-zone untrust mixed no-addr-obj unique-term-prefixes
}
"""

GOOD_HEADER_TERM_PREFIXES_2 = """
header {
  comment:: "This is a test acl with a comment"
  target:: paloalto from-zone trust to-zone trust mixed no-addr-obj unique-term-prefixes
}
"""

BAD_HEADER_1 = """
header {
  comment:: "This header has two address families"
  target:: paloalto from-zone trust to-zone untrust inet6 mixed
}
"""

GRE_PROTO_TERM = """
term test-gre-protocol {
  comment:: "allow GRE protocol to FOOBAR"
  destination-address:: FOOBAR
  protocol:: gre
  action:: accept
}
"""

AH_PROTO_TERM = """
term test-ah-protocol {
  comment:: "allow AH protocol to FOOBAR"
  destination-address:: FOOBAR
  protocol:: ah
  action:: accept
}
"""

AH_TCP_MIXED_PROTO_TERM = """
term test-mixed-protocol {
  source-address:: FOOBAR
  protocol:: ah tcp
  action:: accept
  comment:: "Applications and Services should be split into separate terms."
}
"""

ESP_PROTO_TERM = """
term test-esp-protocol {
  comment:: "allow ESP protocol to FOOBAR"
  destination-address:: FOOBAR
  protocol:: esp
  action:: accept
}
"""

ESP_TCP_MIXED_PROTO_TERM = """
term test-mixed-protocol {
  destination-address:: FOOBAR
  protocol:: esp tcp
  action:: accept
  comment:: "Applications and Services should be split into separate terms."
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  comment:: "This header is very very very very very very very very very very very very very very very very very very very very large"
  destination-address:: FOOBAR
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}

"""

GOOD_TERM_2 = """
term good-term-4 {
  destination-address:: SOME_HOST
  protocol:: tcp
  pan-application:: ssl http
  action:: accept
}
"""

GOOD_TERM_3 = """
term only-pan-app {
  pan-application:: ssl
  action:: accept
}
"""

GOOD_TERM_4_STATELESS_REPLY = """
term good-term-stateless-reply {
  comment:: "ThisIsAStatelessReply"
  destination-address:: SOME_HOST
  protocol:: tcp
  pan-application:: ssl http
  action:: accept
}
"""

SVC_TERM_1 = """
term ssh-term-1 {
  comment:: "Allow SSH"
  destination-address:: FOOBAR
  destination-port:: SSH
  protocol:: tcp
  action:: accept
}

term smtp-term-1 {
  comment:: "Allow SMTP"
  destination-address:: FOOBAR
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

SVC_TERM_2 = """
term smtp-term-1 {
  comment:: "Allow SMTP"
  destination-address:: FOOBAR
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

TCP_ESTABLISHED_TERM = """
term tcp-established {
  destination-address:: SOME_HOST
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""

UDP_ESTABLISHED_TERM = """
term udp-established-term {
  destination-address:: SOME_HOST
  protocol:: udp
  option:: established
  action:: accept
}
"""

UNSUPPORTED_OPTION_TERM = """
term unsupported-option-term {
  destination-address:: SOME_HOST
  protocol:: udp
  option:: inactive
  action:: accept
}
"""

EXPIRED_TERM_1 = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

ICMP_TYPE_TERM_1 = """
term test-icmp {
  protocol:: icmp
  icmp-type:: echo-request echo-reply unreachable
  action:: accept
}
"""

ICMP_TYPE_TERM_2 = """
term test-icmp-2 {
  protocol:: icmp
  icmp-type:: echo-request echo-reply unreachable
  action:: accept
}
"""

ICMPV6_ONLY_TERM = """
term test-icmpv6-only {
  protocol:: icmpv6
  action:: accept
}
"""

ICMPV6_TYPE_TERM = """
term test-icmpv6-types {
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply destination-unreachable
  action:: accept
}
"""

ICMPV6_ABBREVIATION_TYPE_TERM = """
term test-icmpv6-abbreviation-types {
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply destination-unreachable inverse-neighbor-discovery-solicitation inverse-neighbor-discovery-advertisement version-2-multicast-listener-report home-agent-address-discovery-reply
  action:: accept
}
"""

BAD_ICMPV6_TYPE_TERM = """
term test-icmp {
  protocol:: icmpv6
  icmp-type:: echo-request echo-reply unreachable
  action:: accept
  comment:: "This is incorrect because unreachable is not an icmpv6-type."
}
"""

ICMP_ONLY_TERM_1 = """
term test-icmp-only {
  protocol:: icmp
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

DEFAULT_TERM_1 = """
term default-term-1 {
  action:: deny
}
"""

TIMEOUT_TERM = """
term timeout-term {
  protocol:: icmp
  icmp-type:: echo-request
  timeout:: 77
  action:: accept
}
"""

LOGGING_DISABLED = """
term test-disabled-log {
  comment:: "Testing disabling logging for tcp."
  protocol:: tcp
  logging:: disable
  action:: accept
}
"""

LOGGING_BOTH_TERM = """
term test-log-both {
  comment:: "Testing enabling log-both for tcp."
  protocol:: tcp
  logging:: log-both
  action:: accept
}
"""

LOGGING_TRUE_KEYWORD = """
term test-true-log {
  comment:: "Testing enabling logging for udp with true keyword."
  protocol:: udp
  logging:: true
  action:: accept
}
"""

LOGGING_PYTRUE_KEYWORD = """
term test-pytrue-log {
  comment:: "Testing enabling logging for udp with True keyword."
  protocol:: udp
  logging:: True
  action:: accept
}
"""

LOGGING_SYSLOG_KEYWORD = """
term test-syslog-log {
  comment:: "Testing enabling logging for udp with syslog keyword."
  protocol:: udp
  logging:: syslog
  action:: accept
}
"""

LOGGING_LOCAL_KEYWORD = """
term test-local-log {
  comment:: "Testing enabling logging for udp with local keyword."
  protocol:: udp
  logging:: local
  action:: accept
}
"""

ACTION_ACCEPT_TERM = """
term test-accept-action {
  comment:: "Testing accept action for tcp."
  protocol:: tcp
  action:: accept
}
"""

ACTION_COUNT_TERM = """
term test-count-action {
  comment:: "Testing unsupported count action for tcp."
  protocol:: tcp
  action:: count
}
"""

ACTION_NEXT_TERM = """
term test-next-action {
  comment:: "Testing unsupported next action for tcp."
  protocol:: tcp
  action:: next
}
"""

ACTION_DENY_TERM = """
term test-deny-action {
  comment:: "Testing deny action for tcp."
  protocol:: tcp
  action:: deny
}
"""

ACTION_REJECT_TERM = """
term test-reject-action {
  comment:: "Testing reject action for tcp."
  protocol:: tcp
  action:: reject
}
"""

ACTION_RESET_TERM = """
term test-reset-action {
  comment:: "Testing reset action for tcp."
  protocol:: tcp
  action:: reject-with-tcp-rst
}
"""

PLATFORM_TERM = """
term test-accept-action {
  comment:: "Testing accept action for tcp."
  protocol:: tcp
  action:: accept
  platform:: paloalto
}
"""

OTHER_PLATFORM_TERM = """
term test-accept-action {
  comment:: "Testing accept action for tcp."
  protocol:: tcp
  action:: accept
  platform:: juniper
}
"""

PLATFORM_EXCLUDE_TERM = """
term test-accept-action {
  comment:: "Testing accept action for tcp."
  protocol:: tcp
  action:: accept
  platform-exclude:: paloalto
}
"""

OTHER_PLATFORM_EXCLUDE_TERM = """
term test-accept-action {
  comment:: "Testing accept action for tcp."
  protocol:: tcp
  action:: accept
  platform-exclude:: junipersrx
}
"""

HEADER_COMMENTS = """
header {
  comment:: "comment 1"
  comment:: "comment 2"
  target:: paloalto from-zone trust to-zone untrust
}
term policy-1 {
  pan-application:: ssh
  action:: accept
}
term policy-2 {
  pan-application:: web-browsing
  action:: accept
}
header {
  comment:: "comment 3"
  target:: paloalto from-zone trust to-zone dmz
}
term policy-3 {
  pan-application:: web-browsing
  action:: accept
}
header {
  # no comment
  target:: paloalto from-zone trust to-zone dmz-2
}
term policy-4 {
  pan-application:: web-browsing
  action:: accept
}
"""

ZONE_LEN_ERROR = """
header {
  target:: paloalto from-zone %s to-zone %s
}
term policy {
  pan-application:: web-browsing
  action:: accept
}
"""

SUPPORTED_TOKENS = frozenset({
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_type',
    'logging',
    'name',
    'option',
    'owner',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'stateless_reply',
    'timeout',
    'pan_application',
    'translated',
})

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'reject-with-tcp-rst'},
    'option': {'established', 'tcp-established'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2

_IPSET = [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('2001:4860:8000::/33')]
_IPSET2 = [nacaddr.IP('10.23.0.0/22'), nacaddr.IP('10.23.0.6/23', strict=False)]
_IPSET3 = [nacaddr.IP('10.23.0.0/23')]

PATH_VSYS = "./devices/entry[@name='localhost.localdomain']/vsys/entry[@name='vsys1']"
PATH_RULES = PATH_VSYS + '/rulebase/security/rules'
PATH_TAG = PATH_VSYS + '/tag'
PATH_SERVICE = PATH_VSYS + '/service'
PATH_ADDRESSES = PATH_VSYS + '/address'
PATH_ADDRESS_GROUP = PATH_VSYS + '/address-group'


class PaloAltoFWTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testTermAndFilterName(self):
    self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['25']

    paloalto = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1, self.naming), EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES + "/entry[@name='good-term-1']")
    self.assertIsNotNone(x, output)

    self.naming.GetNetAddr.assert_called_once_with('FOOBAR')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testServiceMap(self):
    definitions = naming.Naming()
    definitions._ParseLine('SSH = 22/tcp', 'services')
    definitions._ParseLine('SMTP = 25/tcp', 'services')
    definitions._ParseLine('FOOBAR = 10.0.0.0/8', 'networks')
    definitions._ParseLine('         2001:4860:8000::/33', 'networks')

    pol1 = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + SVC_TERM_1, definitions), EXP_INFO)
    self.assertEqual(
        pol1.service_map.entries, {
            ((), ('22',), 'tcp'): {
                'name': 'service-ssh-term-1-tcp'
            },
            ((), ('25',), 'tcp'): {
                'name': 'service-smtp-term-1-tcp'
            }
        }, pol1.service_map.entries)

    pol2 = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + SVC_TERM_2, definitions), EXP_INFO)
    # The expectation is that there will be a single port mapped.
    self.assertEqual(
        pol2.service_map.entries, {
            ((), ('25',), 'tcp'): {
                'name': 'service-smtp-term-1-tcp'
            }
        }, pol2.service_map.entries)

  def testDefaultDeny(self):
    paloalto = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + DEFAULT_TERM_1, self.naming),
        EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='default-term-1']/action")
    self.assertIsNotNone(x, output)
    self.assertEqual(x.text, 'deny', output)

  def testIcmpTypes(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ICMP_TYPE_TERM_1, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmp']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertCountEqual(
        ['icmp-echo-reply', 'icmp-echo-request', 'icmp-unreachable'], members,
        output)

  def testIcmpTypesMultiplePolicies(self):
    pol = policy.ParsePolicy(
        GOOD_HEADER_1 + ICMP_TYPE_TERM_1 + GOOD_HEADER_2 + ICMP_TYPE_TERM_2,
        self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmp']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertCountEqual(
        ['icmp-echo-reply', 'icmp-echo-request', 'icmp-unreachable'], members,
        output)

    # Check second policy as well.
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmp-2']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertCountEqual(
        ['icmp-echo-reply', 'icmp-echo-request', 'icmp-unreachable'], members,
        output)

  def testIcmpV6Types(self):
    pol = policy.ParsePolicy(GOOD_HEADER_MIXED + ICMPV6_TYPE_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmpv6-types']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertCountEqual([
        'icmp6-echo-reply', 'icmp6-echo-request',
        'icmp6-destination-unreachable'
    ], members, output)

  def testIcmpV6ApplicationAbbreviation(self):
    pol = policy.ParsePolicy(GOOD_HEADER_MIXED + ICMPV6_ABBREVIATION_TYPE_TERM,
                             self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(
        PATH_RULES +
        "/entry[@name='test-icmpv6-abbreviation-types']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertCountEqual([
        'icmp6-echo-reply', 'icmp6-echo-request',
        'icmp6-destination-unreachable', 'icmp6-INV-NBR-DSCVR-SOL',
        'icmp6-INV-NBR-DSCVR-ADV', 'icmp6-version-2-MCAST-LSNR-repo',
        'icmp6-home-agent-ADDR-DSCVR-RPL'
    ], members, output)

  def testBadICMP(self):
    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust %s
}
term rule-1 {
%s
  action:: accept
}"""

    T = """
  icmp-type:: echo-request echo-reply
"""

    pol = policy.ParsePolicy(POL % ("", T), self.naming)
    self.assertRaises(paloaltofw.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

    T = """
  protocol:: udp icmp
  icmp-type:: echo-request echo-reply
"""

    pol = policy.ParsePolicy(POL % ("inet", T), self.naming)
    self.assertRaises(paloaltofw.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

    T = """
  protocol:: icmpv6 icmp
  icmp-type:: echo-request
"""

    pol = policy.ParsePolicy(POL % ("mixed", T), self.naming)
    self.assertRaises(paloaltofw.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

    T = """
  protocol:: icmpv6
  icmp-type:: echo echo-reply
"""
    self.assertRaises(policy.TermInvalidIcmpType,
                      policy.ParsePolicy, POL % ("inet6", T), self.naming)

  def testBadICMPv6Type(self):
    pol = policy.ParsePolicy(GOOD_HEADER_MIXED + BAD_ICMPV6_TYPE_TERM,
                             self.naming)
    self.assertRaises(paloaltofw.PaloAltoFWBadIcmpTypeError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testICMPProtocolOnly(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ICMP_ONLY_TERM_1, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmp-only']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertEqual(['icmp'], members, output)

  def testICMPv6ProtocolOnly(self):
    pol = policy.ParsePolicy(GOOD_HEADER_INET6 + ICMPV6_ONLY_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-icmpv6-only']/application")
    self.assertIsNotNone(x, output)
    members = []
    for node in x:
      self.assertEqual(node.tag, 'member', output)
      members.append(node.text)

    self.assertEqual(['ipv6-icmp'], members, output)

  def testUniqueTermsMultiplePolicies(self):
    pol = policy.ParsePolicy(
        GOOD_HEADER_TERM_PREFIXES_1 + ICMP_TYPE_TERM_1 +
        GOOD_HEADER_TERM_PREFIXES_2 + ICMP_TYPE_TERM_1, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(
        PATH_RULES + "/entry[@name='a5f554bb7a8276615edbd5de-test-icmp']")
    self.assertIsNotNone(x, output)

    # Check second policy as well.
    x = paloalto.config.find(
        PATH_RULES + "/entry[@name='e7d22ea748e04110eaf0495e-test-icmp']")
    self.assertIsNotNone(x, output)

  def testSkipStatelessReply(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_4_STATELESS_REPLY,
                             self.naming)

    # Add stateless_reply to terms, there is no current way to include it in the
    # term definition.
    _, terms = pol.filters[0]
    for term in terms:
      term.stateless_reply = True

    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='good-term-stateless-reply']")
    self.assertIsNone(x, output)

  def testSkipEstablished(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + TCP_ESTABLISHED_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES + "/entry[@name='tcp-established']")
    self.assertIsNone(x, output)

    pol = policy.ParsePolicy(GOOD_HEADER_1 + UDP_ESTABLISHED_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='udp-established-term']")
    self.assertIsNone(x, output)

  def testUnsupportedOptions(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + UNSUPPORTED_OPTION_TERM,
                             self.naming)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testBuildTokens(self):
    self.naming.GetServiceByProto.side_effect = [['25'], ['26']]
    pol1 = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testLoggingBoth(self):
    paloalto = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + LOGGING_BOTH_TERM, self.naming),
        EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-log-both']/log-start")
    self.assertEqual(x, 'yes', output)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-log-both']/log-end")
    self.assertEqual(x, 'yes', output)

  def testDisableLogging(self):
    paloalto = paloaltofw.PaloAltoFW(
        policy.ParsePolicy(GOOD_HEADER_1 + LOGGING_DISABLED, self.naming),
        EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-disabled-log']/log-start")
    self.assertEqual(x, 'no', output)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-disabled-log']/log-end")
    self.assertEqual(x, 'no', output)

  def testLogging(self):
    for term in [
        LOGGING_SYSLOG_KEYWORD, LOGGING_LOCAL_KEYWORD, LOGGING_PYTRUE_KEYWORD,
        LOGGING_TRUE_KEYWORD
    ]:
      paloalto = paloaltofw.PaloAltoFW(
          policy.ParsePolicy(GOOD_HEADER_1 + term, self.naming), EXP_INFO)
      output = str(paloalto)

      # we don't have term name so match all elements with attribute
      # name at the entry level
      x = paloalto.config.findall(PATH_RULES + '/entry[@name]/log-start')
      self.assertEqual(len(x), 0, output)
      x = paloalto.config.findall(PATH_RULES + '/entry[@name]/log-end')
      self.assertEqual(len(x), 1, output)
      self.assertEqual(x[0].text, 'yes', output)

  def testAcceptAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_ACCEPT_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-accept-action']/action")
    self.assertEqual(x, 'allow', output)

  def testDenyAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_DENY_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-deny-action']/action")
    self.assertEqual(x, 'deny', output)

  def testRejectAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_REJECT_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-reject-action']/action")
    self.assertEqual(x, 'reset-client', output)

  def testResetAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_RESET_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-reset-action']/action")
    self.assertEqual(x, 'reset-client', output)

  def testCountAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_COUNT_TERM, self.naming)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testNextAction(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ACTION_NEXT_TERM, self.naming)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testPlatformTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + PLATFORM_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-accept-action']/action")
    self.assertEqual(x, 'allow', output)

  def testOtherPlatformTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + OTHER_PLATFORM_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-accept-action']/action")
    self.assertIsNone(x, output)

  def testPlatformExcludeTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + PLATFORM_EXCLUDE_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-accept-action']/action")
    self.assertIsNone(x, output)

  def testOtherPlatformExcludeTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + OTHER_PLATFORM_EXCLUDE_TERM,
                             self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='test-accept-action']/action")
    self.assertEqual(x, 'allow', output)

  def testGreProtoTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + GRE_PROTO_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-gre-protocol']/application")
    self.assertIsNotNone(x, output)
    self.assertEqual(len(x), 1, output)
    self.assertEqual(x[0].tag, 'member', output)
    self.assertEqual(x[0].text, 'gre', output)

  def testAhProtoTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + AH_PROTO_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-ah-protocol']/application")
    self.assertIsNotNone(x, output)
    self.assertEqual(len(x), 1, output)
    self.assertEqual(x[0].tag, 'member', output)
    self.assertEqual(x[0].text, 'ipsec-ah', output)

  def testAhTcpMixedProtoTerm(self):
    pol = policy.ParsePolicy(
        GOOD_HEADER_1 + AH_TCP_MIXED_PROTO_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    svc = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-1']/service")
    self.assertIsNotNone(svc, output)
    self.assertEqual(len(svc), 1, output)
    self.assertEqual(svc[0].tag, 'member', output)
    self.assertEqual(svc[0].text, 'any-tcp', output)

    app = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-1']/application")
    self.assertIsNotNone(app, output)
    self.assertEqual(len(app), 1, output)
    self.assertEqual(app[0].tag, 'member', output)
    self.assertEqual(app[0].text, 'any', output)

    # Check second policy as well.
    svc = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-2']/service")
    self.assertIsNotNone(svc, output)
    self.assertEqual(len(svc), 1, output)
    self.assertEqual(svc[0].tag, 'member', output)
    self.assertEqual(svc[0].text, 'application-default', output)

    app = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-2']/application")
    self.assertIsNotNone(app, output)
    self.assertEqual(len(app), 1, output)
    self.assertEqual(app[0].tag, 'member', output)
    self.assertEqual(app[0].text, 'ipsec-ah', output)


  def testEspProtoTerm(self):
    pol = policy.ParsePolicy(GOOD_HEADER_1 + ESP_PROTO_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES +
                             "/entry[@name='test-esp-protocol']/application")
    self.assertIsNotNone(x, output)
    self.assertEqual(len(x), 1, output)
    self.assertEqual(x[0].tag, 'member', output)
    self.assertEqual(x[0].text, 'ipsec-esp', output)

  def testEspTcpMixedProtoTerm(self):
    pol = policy.ParsePolicy(
        GOOD_HEADER_1 + ESP_TCP_MIXED_PROTO_TERM, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    svc = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-1']/service")
    self.assertIsNotNone(svc, output)
    self.assertEqual(len(svc), 1, output)
    self.assertEqual(svc[0].tag, 'member', output)
    self.assertEqual(svc[0].text, 'any-tcp', output)

    app = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-1']/application")
    self.assertIsNotNone(app, output)
    self.assertEqual(len(app), 1, output)
    self.assertEqual(app[0].tag, 'member', output)
    self.assertEqual(app[0].text, 'any', output)

    # Check second policy as well.
    svc = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-2']/service")
    self.assertIsNotNone(svc, output)
    self.assertEqual(len(svc), 1, output)
    self.assertEqual(svc[0].tag, 'member', output)
    self.assertEqual(svc[0].text, 'application-default', output)

    app = paloalto.config.find(
        PATH_RULES + "/entry[@name='test-mixed-protocol-2']/application")
    self.assertIsNotNone(app, output)
    self.assertEqual(len(app), 1, output)
    self.assertEqual(app[0].tag, 'member', output)
    self.assertEqual(app[0].text, 'ipsec-esp', output)

  def testHeaderComments(self):
    pol = policy.ParsePolicy(HEADER_COMMENTS, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)

    tag = 'trust_untrust_policy-comment-1'
    x = paloalto.config.find(PATH_TAG + "/entry[@name='%s']/comments" % tag)
    self.assertIsNotNone(x, output)
    self.assertEqual(x.text, 'comment 1 comment 2', output)
    x = paloalto.config.find(PATH_RULES + "/entry[@name='policy-2']/tag")
    self.assertIsNotNone(x, output)
    self.assertEqual(len(x), 1, output)
    self.assertEqual(x[0].tag, 'member', output)
    self.assertEqual(x[0].text, tag, output)

    tag = 'trust_dmz_policy-comment-2'
    x = paloalto.config.find(PATH_TAG + "/entry[@name='%s']/comments" % tag)
    self.assertIsNotNone(x, output)
    self.assertEqual(x.text, 'comment 3', output)
    x = paloalto.config.find(PATH_RULES + "/entry[@name='policy-3']/tag")
    self.assertIsNotNone(x, output)
    self.assertEqual(len(x), 1, output)
    self.assertEqual(x[0].tag, 'member', output)
    self.assertEqual(x[0].text, tag, output)

    x = paloalto.config.find(PATH_RULES + "/entry[@name='policy-4']/tag")
    self.assertIsNone(x, output)

  def testZoneLen(self):
    ZONE_MAX_LEN = 'Z' * 31
    ZONE_TOO_LONG = 'Z' * 32

    # from
    pol = policy.ParsePolicy(ZONE_LEN_ERROR % (ZONE_MAX_LEN, 'dmz'),
                             self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='policy']/from/member")
    self.assertEqual(x, ZONE_MAX_LEN, output)

    pol = policy.ParsePolicy(ZONE_LEN_ERROR % (ZONE_TOO_LONG, 'dmz'),
                             self.naming)
    self.assertRaisesRegex(paloaltofw.PaloAltoFWNameTooLongError,
                           '^Source zone must be 31 characters max',
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

    # to
    pol = policy.ParsePolicy(ZONE_LEN_ERROR % ('dmz', ZONE_MAX_LEN),
                             self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='policy']/to/member")
    self.assertEqual(x, ZONE_MAX_LEN, output)

    pol = policy.ParsePolicy(ZONE_LEN_ERROR % ('dmz', ZONE_TOO_LONG),
                             self.naming)
    self.assertRaisesRegex(paloaltofw.PaloAltoFWNameTooLongError,
                           '^Destination zone must be 31 characters max',
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def test_ZonesRequired(self):
    BAD_HEADERS = [
        'header{target::paloalto}',
        'header{target::paloalto from-zone x}',
        'header{target::paloalto x x to-zone x}',
    ]

    msg = ('^Palo Alto Firewall filter arguments '
           'must specify from-zone and to-zone[.]$')
    for header in BAD_HEADERS:
      pol = policy.ParsePolicy(header + GOOD_TERM_3, self.naming)
      self.assertRaisesRegex(paloaltofw.UnsupportedFilterError, msg,
                             paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def test_LongComments(self):
    POL = """
header {
  comment:: "%s"
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  comment:: "%s"
  pan-application:: ssl
  action:: accept
}"""

    # get maximum lengths
    pol = policy.ParsePolicy(POL % ('C', 'C'), self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    MAX_TAG_COMMENTS_LENGTH = paloalto._MAX_TAG_COMMENTS_LENGTH
    MAX_RULE_DESCRIPTION_LENGTH = paloalto._MAX_RULE_DESCRIPTION_LENGTH

    tag = 'trust_untrust_policy-comment-1'

    # maximum length
    pol = policy.ParsePolicy(
        POL %
        ('C' * MAX_TAG_COMMENTS_LENGTH, 'C' * MAX_RULE_DESCRIPTION_LENGTH),
        self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)

    x = paloalto.config.findtext(PATH_TAG + "/entry[@name='%s']/comments" % tag)
    self.assertEqual(x, 'C' * MAX_TAG_COMMENTS_LENGTH, output)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/description")
    self.assertEqual(x, 'C' * MAX_RULE_DESCRIPTION_LENGTH, output)

    # maximum length + 1
    pol = policy.ParsePolicy(
        POL % ('C' * (MAX_TAG_COMMENTS_LENGTH + 1), 'C' *
               (MAX_RULE_DESCRIPTION_LENGTH + 1)), self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)

    # verify warning
    with self.assertLogs(level='WARN') as log:
      output = str(paloalto)
      self.assertEqual(len(log.output), 2, log.output)
      self.assertIn('comments exceeds maximum length', log.output[0])
      self.assertIn('description exceeds maximum length', log.output[1])

    x = paloalto.config.findtext(PATH_TAG + "/entry[@name='%s']/comments" % tag)
    self.assertEqual(x, 'C' * MAX_TAG_COMMENTS_LENGTH, output)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/description")
    self.assertEqual(x, 'C' * MAX_RULE_DESCRIPTION_LENGTH, output)

  def testTermLen(self):
    TERM = """
term %s {
  pan-application:: ssl
  action:: accept
}
"""

    # get maximum length
    pol = policy.ParsePolicy(GOOD_HEADER_1 + TERM % 'T', self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    TERM_MAX_LENGTH = paloalto._TERM_MAX_LENGTH

    # maximum length
    term = 'T' * TERM_MAX_LENGTH
    pol = policy.ParsePolicy(GOOD_HEADER_1 + TERM % term, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.find(PATH_RULES + "/entry[@name='%s']" % term)
    self.assertIsNotNone(x, output)

    # maximum length + 1
    term = 'T' * (TERM_MAX_LENGTH + 1)
    pol = policy.ParsePolicy(GOOD_HEADER_1 + TERM % term, self.naming)
    regex = '^Term .+ is too long[.] Limit is %d characters' % TERM_MAX_LENGTH
    self.assertRaisesRegex(aclgenerator.TermNameTooLongError, regex,
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testPanApplication(self):
    POL1 = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  action:: accept
}"""

    POL2 = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  pan-application:: %s
  action:: accept
  %s
}"""

    APPS = [
        {'app1'},
        {'app1', 'app2'},
        {'app1', 'app2', 'app3'},
    ]

    POL3 = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  pan-application:: web-browsing
  action:: accept%s
}"""

    T0 = ''
    T1 = """
  protocol:: tcp
"""
    T2 = """
  protocol:: tcp
  destination-port:: PORT1 PORT2
"""

    POL4 = """
header {
  target:: paloalto from-zone trust to-zone untrust %s
}
term rule-1 {
  pan-application:: web-browsing
  action:: accept
  protocol:: %s
}"""

    POL5 = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  pan-application:: web-browsing
  action:: accept
  protocol:: icmp
  icmp-type:: echo-request
}"""

    POL6 = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
  pan-application:: web-browsing
  protocol:: tcp icmp
  destination-port:: PORT1
  action:: accept
}"""

    pol = policy.ParsePolicy(POL1, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/application/member")
    self.assertEqual(x, 'any', output)

    for i, app in enumerate(APPS):
      pol = policy.ParsePolicy(POL2 % (' '.join(app), ''), self.naming)
      paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
      output = str(paloalto)
      x = paloalto.config.findall(PATH_RULES +
                                  "/entry[@name='rule-1']/application/member")
      apps = {elem.text for elem in x}
      self.assertEqual(APPS[i], apps, output)

    for i, app in enumerate(APPS):
      pol = policy.ParsePolicy(POL2 % (' '.join(app), 'protocol:: tcp'),
                               self.naming)
      paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
      output = str(paloalto)
      x = paloalto.config.findall(PATH_RULES +
                                  "/entry[@name='rule-1']/application/member")
      apps = {elem.text for elem in x}
      self.assertEqual(APPS[i], apps, output)

    pol = policy.ParsePolicy(POL3 % T0, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/service/member")
    self.assertEqual(x, 'application-default', output)

    pol = policy.ParsePolicy(POL3 % T1, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/service/member")
    self.assertEqual(x, 'any-tcp', output)

    definitions = naming.Naming()
    definitions._ParseLine('PORT1 = 8080/tcp', 'services')
    definitions._ParseLine('PORT2 = 8081/tcp', 'services')
    pol = policy.ParsePolicy(POL3 % T2, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findtext(PATH_RULES +
                                 "/entry[@name='rule-1']/service/member")
    self.assertEqual(x, 'service-rule-1-tcp', output)

    regex = ('^Term rule-1 contains non tcp, udp protocols '
             'with pan-application:')
    pol = policy.ParsePolicy(POL4 % ('inet', 'tcp icmp'), self.naming)
    self.assertRaisesRegex(paloaltofw.UnsupportedFilterError, regex,
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

    pol = policy.ParsePolicy(POL4 % ('inet6', 'icmpv6'), self.naming)
    self.assertRaisesRegex(paloaltofw.UnsupportedFilterError, regex,
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

    pol = policy.ParsePolicy(POL5, self.naming)
    self.assertRaisesRegex(paloaltofw.UnsupportedFilterError, regex,
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

    self.assertRaisesRegex(policy.MixedPortandNonPortProtos,
                           '^Term rule-1 contains mixed uses of protocols '
                           'with and without port numbers',
                           policy.ParsePolicy, POL6, definitions)

  def testPanPorts(self):
    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
%s
  action:: accept
}"""

    T = """
  protocol:: udp
  destination-port:: NTP
"""

    definitions = naming.Naming()
    definitions._ParseLine('NTP = 123/tcp 123/udp', 'services')
    definitions._ParseLine('DNS = 53/tcp 53/udp', 'services')

    pol = policy.ParsePolicy(POL % T, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    name = "service-rule-1-udp"
    path = "/entry[@name='%s']/protocol/udp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "123", output)
    path = "/entry[@name='%s']/protocol/udp/source-port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertIsNone(x, output)

    T = """
  protocol:: udp
  source-port:: NTP
"""

    pol = policy.ParsePolicy(POL % T, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    name = "service-rule-1-udp"
    path = "/entry[@name='%s']/protocol/udp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "0-65535", output)
    path = "/entry[@name='%s']/protocol/udp/source-port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "123", output)

    T = """
  protocol:: tcp
  source-port:: NTP
  destination-port:: NTP DNS
"""

    pol = policy.ParsePolicy(POL % T, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    name = "service-rule-1-tcp"
    path = "/entry[@name='%s']/protocol/tcp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "53,123", output)
    path = "/entry[@name='%s']/protocol/tcp/source-port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "123", output)

    T = """
  protocol:: tcp
"""

    pol = policy.ParsePolicy(POL % T, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    name = "any-tcp"
    path = "/entry[@name='%s']/protocol/tcp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "0-65535", output)
    path = "/entry[@name='%s']/protocol/tcp/source-port" % name
    x = paloalto.config.find(PATH_SERVICE + path)
    self.assertIsNone(x, output)

    T = """
  protocol:: tcp udp
"""

    pol = policy.ParsePolicy(POL % T, definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    name = "any-tcp"
    path = "/entry[@name='%s']/protocol/tcp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "0-65535", output)
    name = "any-udp"
    path = "/entry[@name='%s']/protocol/udp/port" % name
    x = paloalto.config.findtext(PATH_SERVICE + path)
    self.assertEqual(x, "0-65535", output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/service/member")
    services = {elem.text for elem in x}
    self.assertEqual({"any-tcp", "any-udp"}, services, output)

  def testPortLessNonPort(self):
    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust
}
term rule-1 {
%s
  action:: accept
}"""

    T = """
  protocol:: udp icmp
"""

    pol = policy.ParsePolicy(POL % T, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1-1']/service/member")
    self.assertTrue(len(x) > 0, output)
    services = {elem.text for elem in x}
    self.assertEqual({"any-udp"}, services, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1-2']/application/member")
    self.assertTrue(len(x) > 0, output)
    applications = {elem.text for elem in x}
    self.assertEqual({"icmp"}, applications, output)

    T = """
  protocol:: udp tcp icmp gre
"""

    pol = policy.ParsePolicy(POL % T, self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1-1']/service/member")
    self.assertTrue(len(x) > 0, output)
    services = {elem.text for elem in x}
    self.assertEqual({"any-udp", "any-tcp"}, services, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1-2']/application/member")
    self.assertTrue(len(x) > 0, output)
    applications = {elem.text for elem in x}
    self.assertEqual({"icmp", "gre"}, applications, output)

  def testSrcAnyDstAnyAddressFamily(self):
    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust %s
}
term rule-1 {
  action:: accept
}"""

    pol = policy.ParsePolicy(POL % "mixed", self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    for srcdst in ["source", "destination"]:
      x = paloalto.config.findall(PATH_RULES +
                                  "/entry[@name='rule-1']/%s/member" % srcdst)
      self.assertTrue(len(x) == 1, output)
      values = {elem.text for elem in x}
      self.assertEqual({"any"}, values, output)

    pol = policy.ParsePolicy(POL % "inet", self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    for srcdst in ["source", "destination"]:
      x = paloalto.config.findall(PATH_RULES +
                                  "/entry[@name='rule-1']/%s/member" % srcdst)
      self.assertTrue(len(x) == 1, output)
      values = {elem.text for elem in x}
      self.assertEqual({"any-ipv4"}, values, output)
      x = paloalto.config.find(PATH_RULES +
                               "/entry[@name='rule-1']/negate-source")
      self.assertIsNone(x, output)
      x = paloalto.config.find(PATH_RULES +
                               "/entry[@name='rule-1']/negate-destination")
      self.assertIsNone(x, output)

    pol = policy.ParsePolicy(POL % "inet6", self.naming)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    for srcdst in ["source", "destination"]:
      x = paloalto.config.findall(PATH_RULES +
                                  "/entry[@name='rule-1']/%s/member" % srcdst)
      self.assertTrue(len(x) == 1, output)
      values = {elem.text for elem in x}
      self.assertEqual({"any-ipv4"}, values, output)
      x = paloalto.config.find(PATH_RULES +
                               "/entry[@name='rule-1']/negate-source")
      self.assertIsNotNone(x, output)
      x = paloalto.config.find(PATH_RULES +
                               "/entry[@name='rule-1']/negate-destination")
      self.assertIsNotNone(x, output)

  def testNoAddrObj(self):
    definitions = naming.Naming()
    definitions._ParseLine('NET1 = 10.1.0.0/24', 'networks')
    definitions._ParseLine('NET2 = 10.2.0.0/24', 'networks')
    definitions._ParseLine('NET3 = 10.3.1.0/24', 'networks')
    definitions._ParseLine('       10.3.2.0/24', 'networks')
    definitions._ParseLine('NET4 = 2001:db8:0:aa::/64', 'networks')
    definitions._ParseLine('       2001:db8:0:bb::/64', 'networks')
    definitions._ParseLine('NET5 = NET3 NET4', 'networks')
    definitions._ParseLine('NET6 = 4000::/2', 'networks')
    definitions._ParseLine('NET7 = 8000::/1', 'networks')

    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust %s no-addr-obj
}
term rule-1 {
%s
  action:: accept
  protocol:: tcp
}"""

    T = """
  source-address:: NET1
  destination-address:: NET2 NET3
"""

    pol = policy.ParsePolicy(POL % ("inet", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.1.0.0/24"}, addrs, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.2.0.0/24",
                      "10.3.1.0/24", "10.3.2.0/24"}, addrs, output)

    T = """
  source-address:: NET4
"""

    pol = policy.ParsePolicy(POL % ("inet6", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"2001:db8:0:aa::/64", "2001:db8:0:bb::/64"},
                     addrs, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"any"}, addrs, output)

    T = """
  destination-address:: NET5
"""

    pol = policy.ParsePolicy(POL % ("mixed", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"any"}, addrs, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.3.1.0/24", "10.3.2.0/24",
                      "2001:db8:0:aa::/64", "2001:db8:0:bb::/64"},
                     addrs, output)
    T = """
  source-address:: NET6
  destination-address:: NET7
"""
    pol = policy.ParsePolicy(POL % ("mixed", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"4000::/3", "6000::/3"}, addrs, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"8000::/3",
                      "a000::/3", "c000::/3", "e000::/3"}, addrs, output)

    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust inet no-addr-obj
}
term rule-1 {
  source-address:: NET1 NET2
  action:: accept
  protocol:: tcp
}
header {
  target:: paloalto from-zone trust to-zone untrust inet addr-obj
}
term rule-1 {
  destination-address:: NET3
  action:: accept
  protocol:: tcp
}"""

    pol = policy.ParsePolicy(POL, definitions)
    self.assertRaisesRegex(paloaltofw.UnsupportedHeaderError,
                           '^Cannot mix addr-obj and no-addr-obj header '
                           'option in a single policy file$',
                           paloaltofw.PaloAltoFW, pol, EXP_INFO)

  def testAddrObj(self):
    definitions = naming.Naming()
    definitions._ParseLine('NET1 = 10.1.0.0/24', 'networks')
    definitions._ParseLine('NET2 = 10.2.0.0/24', 'networks')
    definitions._ParseLine('NET3 = 10.3.1.0/24', 'networks')
    definitions._ParseLine('       10.3.2.0/24', 'networks')
    definitions._ParseLine('NET4 = 4000::/128', 'networks')
    definitions._ParseLine('NET5 = 4000::/2', 'networks')

    POL = """
header {
  target:: paloalto from-zone trust to-zone untrust %s addr-obj
}
term rule-1 {
%s
  action:: accept
  protocol:: tcp
}"""

    T = """
  source-address:: NET1
  destination-address:: NET2 NET3
"""

    pol = policy.ParsePolicy(POL % ("inet", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET1"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESS_GROUP +
                                "/entry[@name='NET1']/static/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET1_0"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET1_0']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.1.0.0/24"}, addrs, output)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET2",
                      "NET3"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESS_GROUP +
                                "/entry[@name='NET2']/static/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET2_0"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET2_0']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.2.0.0/24"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESS_GROUP +
                                "/entry[@name='NET3']/static/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET3_0", "NET3_1"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET3_0']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.3.1.0/24"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET3_1']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"10.3.2.0/24"}, addrs, output)

    # These tests check that large IP ranges are broken into equivalent subnets.
    T = """
  source-address:: NET5
"""
    pol = policy.ParsePolicy(POL % ("mixed", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/source/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET5"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESS_GROUP +
                                "/entry[@name='NET5']/static/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET5_0", "NET5_1"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET5_0']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"4000::/3"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET5_1']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"6000::/3"}, addrs, output)

    T = """
  source-address:: NET4
  destination-address:: NET5
"""
    pol = policy.ParsePolicy(POL % ("mixed", T), definitions)
    paloalto = paloaltofw.PaloAltoFW(pol, EXP_INFO)
    output = str(paloalto)
    x = paloalto.config.findall(PATH_RULES +
                                "/entry[@name='rule-1']/destination/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET5"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESS_GROUP +
                                "/entry[@name='NET5']/static/member")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"NET5_0", "NET5_1"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET5_0']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"4000::/3"}, addrs, output)
    x = paloalto.config.findall(PATH_ADDRESSES +
                                "/entry[@name='NET5_1']/ip-netmask")
    self.assertTrue(len(x) > 0, output)
    addrs = {elem.text for elem in x}
    self.assertEqual({"6000::/3"}, addrs, output)

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


# tests/lib/pcap_test.py

# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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.
"""Unittest for pcap rendering module."""

import datetime
from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import pcap
from capirca.lib import policy


GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: pcap test-filter
}
"""

GOOD_HEADER_IN = """
header {
  comment:: "this is a test acl"
  target:: pcap test-filter in
}
"""

GOOD_HEADER_OUT = """
header {
  comment:: "this is a test acl"
  target:: pcap test-filter out
}
"""

GOOD_TERM_ICMP = """
term good-term-icmp {
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_ICMP_TYPES = """
term good-term-icmp-types {
  protocol:: icmp
  icmp-type:: echo-reply unreachable time-exceeded
  action:: deny
}
"""

GOOD_TERM_ICMPV6 = """
term good-term-icmpv6 {
  protocol:: icmpv6
  action:: accept
}
"""

BAD_TERM_ICMP = """
term test-icmp {
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

BAD_TERM_ACTION = """
term bad-term-action {
  protocol:: icmp
  action:: undefined
}
"""

GOOD_TERM_TCP = """
term good-term-tcp {
  comment:: "Test term 1"
  destination-address:: PROD_NETWRK
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_WARNING_TERM = """
term good-warning-term {
  comment:: "Test term 1"
  destination-address:: PROD_NETWRK
  destination-port:: SMTP
  protocol:: tcp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_LOG = """
term good-term-log {
  protocol:: tcp
  logging:: true
  action:: accept
}
"""
GOOD_ICMP_CODE = """
term good_term {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 3 4
  action:: accept
}
"""
EXPIRED_TERM = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

NEXT_TERM = """
term next {
  action:: next
}
"""

NEXT_LOG_TERM = """
term next-log {
  logging:: true
  action:: next
}
"""

ESTABLISHED_TERM = """
term accept-established {
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""

VRRP_TERM = """
term vrrp-term {
  protocol:: vrrp
  action:: accept
}
"""

UNICAST_TERM = """
term unicast-term {
  destination-address:: ANY
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_HBH = """
term good-term-hbh {
  protocol:: hopopt
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'name',
    'option',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'syn',
               'ack',
               'fin',
               'rst',
               'urg',
               'psh',
               'all',
               'none',
               'established',
               'tcp-established'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class PcapFilter(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testTcp(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(dst net 10.0.0.0/8) and (proto \\tcp) and (dst port 25)', result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWRK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testLog(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_LOG, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'proto \\tcp', result,
        'did not find actual term for good-term-log')

  def testIcmp(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'proto \\icmp', result,
        'did not find actual term for good-term-icmp')

  def testIcmpCode(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_ICMP_CODE, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('and icmp[icmpcode] == 3', result, result)
    self.assertIn('and icmp[icmpcode] == 4', result, result)

  def testIcmpTypes(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMP_TYPES, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(proto \\icmp) and (icmp[icmptype] == 0 or icmp[icmptype] == 3'
        ' or icmp[icmptype] == 11)', result,
        'did not find actual term for good-term-icmp-types')

  def testIcmpv6(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMPV6, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        'icmp6', result,
        'did not find actual term for good-term-icmpv6')

  def testBadIcmp(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + BAD_TERM_ICMP, self.naming), EXP_INFO)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      str, acl)

  @mock.patch.object(pcap.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + EXPIRED_TERM, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and '
        'will not be rendered.', 'expired_test', 'test-filter')

  @mock.patch.object(pcap.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + EXPIRING_TERM % exp_date.strftime('%Y-%m-%d'),
        self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring', 'test-filter')

  def testMultiprotocol(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + MULTIPLE_PROTOCOLS_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(proto \\tcp or proto \\udp or proto \\icmp)', result,
        'did not find actual term for multi-proto')

  def testNextTerm(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + NEXT_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn('', result,
                  'did not find actual term for good-term-icmpv6')

  def testTcpOptions(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + ESTABLISHED_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(tcp[tcpflags] & (tcp-ack) == (tcp-ack)', result,
        'did not find actual term for established')

  def testVrrpTerm(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + VRRP_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(proto 112)', result,
        'did not find actual term for vrrp')

  def testMultiHeader(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_LOG + GOOD_HEADER + GOOD_TERM_ICMP,
        self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '((((proto \\tcp))\n))\nor\n((((proto \\icmp))\n))', result,
        'did not find actual terms for multi-header')

  def testDirectional(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER_IN + GOOD_TERM_LOG + GOOD_HEADER_OUT + GOOD_TERM_ICMP,
        self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(((dst net localhost and ((proto \\tcp)))\n))\nor\n'
        '(((src net localhost and ((proto \\icmp)))\n))', result,
        'did not find actual terms for directional')

  def testUnicastIPv6(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('::/0')]

    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER_IN + UNICAST_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertIn(
        '(dst net localhost and ((proto \\tcp)))', result,
        'did not find actual terms for unicast-term')

    self.naming.GetNetAddr.assert_called_once_with('ANY')

  def testHbh(self):
    acl = pcap.PcapFilter(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_HBH, self.naming), EXP_INFO)
    result = str(acl)

    self.assertIn(
        '(ip6 protochain 0)', result,
        'did not find actual terms for unicast-term')

  def testBuildTokens(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']
    pol1 = pcap.PcapFilter(policy.ParsePolicy(GOOD_HEADER + GOOD_TERM_TCP,
                                              self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    pol1 = pcap.PcapFilter(
        policy.ParsePolicy(GOOD_HEADER + GOOD_WARNING_TERM,
                           self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


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


# tests/lib/policy_simple_test.py

# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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.


from absl.testing import absltest

from absl import logging
from capirca.lib import policy_simple


class FieldTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    logging.debug('======> %s <======', self.id())

  def testAppendAppends(self):
    f = policy_simple.Field('Testvalue')
    f.Append('TESTVALUE')
    self.assertEqual(f.value, 'TestvalueTESTVALUE')

  def testStr(self):
    f = policy_simple.Field('Testvalue')
    self.assertEqual('UNKNOWN::Testvalue', str(f))

  def testStrIndents(self):
    f = policy_simple.Field('Testvalue\nTestValue')
    self.assertEqual('UNKNOWN::Testvalue\n            TestValue', str(f))

  def testIntegerField(self):
    self.assertRaises(ValueError, policy_simple.IntegerField, '7.01')
    try:
      _ = policy_simple.IntegerField('7')
    except ValueError:
      self.fail("IntegerField should accept '7' as value.")

  def testNamingFieldRejectsBad(self):
    bads = (
        'corp_internal',
        'CORP+INTERNAL',
    )
    for bad in bads:
      logging.debug('Testing bad "%s".', bad)
      self.assertRaises(ValueError, policy_simple.NamingField, bad)

  def testNamingFieldAcceptsGood(self):
    goods = (
        'CORP_INTERNAL',
        'RFC1918',
        'FOO_BAR102.BAZ101',
    )
    for good in goods:
      try:
        logging.debug('Testing good "%s".', good)
        _ = policy_simple.NamingField(good)
      except ValueError:
        self.fail('Rejected good NamingField value "%s".' % good)

  def testNamingFieldAppendRejectsBad(self):
    f = policy_simple.NamingField('RFC1918')
    bads = (
        'corp_internal',
        'CORP+INTERNAL',
    )
    for bad in bads:
      logging.debug('Testing bad "%s".', bad)
      self.assertRaises(ValueError, f.Append, bad)

  def testNamingFieldAppendAcceptsGood(self):
    f = policy_simple.NamingField('RFC1918')
    goods = (
        'CORP_INTERNAL',
        'RFC1918',
        'FOO_BAR102.BAZ101',
    )
    for good in goods:
      try:
        logging.debug('Testing good "%s".', good)
        _ = f.Append(good)
      except ValueError:
        self.fail('Rejected good NamingField value "%s".' % good)

  def testNamingFieldDedupes(self):
    f = policy_simple.NamingField('RFC1918 CORP_INTERNAL RFC1918')
    f.Append('RFC1918')
    f.Append('CORP_INTERNAL RFC1918')
    self.assertEqual(set(['RFC1918', 'CORP_INTERNAL']), f.value)

  def testNamingFieldStr(self):
    f = policy_simple.NamingField(' '.join(str(x) for x in range(25)))
    expected_str = ('UNKNOWN:: 0 1 10 11 12 13 14 15 16 17 18 19 2 20 21'
                    ' 22 23 24 3 4 5 6 7\n            9')
    self.assertEqual(expected_str, str(f))


class BlockTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    logging.debug('======> %s <======', self.id())

  def testRejectsNonField(self):
    b = policy_simple.Block()
    for t in ('', 3, lambda x: x, policy_simple.Header(),
              policy_simple.Policy('test')):
      self.assertRaises(TypeError, b.AddField, t)

  def testFieldsWithType(self):
    b = policy_simple.Block()
    c1 = policy_simple.Comment('test1')
    c2 = policy_simple.Comment('test2')
    d = policy_simple.DestinationAddress('XYZ')
    s = policy_simple.SourceAddress('ABC')
    for field in (c1, d, c2, s):
      b.AddField(field)

    self.assertEqual([c1, d, c2, s], b.fields)
    self.assertEqual([c1, c2], b.FieldsWithType(policy_simple.Comment))

  def testIter(self):
    a = object()
    b = object()
    c = object()
    block = policy_simple.Block()
    block.fields = (a, b, c)

    self.assertEqual([a, b, c], list(block))


class PolicyTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    logging.debug('======> %s <======', self.id())

  def testAddMember(self):
    p = policy_simple.Policy('test')
    good = [policy_simple.Header(), policy_simple.Term('test'),
            policy_simple.BlankLine(), policy_simple.CommentLine('test'),
            policy_simple.Include('other_pol')]
    bad = ('', 3, lambda x: x, policy_simple.Field('test'))

    for member in good:
      try:
        p.AddMember(member)
      except TypeError:
        self.fail('Policy should accept member "%s"' % member)
    self.assertEqual(good, p.members)

    for member in bad:
      self.assertRaises(TypeError, p.AddMember, member)

  def testIter(self):
    a = object()
    b = object()
    c = object()
    pol = policy_simple.Policy(identifier=None)
    pol.members = (a, b, c)

    self.assertEqual([a, b, c], list(pol))


class PolicyParserTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    logging.debug('======> %s <======', self.id())

  def Parser(self, data):
    return policy_simple.PolicyParser(data=data, identifier='test')

  def testParseCommentLine(self):
    parser = self.Parser('# test-comment-value')
    expected = policy_simple.CommentLine('# test-comment-value')

    pol = parser.Parse()
    self.assertEqual([expected], pol.members)

  def testParseBlankLine(self):
    parser = self.Parser('')
    expected = policy_simple.BlankLine()

    pol = parser.Parse()
    self.assertEqual([expected], pol.members)

  def testParseInclude(self):
    parser = self.Parser('#include other/file #whatever')
    expected = policy_simple.Include('other/file')

    pol = parser.Parse()
    self.assertEqual([expected], pol.members)

  def testParseHeader(self):
    parser = self.Parser('header {\ntarget::Test\n}')
    expected = policy_simple.Header()
    expected.AddField(policy_simple.Target('Test'))

    pol = parser.Parse()
    self.assertEqual(expected, pol.members[0])

  def testParseTerm(self):
    parser = self.Parser('term testy {\ntarget::Test\n}')
    expected = policy_simple.Term('testy')
    expected.AddField(policy_simple.Target('Test'))

    pol = parser.Parse()
    self.assertEqual(expected, pol.members[0])

  def testParseTermBadField(self):
    parser = self.Parser('term testy {\nbad_field::Test\n}')
    self.assertRaises(ValueError, parser.Parse)

  def testUnfinishedBlock(self):
    parser = self.Parser('term testy {\ntarget::Test\n')
    self.assertRaises(ValueError, parser.Parse)


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


# tests/lib/policy_test.py

# Copyright 2011 Google Inc. All Rights Reserved.
#
# 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.

"""Unit tests for policy.py library."""

from absl.testing import absltest
from unittest import mock

from absl import logging
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy


HEADER = """
header {
  comment:: "this is a test acl"
  comment:: "this is another comment"
  target:: juniper test-filter
}
"""
HEADER_2 = """
header {
  comment:: "this goes in the other direction"
  target:: juniper test-filter-outbound
}
"""
HEADER_3 = """
header {
  comment:: "test header 3"
  target:: cisco 50 standard
}
"""
HEADER_4 = """
header {
  comment:: "test header 4"
  target:: iptables
}
"""
HEADER_5 = """
header {
  comment:: "test header 5"
  target:: gce global/networks/default
}
"""
HEADER_6 = """
header {
  comment:: "this is a test nftable acl"
  target::  nftables chain_name input 0 inet
}
"""
HEADER_V6 = """
header {
  comment:: "this is a test inet6 acl"
  comment:: "this is another comment"
  target:: juniper test-filter inet6
}
"""
HEADER_SRX = """
header {
  target:: srx from-zone foo to-zone bar
}
"""
HEADER_OBJ_GRP = """
header {
  target:: cisco foo object-group
}
"""
HEADER_ADDRBOOK_MIXED = """
header {
  target:: srx from-zone to-zone bar
  target:: cisco foo
}
"""
HEADER_HF_1 = """
header {
  comment:: "This is a test of HF INGRESS Policy."
  target:: gcp_hf INGRESS
}
"""

INCLUDE_STATEMENT = """
#include "includes/y.inc"
"""
INCLUDED_Y_FILE = """
term included-term-1 {
  protocol:: tcp
  action:: accept
}
#include "includes/z.inc"
"""

BAD_INCLUDED_FILE = """
term included-term-1 {
  protocol:: tcp
  action:: accept
}
#include "/tmp/z.inc"
"""

BAD_INCLUDED_FILE_1 = """
term included-term-1 {
  protocol:: tcp
  action:: accept
}
#include "includes/../../etc/passwd.inc"
"""

GOOD_INCLUDED_FILE_1 = """
term good-included-term-1 {
  protocol:: tcp
  action:: accept
}
#include "includes/../pol/z.inc"
"""

GOOD_TERM_0 = """
term good-term-0 {
  protocol:: icmp
  action:: accept
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}
"""
GOOD_TERM_2 = """
term good-term-2 {
  protocol:: tcp
  source-address:: PROD_NETWRK
  action:: accept
}
"""
GOOD_TERM_3 = """
term good-term-3 {
  protocol:: tcp
  source-address:: PROD_NETWRK
  destination-port:: SMTP
  action:: accept
}
"""
GOOD_TERM_4 = """
term good-term-4 {
  protocol:: 1
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  action:: accept
}
"""
GOOD_TERM_6 = """
term good-term-6 {
  protocol:: tcp
  destination-port:: MYSQL HIGH_PORTS
  action:: accept
}
"""
GOOD_TERM_7 = """
term good-term-7 {
  protocol:: tcp
  destination-address:: PROD_NETWRK
  destination-exclude:: PROD_EH
  action:: accept
}
"""
GOOD_TERM_8 = """
term good-term-8 {
  protocol:: tcp udp
  destination-port:: DNS
  action:: accept
}
"""
GOOD_TERM_9 = """
term good-term-9 {
  comment:: "first comment"
  comment:: "second comment"
  action:: accept
}
"""
GOOD_TERM_10 = """
term good-term-10 {
  logging:: true
  action:: accept
}
"""
GOOD_TERM_11 = """
term good-term-11 {
  protocol:: icmp
  icmp-type:: echo-reply echo-request unreachable
  action:: accept
}
"""
GOOD_TERM_12 = """
term qos-good-term-12 {
  action:: accept
  qos:: af4
}
"""
GOOD_TERM_13 = """
term good-term-13 {
  source-port:: GOOGLE_PUBLIC
  source-port:: SNMP
  protocol:: udp
  action:: accept
}
"""
GOOD_TERM_14 = """
term good-term-14 {
  source-prefix:: foo_prefix_list
  action:: accept
}
"""
GOOD_TERM_15 = """
term good-term-15 {
  destination-prefix:: bar_prefix_list baz_prefix_list
  action:: accept
}
"""
GOOD_TERM_16 = """
term good-term-16 {
  ether-type:: arp ipv4
  ether-type:: vlan
  action:: accept
}
"""
GOOD_TERM_17 = """
term good-term-17 {
  traffic-type:: broadcast unknown-unicast
  traffic-type:: multicast
  action:: accept
}
"""
GOOD_TERM_18 = """
term good-term-18 {
  comment:: "test verbatim output"
  verbatim:: iptables "mary had a little lamb"
  verbatim:: juniper "mary had another lamb"
}
"""
GOOD_TERM_19 = """
term good-term-19 {
  source-port:: HTTP MYSQL
  destination-address:: PROD_EXTERNAL_SUPER PROD_NETWRK
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_20 = """
term good-term-20 {
  source-port:: MYSQL HTTP
  destination-address:: PROD_NETWRK PROD_EXTERNAL_SUPER
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_21 = """
term good-term-21 {
  source-port:: MYSQL HTTPS
  destination-address:: PROD_NETWRK PROD_EXTERNAL_SUPER
  protocol:: tcp
  action:: accept
}
"""
GOOD_TERM_22 = """
term precedence-term {
  protocol:: icmp
  precedence:: 1
  action:: accept
}
"""
GOOD_TERM_23 = """
term loss-priority-term {
  source-port:: SSH
  protocol:: tcp
  loss-priority:: low
  action:: accept
}
"""
GOOD_TERM_24 = """
term routing-instance-term {
  source-port:: SSH
  protocol:: tcp
  routing-instance:: foobar-router
}
"""
GOOD_TERM_25 = """
term source-interface-term {
  source-port:: SSH
  protocol:: tcp
  source-interface:: foo0
  action:: accept
}
"""
GOOD_TERM_26 = """
term good-term-26 {
  protocol:: tcp
  source-address:: PROD_NETWRK
  source-exclude:: PROD_EH
  action:: accept
}
"""
GOOD_TERM_27 = """
term good-term-27 {
  protocol:: tcp
  address:: PROD_NETWRK
  address-exclude:: PROD_EH
  action:: accept
}
"""
GOOD_TERM_28 = """
term good-term-28 {
  protocol:: tcp
  source-address:: PROD_NETWRK
  source-exclude:: BOTTOM_HALF
  action:: accept
}
"""
GOOD_TERM_29 = """
term good-term-29 {
  protocol:: tcp
  option:: tcp-established
  source-address:: PROD_NETWRK
  action:: accept
}
"""
GOOD_TERM_30 = """
term good-term-30 {
  protocol:: tcp
  action:: accept
  vpn:: special-30
}
"""
GOOD_TERM_31 = """
term good-term-31 {
  protocol:: tcp
  action:: accept
  vpn:: special-31 policy-11
}
"""
GOOD_TERM_32 = """
term good-term-32 {
  forwarding-class:: fritzy
  action:: accept
}
"""
GOOD_TERM_33 = """
term good-term-33 {
  forwarding-class:: flashy
  action:: accept
}
"""
GOOD_TERM_34 = """
term good-term-34 {
  source-tag:: src-tag
  destination-tag:: dest-tag
  action:: accept
}
"""
GOOD_TERM_35 = """
term good-term-35 {
  source-address:: PROD_NETWRK
  next-ip:: NEXT_IP
}
"""
GOOD_TERM_36 = """
term good-term-36 {
  forwarding-class:: flashy fritzy
  action:: accept
}
"""
GOOD_TERM_37 = """
term good-term-37 {
  protocol:: icmp
  action:: accept
  log_name:: "my special prefix"
}
"""
GOOD_TERM_38 = """
term good-term-38 {
  source-prefix-except:: foo_prefix_list
  action:: accept
}
"""
GOOD_TERM_39 = """
term good-term-39 {
  destination-prefix-except:: bar_prefix_list baz_prefix_list
  action:: accept
}
"""
GOOD_TERM_40 = """
term good-term-38 {
  source-prefix:: foo_prefix_list
  source-prefix-except:: foo_prefix_list_except
  action:: accept
}
"""
GOOD_TERM_41 = """
term good-term-39 {
  destination-prefix:: bar_prefix_list
  destination-prefix-except:: bar_prefix_list_except
  action:: accept
}
"""
GOOD_TERM_42 = """
term good-term-42 {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 3 4
  action:: accept
}
"""
GOOD_TERM_43 = """
term good-term-43 {
  ttl:: 10
  action:: accept
}
"""
GOOD_TERM_44 = """
term good-term-44 {
  logging:: syslog
  log-limit:: 999/day
  action:: accept
}
"""
GOOD_TERM_45 = """
term good-term-45 {
  source-address:: ANY
  action:: accept
  target-service-accounts:: acct1@blah.com
}
"""
GOOD_TERM_46 = """
term good-term-46 {
  protocol:: icmp tcp udp gre esp ah sctp
  encapsulate:: stuff_and_things
}
"""
GOOD_TERM_47 = """
term good-term-47 {
  protocol:: icmp tcp udp gre esp ah sctp
  port-mirror:: true
}
"""
GOOD_TERM_48 = """
term good-term-48 {
  protocol:: icmp
  source-zone:: zone1 zone2
  destination-zone:: zone1 zone2
  action:: accept
}
"""
GOOD_TERM_49 = """
term good-term-46 {
  protocol:: udp
  decapsulate:: mpls-in-udp
}
"""
GOOD_TERM_50 = """
term good-term-45 {
  source-address:: ANY
  action:: accept
  source-service-accounts:: acct1@blah.com
}
"""
GOOD_TERM_V6_1 = """
term good-term-v6-1 {
  hop-limit:: 5
  action:: accept
}
"""
GOOD_TERM_V6_2 = """
term good-term-v6-1 {
  hop-limit:: 5-7
  action:: accept
}
"""

TERM_SUPER_2 = """
term term-super {
  address:: PROD
  action:: accept
}
"""
TERM_SUPER_3 = """
term term-super {
  protocol-except:: tcp udp icmpv6
  counter:: stuff_and_things
  action:: reject
}
"""
TERM_SUB_2 = """
term term-sub {
  protocol:: icmp
  action:: accept
}
"""
TERM_UNSORTED_ICMP_TYPE = """
term good-term-11 {
  protocol:: icmp
  icmp-type:: unreachable echo-request echo-reply
  action:: accept
}
"""
TERM_UNSORTED_ICMP_CODE = """
term good-term-11 {
  icmp-type:: unreachable
  icmp-code:: 15 4 9 1
  action:: accept
}
"""
BAD_TERM_1 = """
term bad-term- 1 {
  protocol:: tcp
  action:: reject
}
"""
BAD_TERM_2 = """
term bad-term-2 {
  prootocol:: tcp
  action:: accept
}
"""
BAD_TERM_3 = """
term bad-term-3 {
  protocol:: tcp
  source-port:: SNMP
  action:: accept
}
"""
BAD_TERM_4 = """
term bad-term-4 {
  source-port:: SMTP
  action:: accept
}
"""
BAD_TERM_5 = """
term bad-term-5 {
  protocol:: tcp
  destination-address:: PROD_EH
  destination-exclude:: PROD_NETWRK
  action:: accept
}
"""
BAD_TERM_6 = """
term bad-term-6 {
  logging:: unvalidloggingoption
  action:: accept
}
"""
BAD_TERM_7 = """
term bad-term-7 {
  action:: discard
}
"""
BAD_TERM_8 = """
term bad-term-8 {
  akshun:: accept
}
"""
BAD_TERM_9 = """
term bad-term-9 {
  ether-type:: arp
  protocol:: udp
  action:: accept
}
"""
BAD_TERM_10 = """
term bad-term-10 {
  verbatim:: cisco "mary had a little lamb"

  action:: accept
}
"""
BAD_TERM_12 = """
term bad-term-12 {
  protocol:: icmp
  icmp-type:: echo-foo packet-too-beaucoups
  action:: accept
}
"""
BAD_TERM_13 = """
term bad-term-13 {
  protocol:: icmp
  icmp-type:: unreachable
  icmp-code:: 99
  action:: accept
}
"""
BAD_TERM_14 = """
term bad-term-14 {
  protocol:: icmp
  icmp-type:: unreachable redirect
  icmp-code:: 3
  action:: accept
}
"""
BAD_TERM_15 = """
term bad-term-15 {
  ttl:: 300
  action:: accept
}
"""
BAD_TERM_16 = """
term bad-term-16 {
  destination-port:: FOO
  protocol:: tcp udp gre
  action:: accept
}
"""

# pylint: disable=maybe-no-member


class PolicyTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  @mock.patch.object(policy, '_ReadFile')
  def testIncludes(self, mock_file):
    """Ensure includes work, as well as nested included."""
    mock_file.side_effect = [INCLUDED_Y_FILE, GOOD_TERM_5]

    # contents of our base policy (which has an included file)
    pol = HEADER + INCLUDE_STATEMENT + GOOD_TERM_1
    p = policy.ParsePolicy(pol, self.naming)
    _, terms = p.filters[0]
    # ensure include worked and we now have 3 terms in this policy
    self.assertEqual(len(terms), 3)
    # ensure included_term_1 is included as first term
    self.assertEqual(terms[0].name, 'included-term-1')
    # ensure good-term-5 is included as second term
    self.assertEqual(terms[1].name, 'good-term-5')
    # ensure good-term-1 shows up as the second term
    self.assertEqual(terms[2].name, 'good-term-1')

    mock_file.assert_has_calls(
        [mock.call('includes/y.inc'), mock.call('includes/z.inc')]
    )

  @mock.patch.object(policy, '_ReadFile')
  def testBadIncludes(self, mock_file):
    """Ensure nested includes error handling works."""
    mock_file.side_effect = [BAD_INCLUDED_FILE, GOOD_TERM_5]

    # contents of our base policy (which has a bad included file)
    pol = HEADER + INCLUDE_STATEMENT + GOOD_TERM_1
    self.assertRaises(
        policy.InvalidIncludeDirectoryError,
        policy.ParsePolicy,
        pol,
        self.naming,
    )
    # Ensuring relative paths don't bypass invalid directory checks
    mock_file.side_effect = [BAD_INCLUDED_FILE_1, GOOD_TERM_5]
    pol = HEADER + BAD_INCLUDED_FILE_1 + GOOD_TERM_1
    self.assertRaises(
        policy.InvalidIncludeDirectoryError,
        policy.ParsePolicy,
        pol,
        self.naming,
    )

  @mock.patch.object(policy, '_ReadFile')
  def testGoodIncludesWithRelativePaths(self, mock_file):
    """Ensure nested includes error handling works for valid files."""
    mock_file.side_effect = [GOOD_TERM_5]
    # base policy has a good included file, with relative paths
    pol = HEADER + GOOD_INCLUDED_FILE_1 + GOOD_TERM_1
    p = policy.ParsePolicy(pol, self.naming)
    _, terms = p.filters[0]
    # ensure include worked and we now have 3 terms in this policy
    self.assertEqual(len(terms), 3)
    self.assertEqual(terms[0].name, 'good-included-term-1')
    self.assertEqual(terms[1].name, 'good-term-5')
    self.assertEqual(terms[2].name, 'good-term-1')

  def testGoodPol(self):
    pol = HEADER + GOOD_TERM_1 + GOOD_TERM_2
    self.naming.GetNetAddr.return_value = [nacaddr.IPv4('10.0.0.0/8')]

    ret = policy.ParsePolicy(pol, self.naming)
    # we should only have one filter from that
    self.assertEqual(len(ret.filters), 1)
    header, terms = ret.filters[0]
    self.assertEqual(type(ret), policy.Policy)
    self.assertEqual(str(terms[0].protocol[0]), 'icmp')
    self.assertEqual(len(terms), 2)
    # the comment is stored as a double quoted string, complete with double
    # quotes.
    self.assertEqual(str(header.comment[0]), 'this is a test acl')
    self.assertEqual(str(header.comment[1]), 'this is another comment')
    self.assertEqual(str(header.target[0]), 'juniper')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWRK')

  def testBadPol(self):
    pol = HEADER + BAD_TERM_1
    self.assertRaises(policy.ParseError, policy.ParsePolicy, pol, self.naming)

  def testMissingHeader(self):
    pol = GOOD_TERM_1 + GOOD_TERM_2
    self.assertRaises(policy.ParseError, policy.ParsePolicy, pol, self.naming)

  def testService(self):
    pol = HEADER + GOOD_TERM_1 + GOOD_TERM_3
    self.naming.GetNetAddr.return_value = [nacaddr.IPv4('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(len(terms), 2)
    self.assertEqual(str(terms[1].protocol[0]), 'tcp')
    self.assertEqual(terms[1].destination_port[0], (25, 25))

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWRK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testInvalidKeyword(self):
    pol = HEADER + BAD_TERM_2
    self.assertRaises(policy.ParseError, policy.ParsePolicy, pol, self.naming)

  def testNumericProtocol(self):
    pol = HEADER + GOOD_TERM_4
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].protocol[0]), '1')

  def testHopLimitSingle(self):
    pol = HEADER_V6 + GOOD_TERM_V6_1
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].hop_limit[0]), '5')

  def testHopLimitRange(self):
    pol = HEADER_V6 + GOOD_TERM_V6_2
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].hop_limit[2]), '7')

  def testBadPortProtocols(self):
    pol = HEADER + BAD_TERM_3
    self.naming.GetServiceByProto('SNMP', 'tcp').AndReturn([])
    self.assertRaises(policy.TermPortProtocolError, policy.ParsePolicy, pol,
                      self.naming)

  def testBadPortProtocols2(self):
    pol = HEADER + BAD_TERM_4
    self.assertRaises(policy.TermPortProtocolError, policy.ParsePolicy, pol,
                      self.naming)

  def testMinimumTerm(self):
    pol = HEADER + GOOD_TERM_5
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(len(terms), 1)
    self.assertEqual(str(terms[0].action[0]), 'accept')

  def testPortCollapsing(self):
    pol = HEADER + GOOD_TERM_6
    self.naming.GetServiceByProto.return_value = ['3306']
    self.naming.GetServiceByProto.return_value = ['1024-65535']

    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertSequenceEqual(terms[0].destination_port, [(1024, 65535)])

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('MYSQL', 'tcp'),
        mock.call('HIGH_PORTS', 'tcp')], any_order=True)

  def testPortCollapsing2(self):
    pol = HEADER + GOOD_TERM_8
    self.naming.GetServiceByProto.side_effect = [['53'], ['53']]

    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertSequenceEqual(terms[0].destination_port, [(53, 53)])

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('DNS', 'tcp'),
        mock.call('DNS', 'udp')], any_order=True)

  def testMinimumTerm2(self):
    pol = HEADER + GOOD_TERM_9
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].comment[0]), 'first comment')
    self.assertEqual(str(terms[0].comment[1]), 'second comment')

  def testLogNameTerm(self):
    pol = HEADER_6 + GOOD_TERM_37
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].log_name), 'my special prefix')

  def testTermEquality(self):
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('64.233.160.0/19'), nacaddr.IPv4('66.102.0.0/20'),
         nacaddr.IPv4('66.249.80.0/20'), nacaddr.IPv4('72.14.192.0/18'),
         nacaddr.IPv4('72.14.224.0/20'), nacaddr.IPv4('216.239.32.0/19')],
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('64.233.160.0/19'), nacaddr.IPv4('66.102.0.0/20'),
         nacaddr.IPv4('66.249.80.0/20'), nacaddr.IPv4('72.14.192.0/18'),
         nacaddr.IPv4('72.14.224.0/20'), nacaddr.IPv4('216.239.32.0/19')],
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('64.233.160.0/19'), nacaddr.IPv4('66.102.0.0/20'),
         nacaddr.IPv4('66.249.80.0/20'), nacaddr.IPv4('72.14.192.0/18'),
         nacaddr.IPv4('72.14.224.0/20'), nacaddr.IPv4('216.239.32.0/19')]]
    self.naming.GetServiceByProto.side_effect = [
        ['80'], ['3306'], ['3306'], ['80'], ['3306'], ['443']]

    pol_text = HEADER + GOOD_TERM_19 + GOOD_TERM_20 + GOOD_TERM_21
    ret = policy.ParsePolicy(pol_text, self.naming, shade_check=False)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(len(terms), 3)
    self.assertEqual(terms[0], terms[1])
    self.assertNotEqual(terms[0], terms[2])

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_EXTERNAL_SUPER'),
        mock.call('PROD_NETWRK'),
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EXTERNAL_SUPER'),
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EXTERNAL_SUPER')], any_order=True)
    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('HTTP', 'tcp'),
        mock.call('MYSQL', 'tcp'),
        mock.call('MYSQL', 'tcp'),
        mock.call('HTTP', 'tcp'),
        mock.call('MYSQL', 'tcp'),
        mock.call('HTTPS', 'tcp')], any_order=True)

  def testGoodDestAddrExcludes(self):
    pol = HEADER + GOOD_TERM_7
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.62.0.0/15')]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].destination_address_exclude[0],
                     nacaddr.IPv4('10.62.0.0/15'))

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testGoodSrcAddrExcludes(self):
    pol = HEADER + GOOD_TERM_26
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.62.0.0/15')]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].source_address_exclude[0],
                     nacaddr.IPv4('10.62.0.0/15'))

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testGoodAddrExcludes(self):
    pol = HEADER + GOOD_TERM_27
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.62.0.0/15')]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].address_exclude[0],
                     nacaddr.IPv4('10.62.0.0/15'))

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testGoodAddrExcludesFlatten(self):
    expected = sorted([nacaddr.IPv4(u'10.0.0.0/11'),
                       nacaddr.IPv4(u'10.32.0.0/12'),
                       nacaddr.IPv4(u'10.48.0.0/13'),
                       nacaddr.IPv4(u'10.56.0.0/14'),
                       nacaddr.IPv4(u'10.60.0.0/15'),
                       nacaddr.IPv4(u'10.64.0.0/10'),
                       nacaddr.IPv4(u'10.130.0.0/15'),
                       nacaddr.IPv4(u'10.132.0.0/14'),
                       nacaddr.IPv4(u'10.136.0.0/13'),
                       nacaddr.IPv4(u'10.144.0.0/12'),
                       nacaddr.IPv4(u'10.160.0.0/11'),
                       nacaddr.IPv4(u'10.192.0.0/10')])
    pol = HEADER + GOOD_TERM_27
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.62.0.0/15'), nacaddr.IPv4('10.129.0.0/15',
                                                    strict=False)]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    terms[0].FlattenAll()

    self.assertEqual(sorted(terms[0].address), expected)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testGoodAddrExcludesFlattenMultiple(self):
    pol = HEADER + GOOD_TERM_27
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.1.0.0/16'),
         nacaddr.IPv4('10.2.0.0/16'),
         nacaddr.IPv4('10.3.0.0/16'),
         nacaddr.IPv4('192.168.0.0/16')],
        [nacaddr.IPv4('10.2.0.0/15')]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    terms[0].FlattenAll()
    self.assertEqual(terms[0].address,
                     [nacaddr.IPv4('10.1.0.0/16'),
                      nacaddr.IPv4('192.168.0.0/16')])

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testGoodAddrExcludesFlattenAll(self):
    pol = HEADER + GOOD_TERM_27
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.1.0.0/16'),
         nacaddr.IPv4('10.2.0.0/16'),
         nacaddr.IPv4('10.3.0.0/16')],
        [nacaddr.IPv4('10.0.0.0/8')]]

    ret = policy.ParsePolicy(pol, self.naming)
    _, terms = ret.filters[0]
    terms[0].FlattenAll()
    self.assertEqual(terms[0].address, [])

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_EH')], any_order=True)

  def testLogging(self):
    pol = HEADER + GOOD_TERM_10
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(str(terms[0].logging[0]), 'true')

  def testBadLogging(self):
    pol = HEADER + BAD_TERM_6
    self.assertRaises(policy.InvalidTermLoggingError, policy.ParsePolicy, pol,
                      self.naming)

  def testBadAction(self):
    pol = HEADER + BAD_TERM_7
    self.assertRaises(policy.InvalidTermActionError, policy.ParsePolicy, pol,
                      self.naming)

  def testMultifilter(self):
    pol = HEADER + GOOD_TERM_1 + HEADER_2 + GOOD_TERM_1
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.headers), 2)

  def testBadMultifilter(self):
    pol = HEADER + HEADER_2 + GOOD_TERM_1
    self.assertRaises(policy.NoTermsError, policy.ParsePolicy, pol,
                      self.naming)

  def testICMPTypes(self):
    pol = HEADER + GOOD_TERM_11
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].icmp_type[0], 'echo-reply')

  def testBadICMPTypes(self):
    pol = HEADER + BAD_TERM_12
    self.assertRaises(policy.TermInvalidIcmpType,
                      policy.ParsePolicy, pol, self.naming)

  def testICMPTypesSorting(self):
    pol = HEADER + TERM_UNSORTED_ICMP_TYPE
    ret = policy.ParsePolicy(pol, self.naming)
    icmp_types = ['echo-reply', 'echo-request', 'unreachable']
    expected = 'icmp_type: %s' % icmp_types
    self.assertIn(expected, str(ret))

  def testICMPCodesSorting(self):
    pol = HEADER + TERM_UNSORTED_ICMP_CODE
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertIn('icmp_code: [1, 4, 9, 15]', str(ret))

  def testReservedWordTermName(self):
    pol = HEADER + GOOD_TERM_12
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].qos, 'af4')
    self.assertEqual(terms[0].name, 'qos-good-term-12')

  def testMultiPortLines(self):
    pol = HEADER + GOOD_TERM_13
    self.naming.GetServiceByProto.side_effect = [['22', '160-162'], ['161']]

    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertSequenceEqual(terms[0].source_port, [(22, 22), (160, 162)])

    self.naming.GetServiceByProto.assert_has_calls([
        mock.call('GOOGLE_PUBLIC', 'udp'),
        mock.call('SNMP', 'udp')], any_order=True)

  def testErrorLineNumber(self):
    pol = HEADER + GOOD_TERM_13 + BAD_TERM_8
    self.assertRaisesRegex(policy.ParseError,
                           r'ERROR on "akshun" \(type STRING, line 1',
                           policy.ParsePolicy, pol, self.naming)

  def testPrefixList(self):
    spol = HEADER + GOOD_TERM_14
    dpol = HEADER + GOOD_TERM_15

    # check on the source prefix list
    ret = policy.ParsePolicy(spol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].source_prefix, ['foo_prefix_list'])

    # check on the destination prefix list
    ret = policy.ParsePolicy(dpol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].destination_prefix,
                     ['bar_prefix_list', 'baz_prefix_list'])

  def testPrefixListExcept(self):
    spol = HEADER + GOOD_TERM_38
    dpol = HEADER + GOOD_TERM_39

    # check on the source prefix except list
    ret = policy.ParsePolicy(spol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].source_prefix_except, ['foo_prefix_list'])

    # check on the destination prefix except list
    ret = policy.ParsePolicy(dpol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].destination_prefix_except,
                     ['bar_prefix_list', 'baz_prefix_list'])

  def testPrefixListMixed(self):
    spol = HEADER + GOOD_TERM_40
    dpol = HEADER + GOOD_TERM_41

    # check on the source prefix list with mixed values
    ret = policy.ParsePolicy(spol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].source_prefix, ['foo_prefix_list'])
    self.assertEqual(terms[0].source_prefix_except,
                     ['foo_prefix_list_except'])

    # check on the destination prefix with mixed values
    ret = policy.ParsePolicy(dpol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].destination_prefix, ['bar_prefix_list'])
    self.assertEqual(terms[0].destination_prefix_except,
                     ['bar_prefix_list_except'])

  def testEtherTypes(self):
    pol = HEADER + GOOD_TERM_16
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].ether_type[0], 'arp')
    self.assertEqual(terms[0].ether_type[1], 'ipv4')
    self.assertEqual(terms[0].ether_type[2], 'vlan')

  def testTrafficTypes(self):
    pol = HEADER + GOOD_TERM_17
    ret = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(terms[0].traffic_type[0], 'broadcast')
    self.assertEqual(terms[0].traffic_type[1], 'unknown-unicast')
    self.assertEqual(terms[0].traffic_type[2], 'multicast')

  def testBadProtocolEtherTypes(self):
    pol = HEADER + BAD_TERM_9
    self.assertRaises(policy.TermProtocolEtherTypeError, policy.ParsePolicy,
                      pol, self.naming)

  def testVerbatimTerm(self):
    pol = policy.ParsePolicy(HEADER + GOOD_TERM_18, self.naming)
    _, terms = pol.filters[0]
    self.assertEqual(terms[0].verbatim[0][0], 'iptables')
    self.assertEqual(terms[0].verbatim[0][1], 'mary had a little lamb')
    self.assertEqual(terms[0].verbatim[1][0], 'juniper')
    self.assertEqual(terms[0].verbatim[1][1], 'mary had another lamb')

  def testVerbatimMixed(self):
    pol = HEADER + BAD_TERM_10
    self.assertRaises(policy.ParseError, policy.ParsePolicy, pol, self.naming)

  def testIntegerFilterName(self):
    pol_text = HEADER_3 + GOOD_TERM_0
    pol = policy.ParsePolicy(pol_text, self.naming)
    self.assertEqual(pol.headers[0].target[0].options[0], '50')

  def testPrecedence(self):
    pol_text = HEADER + GOOD_TERM_22
    pol = policy.ParsePolicy(pol_text, self.naming)
    self.assertEqual(len(pol.filters), 1)
    _, terms = pol.filters[0]
    self.assertEqual(terms[0].precedence, [1])

  def testLossPriority(self):
    self.naming.GetServiceByProto.return_value = ['22']

    pol = policy.ParsePolicy(HEADER + GOOD_TERM_23, self.naming)
    self.assertEqual(len(pol.filters), 1)
    _, terms = pol.filters[0]
    self.assertEqual(terms[0].loss_priority, 'low')

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testRoutingInstance(self):
    self.naming.GetServiceByProto.return_value = ['22']

    pol = policy.ParsePolicy(HEADER + GOOD_TERM_24, self.naming)
    self.assertEqual(len(pol.filters), 1)
    _, terms = pol.filters[0]
    self.assertEqual(terms[0].routing_instance, 'foobar-router')

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testSourceInterface(self):
    self.naming.GetServiceByProto.return_value = ['22']

    pol = policy.ParsePolicy(HEADER_4 + GOOD_TERM_25, self.naming)
    self.assertEqual(len(pol.filters), 1)
    header, terms = pol.filters[0]
    self.assertEqual(str(header.target[0]), 'iptables')
    self.assertEqual(terms[0].source_interface, 'foo0')

    self.naming.GetServiceByProto.assert_called_once_with('SSH', 'tcp')

  def testShadingDetection(self):
    pol2 = HEADER + GOOD_TERM_2 + GOOD_TERM_3
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')], [nacaddr.IPv4('10.0.0.0/8')]]
    self.naming.GetServiceByProto.return_value = ['25']

    # same protocol, same saddr, shaded term defines a port.
    self.assertRaises(policy.ShadingError, policy.ParsePolicy, pol2,
                      self.naming, shade_check=True)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('PROD_NETWRK')])
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testVpnConfigWithoutPairPolicy(self):
    pol = policy.ParsePolicy(HEADER_4 + GOOD_TERM_30, self.naming)
    self.assertEqual(len(pol.filters), 1)
    self.assertEqual('special-30', pol.filters[0][1][0].vpn[0])
    self.assertEqual('', pol.filters[0][1][0].vpn[1])

  def testVpnConfigWithPairPolicy(self):
    pol = policy.ParsePolicy(HEADER_4 + GOOD_TERM_31, self.naming)
    self.assertEqual(len(pol.filters), 1)
    self.assertEqual('special-31', pol.filters[0][1][0].vpn[0])
    self.assertEqual('policy-11', pol.filters[0][1][0].vpn[1])

  def testForwardingClassPolicy(self):
    pol = policy.ParsePolicy(HEADER + GOOD_TERM_32, self.naming)
    self.assertEqual(['fritzy'], pol.filters[0][1][0].forwarding_class)

  def testMultipleForwardingClassPolicy(self):
    pol = policy.ParsePolicy(HEADER + GOOD_TERM_36, self.naming)
    self.assertEqual(['flashy', 'fritzy'],
                     pol.filters[0][1][0].forwarding_class)

  def testForwardingClassEqual(self):
    pol_text = HEADER + GOOD_TERM_32 + GOOD_TERM_33
    ret = policy.ParsePolicy(pol_text, self.naming, shade_check=False)
    self.assertEqual(len(ret.filters), 1)
    _, terms = ret.filters[0]
    self.assertEqual(len(terms), 2)
    self.assertNotEqual(terms[0], terms[1])

  def testTagSupportAndNetworkHeaderParsing(self):
    pol = policy.ParsePolicy(HEADER_5 + GOOD_TERM_34, self.naming)
    self.assertEqual(len(pol.filters), 1)
    header, terms = pol.filters[0]
    self.assertEqual(str(header.target[0]), 'gce')
    self.assertEqual(header.FilterOptions('gce'), ['global/networks/default'])
    self.assertEqual(terms[0].source_tag, ['src-tag'])
    self.assertEqual(terms[0].destination_tag, ['dest-tag'])

  def testEq(self):
    """Sanity test to verify __eq__ works on Policy objects."""
    policy1 = policy.ParsePolicy(HEADER_4 + GOOD_TERM_30, self.naming)
    policy2 = policy.ParsePolicy(HEADER_4 + GOOD_TERM_30, self.naming)
    policy3 = policy.ParsePolicy(HEADER_5 + GOOD_TERM_34, self.naming)
    self.assertEqual(policy1, policy2)
    self.assertNotEqual(policy1, policy3)
    self.assertNotEqual(policy2, policy3)

  def testNextIP(self):
    pol = HEADER_2 + GOOD_TERM_35
    expected = nacaddr.IPv4('10.1.1.1/32')
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')], [nacaddr.IPv4('10.1.1.1/32')]]

    result = policy.ParsePolicy(pol, self.naming)
    self.assertEqual(result.filters[0][1][0].next_ip[0], expected)

    self.naming.GetNetAddr.assert_has_calls([
        mock.call('PROD_NETWRK'),
        mock.call('NEXT_IP')])

  def testStr(self):
    """Sanity test to verify __eq__ works on Policy objects."""
    pol = policy.ParsePolicy(HEADER_4 + GOOD_TERM_30, self.naming)
    logging.info('Ensuring string formatting doesn\'t throw errors: %s', pol)

  def testTermAddressByteLength(self):
    """Tests the AddressByteLength function."""
    pol = HEADER + GOOD_TERM_2
    self.naming.GetNetAddr.return_value = [
        nacaddr.IPv4('10.0.0.1/32'), nacaddr.IPv4('10.0.0.2/32'),
        nacaddr.IPv6('2001:4860:4860::8844/128'),
        nacaddr.IPv6('2001:4860:4860::8888/128')]
    ret = policy.ParsePolicy(pol, self.naming)
    term = ret.filters[0][1][0]
    self.assertEqual(2, term.AddressesByteLength([4]))
    self.assertEqual(8, term.AddressesByteLength([6]))
    self.assertEqual(10, term.AddressesByteLength())

  # pylint: enable=maybe-no-member

  def testICMPCodes(self):
    pol = HEADER + GOOD_TERM_42

    result = policy.ParsePolicy(pol, self.naming)
    self.assertIn('icmp_code: [3, 4]', str(result))

  def testBadICMPCodes(self):
    pol = HEADER + BAD_TERM_13
    pol2 = HEADER + BAD_TERM_14
    self.assertRaises(policy.ICMPCodeError, policy.ParsePolicy, pol,
                      self.naming)
    self.assertRaises(policy.ICMPCodeError, policy.ParsePolicy, pol2,
                      self.naming)

  def testOptimizedConsistency(self):
    pol = HEADER + GOOD_TERM_2 + GOOD_TERM_3
    unoptimized_addr = [nacaddr.IPv4('10.16.128.6/32'),
                        nacaddr.IPv4('10.16.128.7/32')]
    optimized_addr = nacaddr.CollapseAddrList(unoptimized_addr)
    self.naming.GetNetAddr.return_value = unoptimized_addr
    self.naming.GetServiceByProto.return_value = ['25']
    ret_unoptimized = policy.ParsePolicy(pol, self.naming, optimize=False)
    self.assertFalse(policy._OPTIMIZE)
    ret_optimized = policy.ParsePolicy(pol, self.naming)
    self.assertTrue(policy._OPTIMIZE)
    for _, terms in ret_unoptimized.filters:
      for term in terms:
        self.assertEqual(unoptimized_addr, term.source_address)
    for _, terms in ret_optimized.filters:
      for term in terms:
        self.assertEqual(optimized_addr, term.source_address)

  def testShadeCheckConsistency(self):
    pol = HEADER + TERM_SUPER_3 + TERM_SUB_2
    self.assertRaises(policy.ShadingError, policy.ParsePolicy, pol, self.naming,
                      shade_check=True)
    self.assertTrue(policy._SHADE_CHECK)
    _ = policy.ParsePolicy(pol, self.naming)
    self.assertFalse(policy._SHADE_CHECK)

  def testEncapsulate(self):
    pol = HEADER + GOOD_TERM_46
    result = policy.ParsePolicy(pol, self.naming)
    self.assertIn('encapsulate: stuff_and_things', str(result))

  def testDecapsulate(self):
    pol = HEADER + GOOD_TERM_49
    result = policy.ParsePolicy(pol, self.naming)
    self.assertIn('decapsulate: mpls-in-udp', str(result))

  def testPortMirror(self):
    pol = HEADER + GOOD_TERM_47
    result = policy.ParsePolicy(pol, self.naming)
    self.assertIn('port_mirror: true', str(result))

  def testSrxGLobalZone(self):
    pol = HEADER + GOOD_TERM_48
    result = policy.ParsePolicy(pol, self.naming)
    zones = ['zone1', 'zone2']
    expected_source = 'source_zone: %s' % zones
    expected_destination = 'destination_zone: %s' % zones
    self.assertIn(expected_source, str(result))
    self.assertIn(expected_destination, str(result))

  def testTTL(self):
    pol = HEADER + GOOD_TERM_43
    result = policy.ParsePolicy(pol, self.naming)
    self.assertIn('ttl: 10', str(result))

  def testInvalidTTL(self):
    pol = HEADER + BAD_TERM_15
    self.assertRaises(policy.InvalidTermTTLValue, policy.ParsePolicy,
                      pol, self.naming)

  def testNeedAddressBook(self):
    pol1 = policy.ParsePolicy(HEADER + GOOD_TERM_1, self.naming)
    pol2 = policy.ParsePolicy(HEADER_SRX + GOOD_TERM_1, self.naming)
    pol3 = policy.ParsePolicy(HEADER_OBJ_GRP + GOOD_TERM_1, self.naming)
    pol4 = policy.ParsePolicy(HEADER_ADDRBOOK_MIXED + GOOD_TERM_1, self.naming)
    self.assertFalse(pol1._NeedsAddressBook())
    self.assertTrue(pol2._NeedsAddressBook())
    self.assertTrue(pol3._NeedsAddressBook())
    self.assertTrue(pol4._NeedsAddressBook())

  def testAddressCleanupCorrect(self):
    unoptimized_addr = [nacaddr.IPv4('10.16.128.6/32', token='FOO'),
                        nacaddr.IPv4('10.16.128.7/32', token='BAR')]
    self.naming.GetNetAddr.return_value = unoptimized_addr
    pol = policy.ParsePolicy(HEADER + GOOD_TERM_2, self.naming)
    term = pol.filters[0][1][0]
    self.assertEqual(nacaddr.CollapseAddrList(unoptimized_addr),
                     term.source_address)
    pol = policy.ParsePolicy(HEADER_SRX + GOOD_TERM_2, self.naming)
    term = pol.filters[0][1][0]
    self.assertEqual(nacaddr.CollapseAddrListPreserveTokens(unoptimized_addr),
                     term.source_address)

  def testLogLimit(self):
    pol = policy.ParsePolicy(HEADER_4 + GOOD_TERM_44, self.naming)
    term = pol.filters[0][1][0]
    self.assertEqual((u'999', u'day'), term.log_limit)

  def testGREandTCPUDPError(self):
    pol = HEADER + BAD_TERM_16
    self.naming.GetServiceByProto.return_value = ['25']
    self.assertRaises(policy.MixedPortandNonPortProtos, policy.ParsePolicy,
                      pol, self.naming)

  def testSourceServiceAccount(self):
    pol = HEADER_HF_1 + GOOD_TERM_50

    result = policy.ParsePolicy(pol, self.naming)
    term = result.filters[0][1][0]
    self.assertEqual(
        ['acct1@blah.com'],
        term.source_service_accounts)

  def testTargetServiceAccount(self):
    pol = HEADER_HF_1 + GOOD_TERM_45

    result = policy.ParsePolicy(pol, self.naming)
    term = result.filters[0][1][0]
    self.assertEqual(
        ['acct1@blah.com'],
        term.target_service_accounts)

  # Contains Tests

  def testVerbatimContains(self):
    term_one = policy.Term(policy.VarType(23, ('iptables', 'foo')))
    term_two = policy.Term(policy.VarType(23, ('iptables', 'bar')))
    term_three = policy.Term(policy.VarType(23, ('juniper', 'foo')))
    self.assertIn(term_one, term_one)
    self.assertNotIn(term_two, term_one)
    self.assertNotIn(term_three, term_one)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testIpAndPortContains(self, mock_naming):
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.1.1.1/32')]]
    term_one = policy.Term([policy.VarType(3, 'PROD'),
                            policy.VarType(7, (22, 22)),
                            policy.VarType(7, (80, 80)),
                            policy.VarType(10, 'tcp')])
    term_one.AddObject(policy.VarType(2, 'accept'))
    term_two = policy.Term([policy.VarType(3, 'SMALLER_PROD'),
                            policy.VarType(7, (22, 22)),
                            policy.VarType(10, 'tcp')])
    term_two.AddObject(policy.VarType(2, 'accept'))
    self.assertIn(term_two, term_one)
    self.assertNotIn(term_one, term_two)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testEmptyIpContains(self, mock_naming):
    # testTermContains2 differs from testTermContains in that TERM_SUPER_2
    # only defines a source addres. it's meant to catch the case where
    # the containing term has less detail (and is hence, less restrictive)
    # than the contained term
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.1.1.1/32')]]
    term_one = policy.Term([policy.VarType(5, 'PROD')])
    term_one.AddObject(policy.VarType(2, 'accept'))
    term_two = policy.Term([policy.VarType(3, 'SMALLER_PROD'),
                            policy.VarType(7, (22, 22))])
    term_two.AddObject(policy.VarType(2, 'accept'))
    self.assertIn(term_two, term_one)
    self.assertNotIn(term_one, term_two)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testIpExcludeContains(self, mock_naming):
    # This 'contains' test kicks the tires on source-address and
    # source-address-exclude.
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.62.0.0/15')]]
    term_one = policy.Term([policy.VarType(3, 'FOO')])
    term_two = policy.Term([policy.VarType(3, 'FOO'),
                            policy.VarType(11, 'BAR')])
    self.assertIn(term_two, term_one)
    self.assertNotIn(term_one, term_two)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testIpDualExcludeContains(self, mock_naming):
    # One term has (10.0.0.0/8, except 10.10.0.0/24), it should contain a term
    # that has (10.0.0.0/8 except 10.0.0.0/9.
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.10.0.0/24')],
        [nacaddr.IPv4('10.0.0.0/8')],
        [nacaddr.IPv4('10.0.0.0/9')]]
    term_one = policy.Term([policy.VarType(3, 'FOO'),
                            policy.VarType(11, 'BAR')])
    term_two = policy.Term([policy.VarType(3, 'FOO'),
                            policy.VarType(11, 'BAR')])
    self.assertIn(term_two, term_one)
    self.assertNotIn(term_one, term_two)

  def testOptionsContains(self):
    # Tests 'contains' testing of the options field. A term without set options
    # contains one which has them set.
    tcp_est_term = policy.Term([policy.VarType(9, 'tcp-established')])
    term = policy.Term([])
    tcp_udp_est_term = policy.Term([policy.VarType(9, 'tcp-established'),
                                    policy.VarType(9, 'established')])
    self.assertNotIn(term, tcp_est_term)
    self.assertNotIn(tcp_est_term, term)
    self.assertIn(tcp_est_term, tcp_udp_est_term)
    self.assertNotIn(tcp_udp_est_term, tcp_est_term)

  def testPrecedenceContains(self):
    # Tests 'contains' testing of the precedence field. A term without set
    # precedence contains one which has them set.
    p_term = policy.Term([policy.VarType(26, 1)])
    no_p_term = policy.Term([])
    self.assertIn(p_term, p_term)
    self.assertIn(no_p_term, no_p_term)
    self.assertNotIn(no_p_term, p_term)
    self.assertNotIn(p_term, no_p_term)

  def testProtocolExceptContains(self):
    # Test the protocol-except keyword.
    pexcept_term = policy.Term([policy.VarType(8, 'tcp')])
    pexpect_term_udp = policy.Term([policy.VarType(8, 'udp')])
    p_term = policy.Term([policy.VarType(10, 'icmp')])
    p_term_tcp = policy.Term([policy.VarType(10, 'tcp')])
    self.assertIn(p_term, pexcept_term)
    self.assertIn(pexcept_term, pexcept_term)
    self.assertNotIn(p_term_tcp, pexcept_term)
    self.assertNotIn(pexpect_term_udp, pexcept_term)

  def testProtocolTermNotInAnotherTermContains(self):
    term_one = policy.Term([policy.VarType(10, 'tcp')])
    term_two = policy.Term([policy.VarType(10, 'udp')])
    self.assertNotIn(term_one, term_two)

  def testTargetServiceAccountContains(self):
    two_target_sa = ['acct1@blah.com', 'acct2@blah.com']
    one_target_sa = ['acct3@blah.com']

    term = policy.Term([policy.VarType(60, two_target_sa)])
    self.assertIn(two_target_sa, term.target_service_accounts)

    term.AddObject(policy.VarType(60, one_target_sa))
    self.assertIn(one_target_sa, term.target_service_accounts)

  def testProtoExceptNotInEmptyTerm(self):
    term_one = policy.Term([policy.VarType(8, 'tcp')])
    term_two = policy.Term([])
    self.assertNotIn(term_two, term_one)

  def testProtocolNotInProtoExcept(self):
    term_one = policy.Term([policy.VarType(8, 'tcp')])
    term_two = policy.Term([policy.VarType(10, 'udp')])
    self.assertNotIn(term_one, term_two)

  def testProtocolNotInEmptyTerm(self):
    term_one = policy.Term([policy.VarType(10, 'tcp')])
    term_two = policy.Term([])
    self.assertNotIn(term_two, term_one)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testAddrNotInAddr(self, mock_naming):
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('192.168.1.1/32')],
        [nacaddr.IPv4('10.1.1.0/24')],
        [nacaddr.IPv4('10.1.1.0/24')],
        [nacaddr.IPv4('10.1.1.0/24')]]
    term = policy.Term([policy.VarType(5, 'FOO')])
    addr_term = policy.Term([policy.VarType(5, 'FOO')])
    saddr_term = policy.Term([policy.VarType(3, 'FOO')])
    daddr_term = policy.Term([policy.VarType(4, 'FOO')])
    self.assertNotIn(addr_term, term)
    self.assertNotIn(saddr_term, term)
    self.assertNotIn(daddr_term, term)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testDestAddrNotInDestAddr(self, mock_naming):
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('192.168.1.1/32')],
        [nacaddr.IPv4('10.1.1.0/24')]]
    term_one = policy.Term([policy.VarType(4, 'FOO')])
    term_two = policy.Term([policy.VarType(4, 'FOO')])
    self.assertNotIn(term_one, term_two)

  def testSourcePortNotInSourcePort(self):
    term_one = policy.Term([policy.VarType(6, (22, 22))])
    term_two = policy.Term([policy.VarType(6, (23, 23))])
    self.assertNotIn(term_one, term_two)

  def testDestinationPortNotInDestinationPort(self):
    term_one = policy.Term([policy.VarType(7, (22, 22))])
    term_two = policy.Term([policy.VarType(7, (23, 23))])
    self.assertNotIn(term_one, term_two)

  def testSourcePrefixContains(self):
    term_one = policy.Term([policy.VarType(19, 'foo')])
    self.assertIn(term_one, term_one)

  def testSourcePrefixNotInSourcePrefix(self):
    term_one = policy.Term([policy.VarType(19, 'foo')])
    term_two = policy.Term([policy.VarType(19, 'bar')])
    self.assertNotIn(term_one, term_two)

  def testDestinationPrefixContains(self):
    term_one = policy.Term([policy.VarType(20, 'foo')])
    self.assertIn(term_one, term_one)

  def testDestinationPrefixNotInDestinationPrefix(self):
    term_one = policy.Term([policy.VarType(20, 'foo')])
    term_two = policy.Term([policy.VarType(20, 'bar')])
    self.assertNotIn(term_one, term_two)

  def testSourcePrefixExceptContains(self):
    term_one = policy.Term([policy.VarType(50, 'foo')])
    self.assertIn(term_one, term_one)

  def testSourcePrefixExceptNotInSourcePrefixExcept(self):
    term_one = policy.Term([policy.VarType(50, 'foo')])
    term_two = policy.Term([policy.VarType(50, 'bar')])
    self.assertNotIn(term_one, term_two)

  def testDestinationPrefixExceptContains(self):
    term_one = policy.Term([policy.VarType(51, 'foo')])
    self.assertIn(term_one, term_one)

  def testDestinationPrefixExceptNotInDestinationPrefixExcept(self):
    term_one = policy.Term([policy.VarType(51, 'foo')])
    term_two = policy.Term([policy.VarType(51, 'bar')])
    self.assertNotIn(term_one, term_two)

  def testSourceTagContains(self):
    term_one = policy.Term([policy.VarType(44, 'foo')])
    self.assertIn(term_one, term_one)

  def testSourceTagNotInSourceTag(self):
    term_one = policy.Term([policy.VarType(44, 'foo')])
    term_two = policy.Term([policy.VarType(44, 'bar')])
    self.assertNotIn(term_one, term_two)

  def testForwardingClassContains(self):
    term_one = policy.Term([policy.VarType(43, 'foo')])
    term_two = policy.Term(
        [policy.VarType(43, 'bar'),
         policy.VarType(43, 'foo')])
    self.assertIn(term_one, term_one)
    self.assertIn(term_one, term_two)

  def testForwardingClassNotIn(self):
    term_one = policy.Term([policy.VarType(43, 'foo')])
    term_two = policy.Term([policy.VarType(43, 'bar')])
    term_three = policy.Term([])
    self.assertNotIn(term_one, term_two)
    self.assertNotIn(term_three, term_one)

  def testForwardingClassExceptContains(self):
    term_one = policy.Term([policy.VarType(52, 'foo')])
    self.assertIn(term_one, term_one)

  def testForwardingClassExceptNotIn(self):
    term_one = policy.Term([policy.VarType(52, 'foo')])
    term_two = policy.Term([policy.VarType(52, 'bar')])
    term_three = policy.Term([])
    self.assertNotIn(term_one, term_two)
    self.assertNotIn(term_three, term_one)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testNextIPContained(self, mock_naming):
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('192.168.1.1/32')]]
    term_one = policy.Term([policy.VarType(46, 'FOO')])
    self.assertIn(term_one, term_one)

  @mock.patch.object(policy, 'DEFINITIONS')
  def testNextIPNotIn(self, mock_naming):
    mock_naming.GetNetAddr.side_effect = [
        [nacaddr.IPv4('192.168.1.1/32')]]
    term_one = policy.Term([policy.VarType(46, 'FOO')])
    term_two = policy.Term([])
    self.assertNotIn(term_two, term_one)

  def testPortContains(self):
    # Test 'contains' against port field and that it matches
    # source/destination/port fields.
    port_term = policy.Term([policy.VarType(32, (25, 25))])
    sport_term = policy.Term([policy.VarType(6, (25, 25))])
    dport_term = policy.Term([policy.VarType(7, (25, 25))])
    self.assertIn(sport_term, port_term)
    self.assertIn(dport_term, port_term)
    self.assertIn(port_term, port_term)
    alt_port_term = policy.Term([policy.VarType(32, (25, 30))])
    sport_term = policy.Term([policy.VarType(6, (25, 30))])
    dport_term = policy.Term([policy.VarType(7, (25, 30))])
    self.assertNotIn(alt_port_term, port_term)
    self.assertNotIn(sport_term, port_term)
    self.assertNotIn(dport_term, port_term)

  def testFragmentOffset(self):
    fo_term = policy.Term([])
    fo_term.AddObject(policy.VarType(17, '80'))
    fo_range_term = policy.Term([])
    fo_range_term.AddObject(policy.VarType(17, '60-90'))
    fo_smaller_range_term = policy.Term([])
    fo_smaller_range_term.AddObject(policy.VarType(17, '65-82'))
    term = policy.Term([])

    self.assertIn(fo_term, fo_term)
    self.assertIn(fo_term, fo_range_term)
    self.assertNotIn(fo_range_term, fo_term)
    self.assertIn(fo_smaller_range_term, fo_range_term)
    self.assertNotIn(fo_range_term, fo_smaller_range_term)
    self.assertNotIn(term, fo_term)

  def testTermTargetResources(self):
    target_resources = [('p1', 'v1'), ('p2', 'v2')]
    target_resource_2 = [('p3', 'v3')]
    term_one = policy.Term(
        [policy.VarType(policy.VarType.TARGET_RESOURCES, target_resources)])
    term_one.AddObject(policy.VarType(59, target_resource_2))

    self.assertIn(target_resources, term_one.target_resources)
    self.assertIn(target_resource_2, term_one.target_resources)

  def testParsePolicySingleTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: (proj1,vpc1)
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    self.assertIn(('proj1', 'vpc1'), terms[0].target_resources)

  def testParsePolicyMultipleTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: (proj1,vpc1)
      target-resources:: (proj2,vpc2)
      target-resources:: (proj3,vpc3)
      target-resources:: (proj4,vpc4)
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    expected_target_resources = [('proj1', 'vpc1'), ('proj2', 'vpc2'),
                                 ('proj3', 'vpc3'), ('proj4', 'vpc4')]
    self.assertListEqual(expected_target_resources, terms[0].target_resources)

  def testParsePolicyMultipleCommaSepTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: (proj1,vpc1),(proj2,vpc2),(proj3,vpc3),(proj4,vpc4)
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    expected_target_resources = [('proj1', 'vpc1'), ('proj2', 'vpc2'),
                                 ('proj3', 'vpc3'), ('proj4', 'vpc4')]
    self.assertListEqual(expected_target_resources, terms[0].target_resources)

  def testParsePolicyMultipleSpaceSepTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: (proj1,vpc1) (proj2,vpc2) (proj3,vpc3) (proj4,vpc4)
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    expected_target_resources = [('proj1', 'vpc1'), ('proj2', 'vpc2'),
                                 ('proj3', 'vpc3'), ('proj4', 'vpc4')]
    self.assertListEqual(expected_target_resources, terms[0].target_resources)

  def testParsePolicyMultipleArrayCommaTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: [(proj1,vpc1),(proj2,vpc2),(proj3,vpc3),(proj4,vpc4)]
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    expected_target_resources = [('proj1', 'vpc1'), ('proj2', 'vpc2'),
                                 ('proj3', 'vpc3'), ('proj4', 'vpc4')]
    self.assertListEqual(expected_target_resources, terms[0].target_resources)

  def testParsePolicyMultipleArraySpaceTargetResources(self):
    good_term_target_resources = """
    term target-resource-term {
      action:: deny
      target-resources:: [(proj1,vpc1) (proj2,vpc2) (proj3,vpc3) (proj4,vpc4)]
    }"""
    pol = HEADER_HF_1 + good_term_target_resources
    p = policy.ParsePolicy(pol, self.naming)
    self.assertIsInstance(p, policy.Policy)

    _, terms = p.filters[0]
    self.assertIn('deny', terms[0].action)
    expected_target_resources = [('proj1', 'vpc1'), ('proj2', 'vpc2'),
                                 ('proj3', 'vpc3'), ('proj4', 'vpc4')]
    self.assertListEqual(expected_target_resources, terms[0].target_resources)


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


# tests/lib/sonic_test.py

# Copyright 2022 Google Inc. All Rights Reserved.
#
# 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.
"""Unittest for Sonic rendering module."""

import json
from unittest import mock

from absl.testing import absltest
from absl.testing import parameterized
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import sonic

GOOD_HEADER = """
header {
  comment:: "The general policy comment."
  target:: sonic MyPolicyName inet
}
"""


class SonicTest(parameterized.TestCase):

  def setUp(self):
    super().setUp()
    self.addCleanup(mock.patch.stopall)

    self.naming = naming.Naming("./def")
    self.mock_naming_get_net_addr = mock.patch.object(
        self.naming, "GetNetAddr", autospec=True).start()
    self.mock_naming_get_net_addr.return_value = [
        nacaddr.IP("10.2.3.4/32"),
        nacaddr.IP("2001:4860:8000::5/128"),
    ]
    # Print a info message when a term is set to expire in that many weeks.
    # This is normally passed from command line.
    self.exp_info = 2

  def testSingleSrcIPv4(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      action:: accept
    }
    """
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "PRIORITY": "65526",
          "PACKET_ACTION": "FORWARD",
          "SRC_IP": "10.2.3.4/32"
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))
    self.mock_naming_get_net_addr.assert_called_once_with("CORP_EXTERNAL")

  def testSingleSrcSingleDstIPv4(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      destination-address:: CORP_EXTERNAL
      action:: accept
    }
    """
    expected = json.loads("""{
    "ACL_RULE": {
      "MyPolicyName|RULE_10": {
        "PRIORITY": "65526",
        "PACKET_ACTION": "FORWARD",
        "SRC_IP": "10.2.3.4/32",
        "DST_IP": "10.2.3.4/32"
      }
    }
    }""")

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testSingleSrcSingleDstIPv6(self):
    header = """
    header {
      comment:: "The general policy comment."
      target:: sonic MyPolicyName inet6
    }
    """
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      destination-address:: CORP_EXTERNAL
      action:: accept
    }
    """
    expected = json.loads("""{
    "ACL_RULE": {
      "MyPolicyName|RULE_10": {
        "PRIORITY": "65526",
        "PACKET_ACTION": "FORWARD",
        "SRC_IPV6": "2001:4860:8000::5/128",
        "DST_IPV6": "2001:4860:8000::5/128"
      }
    }
    }""")

    acl = sonic.Sonic(
        policy.ParsePolicy(header + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testMultiSrcIPv4(self):
    self.mock_naming_get_net_addr.return_value = [
        nacaddr.IP("10.2.3.4/32"),
        nacaddr.IP("4.4.4.4/32"),
    ]
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      action:: accept
    }
    """
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "PRIORITY": "65526",
          "PACKET_ACTION": "FORWARD",
          "SRC_IP": "4.4.4.4/32"
        },
        "MyPolicyName|RULE_20": {
          "PRIORITY": "65516",
          "PACKET_ACTION": "FORWARD",
          "SRC_IP": "10.2.3.4/32"
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testMultiSrcMultiDstIPv4(self):
    self.mock_naming_get_net_addr.return_value = [
        nacaddr.IP("10.2.3.4/32"),
        nacaddr.IP("4.4.4.4/32"),
    ]
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "DST_IP": "4.4.4.4/32",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65526",
          "SRC_IP": "4.4.4.4/32"
        },
        "MyPolicyName|RULE_20": {
          "DST_IP": "10.2.3.4/32",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65516",
          "SRC_IP": "4.4.4.4/32"
        },
        "MyPolicyName|RULE_30": {
          "DST_IP": "4.4.4.4/32",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65506",
          "SRC_IP": "10.2.3.4/32"
        },
        "MyPolicyName|RULE_40": {
          "DST_IP": "10.2.3.4/32",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65496",
          "SRC_IP": "10.2.3.4/32"
        }
      }
    }
    """)
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      destination-address:: CORP_EXTERNAL
      action:: accept
    }
    """
    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testDrop(self):
    pol = """
    term good-term-1 {
      action:: deny
    }
    """

    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "PRIORITY": "65526",
          "PACKET_ACTION": "DROP"
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testMultiTerm(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      action:: accept
    }
    term good-term-2 {
      destination-address:: CORP_EXTERNAL
      action:: accept
    }
    """

    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "PRIORITY": "65526",
          "PACKET_ACTION": "FORWARD",
          "SRC_IP": "10.2.3.4/32"
        },
        "MyPolicyName|RULE_20": {
          "PRIORITY": "65516",
          "PACKET_ACTION": "FORWARD",
          "DST_IP": "10.2.3.4/32"
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testProtocols(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: tcp udp
      action:: accept
    }
    """
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "IP_PROTOCOL": "6",
          "PRIORITY": "65526",
          "PACKET_ACTION": "FORWARD",
          "SRC_IP": "10.2.3.4/32"
        },
        "MyPolicyName|RULE_20": {
          "IP_PROTOCOL": "17",
          "PRIORITY": "65516",
          "PACKET_ACTION": "FORWARD",
          "SRC_IP": "10.2.3.4/32"
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testICMPv4(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: icmp
      icmp-type:: echo-request echo-reply
      action:: accept
    }
    """
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "ICMP_TYPE": "0",
          "IP_PROTOCOL": "1",
          "PRIORITY": "65526",
          "PACKET_ACTION": "FORWARD",
          "SRC_IP": "10.2.3.4/32"
        },
        "MyPolicyName|RULE_20": {
          "ICMP_TYPE": "8",
          "IP_PROTOCOL": "1",
          "PRIORITY": "65516",
          "PACKET_ACTION": "FORWARD",
          "SRC_IP": "10.2.3.4/32"
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testICMPv6(self):
    header = """
    header {
      comment:: "The general policy comment."
      target:: sonic MyPolicyName inet6
    }
    """
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: icmpv6
      icmp-type:: echo-request echo-reply
      action:: accept
    }
    """
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "ICMPV6_TYPE": "128",
          "IP_PROTOCOL": "58",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65526",
          "SRC_IPV6": "2001:4860:8000::5/128"
        },
        "MyPolicyName|RULE_20": {
          "ICMPV6_TYPE": "129",
          "IP_PROTOCOL": "58",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65516",
          "SRC_IPV6": "2001:4860:8000::5/128"
        }
      }
    }""")

    acl = sonic.Sonic(
        policy.ParsePolicy(header + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testICMPv4DoesNotRenderIPv6(self):
    mixed_af_header = """
    header {
      comment:: "The general policy comment."
      target:: sonic MyPolicyName inet inet6
    }
    """
    self.mock_naming_get_net_addr.return_value = [
        nacaddr.IP("2001:a:b:c::/128"),
    ]
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: icmp
      icmp-type:: echo-request
      action:: accept
    }
    """

    acl = sonic.Sonic(
        policy.ParsePolicy(mixed_af_header + pol, self.naming), self.exp_info)

    self.assertEqual({"ACL_RULE": {}}, json.loads(str(acl)))

  def testICMPv6DoesNotRenderIPv4(self):
    mixed_af_header = """
    header {
      comment:: "The general policy comment."
      target:: sonic MyPolicyName inet inet6
    }
    """
    self.mock_naming_get_net_addr.return_value = [
        nacaddr.IP("1.2.3.4/32"),
    ]
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: icmpv6
      icmp-type:: echo-request
      action:: accept
    }
    """

    acl = sonic.Sonic(
        policy.ParsePolicy(mixed_af_header + pol, self.naming), self.exp_info)

    self.assertEqual({"ACL_RULE": {}}, json.loads(str(acl)))

  def testSrcPortSingle(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: tcp
      source-port:: SSH HTTPS
      action:: accept
    }
    """
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "IP_PROTOCOL": "6",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65526",
          "SRC_IP": "10.2.3.4/32",
          "L4_SRC_PORT": "22"
        },
        "MyPolicyName|RULE_20": {
          "IP_PROTOCOL": "6",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65516",
          "SRC_IP": "10.2.3.4/32",
          "L4_SRC_PORT": "443"
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testSrcPortRange(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: udp
      source-port:: TRACEROUTE
      action:: accept
    }
    """
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "IP_PROTOCOL": "17",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65526",
          "SRC_IP": "10.2.3.4/32",
          "L4_SRC_PORT_RANGE": "33434-33534"
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testDstPortSingle(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: tcp
      destination-port:: SSH HTTPS
      action:: accept
    }
    """
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "IP_PROTOCOL": "6",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65526",
          "SRC_IP": "10.2.3.4/32",
          "L4_DST_PORT": "22"
        },
        "MyPolicyName|RULE_20": {
          "IP_PROTOCOL": "6",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65516",
          "SRC_IP": "10.2.3.4/32",
          "L4_DST_PORT": "443"
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testDstPortRange(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: udp
      destination-port:: TRACEROUTE
      action:: accept
    }
    """
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "IP_PROTOCOL": "17",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65526",
          "SRC_IP": "10.2.3.4/32",
          "L4_DST_PORT_RANGE": "33434-33534"
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testTCPEstablished(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: tcp
      option:: tcp-established
      action:: accept
    }
    """
    expected = json.loads("""
    {
      "ACL_RULE": {
        "MyPolicyName|RULE_10": {
          "IP_PROTOCOL": "6",
          "PACKET_ACTION": "FORWARD",
          "PRIORITY": "65526",
          "SRC_IP": "10.2.3.4/32",
          "TCP_FLAGS": [
            "0x10/0x10",
            "0x4/0x4"
          ]
        }
      }
    }
    """)

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual(expected, json.loads(str(acl)))

  def testTermWithWrongPlatform(self):
    pol = """
    term good-term-1 {
      source-address:: CORP_EXTERNAL
      protocol:: tcp
      source-port:: SSH
      action:: accept
      platform:: FAKEPLATFORM
    }
    """

    acl = sonic.Sonic(
        policy.ParsePolicy(GOOD_HEADER + pol, self.naming), self.exp_info)

    self.assertEqual({"ACL_RULE": {}}, json.loads(str(acl)))


if __name__ == "__main__":
  absltest.main()


# tests/lib/speedway_test.py

# Copyright 2008 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for Speedway rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import speedway


GOOD_HEADER_1 = """
header {
  comment:: "this is a test acl"
  target:: speedway INPUT ACCEPT
}
"""

GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_2 = """
term good-term-2 {
  protocol:: icmp
  policer:: batman
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'counter',
    'destination_address',
    'destination_address_exclude',
    'destination_interface',
    'destination_port',
    'destination_prefix',
    'expiration',
    'fragment_offset',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'log_limit',
    'name',
    'option',
    'owner',
    'packet_length',
    'platform',
    'platform_exclude',
    'protocol',
    'routing_instance',
    'source_address',
    'source_address_exclude',
    'source_interface',
    'source_port',
    'source_prefix',
    'translated',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'first-fragment',
               'initial',
               'sample',
               'tcp-established',
               'tcp-initial',
               'syn',
               'ack',
               'fin',
               'rst',
               'urg',
               'psh',
               'all',
               'none'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class SpeedwayTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testSpeedwayOutputFormat(self):
    acl = speedway.Speedway(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                               self.naming), EXP_INFO)
    result = []
    result.extend(str(acl).split('\n'))
    self.assertEqual('*filter', result[0],
                     '*filter designation does not appear at top of generated '
                     'policy.')
    self.assertIn(':INPUT ACCEPT', result,
                  'input default policy of accept not set.')
    self.assertIn('-N I_good-term-1', result,
                  'did not find new chain for good-term-1.')
    self.assertIn(
        '-A I_good-term-1 -p icmp -m state --state NEW,ESTABLISHED,RELATED'
        ' -j ACCEPT', result, 'did not find append for good-term-1.')
    self.assertEqual('COMMIT', result[len(result)-2],
                     'COMMIT does not appear at end of output policy.')

  def testBuildTokens(self):
    pol1 = speedway.Speedway(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                                self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = speedway.Speedway(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2,
                                                self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


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


# tests/lib/srxlo_test.py

# Copyright 2014 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for Srxlo rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import srxlo


GOOD_HEADER_1 = """
header {
  comment:: "this is a test acl"
  target:: srxlo test-filter inet6
}
"""
GOOD_HEADER_2 = """
header {
  comment:: "this is a test acl"
  target:: srxlo test-filter inet
}
"""
GOOD_TERM_1 = """
term good-term-1 {
  protocol:: icmpv6
  action:: accept
}
"""
GOOD_TERM_2 = """
term good-term-2 {
  protocol:: icmpv6
  icmp-type:: destination-unreachable
  action:: accept
}
"""
GOOD_TERM_3 = """
term good-term-3 {
  protocol:: icmpv6
  action:: accept
  option:: inactive
}
"""
GOOD_TERM_4 = """
term good-term-4 {
  protocol:: icmp
  action:: accept
}
"""
GOOD_TERM_5 = """
term good-term-5 {
  protocol-except:: icmp
  action:: accept
}
"""
GOOD_TERM_6 = """
term good-term-6 {
  protocol-except:: icmpv6
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'address',
    'comment',
    'counter',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'destination_prefix',
    'destination_prefix_except',
    'dscp_except',
    'dscp_match',
    'dscp_set',
    'ether_type',
    'expiration',
    'filter_term',
    'forwarding_class',
    'forwarding_class_except',
    'fragment_offset',
    'hop_limit',
    'icmp_code',
    'icmp_type',
    'stateless_reply',
    'logging',
    'loss_priority',
    'name',
    'next_ip',
    'option',
    'owner',
    'packet_length',
    'platform',
    'platform_exclude',
    'policer',
    'port',
    'precedence',
    'protocol',
    'protocol_except',
    'qos',
    'restrict_address_family',
    'routing_instance',
    'source_address',
    'source_address_exclude',
    'source_port',
    'source_prefix',
    'source_prefix_except',
    'traffic_class_count',
    'traffic_type',
    'translated',
    'ttl',
    'verbatim',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny', 'reject', 'next', 'reject-with-tcp-rst'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request',
        'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request',
        'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
    'option': {'established',
               'first-fragment',
               'inactive',
               'is-fragment',
               '.*',  # not actually a lex token!
               'sample',
               'tcp-established',
               'tcp-initial'}
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class SRXloTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testIcmp(self):
    output = str(srxlo.SRXlo(policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_1 +
                                                GOOD_TERM_4, self.naming),
                 EXP_INFO))
    self.assertIn('protocol icmp;', output,
                  'missing or incorrect ICMP specification')
    self.assertNotIn('icmp6;', output,
                     'missing or incorrect ICMP specification')
    self.assertNotIn('icmpv6;', output,
                     'missing or incorrect ICMP specification')

  def testIcmpv6(self):
    output = str(srxlo.SRXlo(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1 +
                                                GOOD_TERM_4, self.naming),
                 EXP_INFO))
    self.assertIn('next-header icmp6;', output,
                  'missing or incorrect ICMPv6 specification')
    self.assertNotIn('icmp;', output,
                     'missing or incorrect ICMPv6 specification')

  def testIcmpv6Type(self):
    output = str(srxlo.SRXlo(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_2,
                                                self.naming), EXP_INFO))
    self.assertIn('next-header icmp6;', output,
                  'missing or incorrect ICMPv6 specification')
    self.assertIn('icmp-type 1;', output,
                  'missing or incorrect ICMPv6 type specification')

  def testBuildTokens(self):
    # self.naming.GetServiceByProto.side_effect = [['25'], ['26']]
    pol1 = srxlo.SRXlo(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_1,
                                          self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.max_diff = None
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = srxlo.SRXlo(policy.ParsePolicy(
        GOOD_HEADER_1 + GOOD_TERM_1, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testInactiveTerm(self):
    output = str(srxlo.SRXlo(policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_3,
                                                self.naming), EXP_INFO))
    self.assertIn('inactive: term good-term-3 {', output)

  def testIcmpExcept(self):
    output = str(
        srxlo.SRXlo(
            policy.ParsePolicy(GOOD_HEADER_2 + GOOD_TERM_5, self.naming),
            EXP_INFO))
    self.assertIn('protocol-except icmp;', output,
                  'missing or incorrect ICMP specification in protocol-except')
    self.assertNotIn(
        'icmp6;', output,
        'missing or incorrect ICMP specification in protocol-except')
    self.assertNotIn(
        'icmpv6;', output,
        'missing or incorrect ICMP specification in protocol-except')

  def testIcmpv6Except(self):
    output = str(
        srxlo.SRXlo(
            policy.ParsePolicy(GOOD_HEADER_1 + GOOD_TERM_6, self.naming),
            EXP_INFO))
    self.assertIn(
        'next-header-except icmp6;', output,
        'missing or incorrect ICMPv6 specification in protocol-except')
    self.assertNotIn(
        'icmp;', output,
        'missing or incorrect ICMPv6 specification in protocol-except')


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


# tests/lib/summarizer_test.py

# Copyright 2014 Google Inc. All Rights Reserved.
#
# 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 discontinuous subnet mask summarizer."""

import os

import random
import time
from absl.testing import absltest

from absl import logging
from capirca.lib import nacaddr
from capirca.lib import summarizer


class SummarizerTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    random_seed = int(time.time())
    value = os.environ.get('TEST_RANDOM_SEED', '')
    try:
      random_seed = int(value)
    except ValueError:
      pass
    logging.info('Seeding random generator with seed %d', random_seed)
    random.seed(random_seed)

  def testToDottedQuad(self):
    net = summarizer.DSMNet(1 << 32, 4294967264)
    self.assertRaises(ValueError)
    net = summarizer.DSMNet(3232235584, 1 << 16)
    self.assertRaises(ValueError)
    net = summarizer.DSMNet(3232235584, 4294967264)
    self.assertEqual(summarizer.ToDottedQuad(net),
                     ('192.168.0.64', '255.255.255.224'))
    net = summarizer.DSMNet(3232235584, 4294901984)
    self.assertEqual(summarizer.ToDottedQuad(net, negate=True),
                     ('192.168.0.64', '0.0.255.31'))

    test_data = [(summarizer.DSMNet(3232235584, 4294967295), True,
                  ('192.168.0.64', '32')),
                 (summarizer.DSMNet(3232235584, 4294901760), True,
                  ('192.168.0.64', '16')),
                 (summarizer.DSMNet(3232235584, 4294967294), True,
                  ('192.168.0.64', '31')),
                 (summarizer.DSMNet(3232235584, 4290772992), True,
                  ('192.168.0.64', '10')),
                 (summarizer.DSMNet(3232235584, 4294966016), True,
                  ('192.168.0.64', '255.255.251.0')),
                 (summarizer.DSMNet(3232235584, 4294901504), True,
                  ('192.168.0.64', '255.254.255.0'))]

    for net, nondsm, expected in test_data:
      self.assertEqual(summarizer.ToDottedQuad(net, nondsm=nondsm), expected)

  def testInt32ToDottedQuad(self):
    self.assertEqual(summarizer._Int32ToDottedQuad(3232235584),
                     '192.168.0.64')

  def testSummarizeEmptyList(self):
    nets = []
    result = summarizer.Summarize(nets)
    self.assertEqual(result, [])

  def testSummarizeNoNetworks(self):
    nets = []
    for octet in range(0, 256):
      net = nacaddr.IPv4('192.' + str(255 - octet) + '.' +
                         str(octet) + '.64/27')
      nets.append(net)
    random.shuffle(nets)
    result = summarizer.Summarize(nets)
    self.assertEqual(len(result), 256)

  def testSummarizeSomeNetworks(self):
    nets = [
        # continiously summarizable to one /25
        nacaddr.IPv4('192.168.0.0/27'),
        nacaddr.IPv4('192.168.0.32/27'),
        nacaddr.IPv4('192.168.0.64/27'),
        nacaddr.IPv4('192.168.0.96/27'),
        # discontiniously summarizable with above
        nacaddr.IPv4('128.168.0.0/25'),
        # not summarizable with above
        nacaddr.IPv4('10.0.0.0/8'),
    ]
    for octet in range(0, 256):
      net = nacaddr.IPv4('172.16.' + str(octet) + '.96/30')
      nets.append(net)
    random.shuffle(nets)
    result = summarizer.Summarize(nets)
    self.assertEqual(result, [summarizer.DSMNet(167772160, 4278190080),
                              summarizer.DSMNet(2158493696, 3221225344),
                              summarizer.DSMNet(2886729824, 4294902012)])

  def testSummarizeAllNetworks(self):
    nets = []
    for octet in range(0, 256):
      net = nacaddr.IPv4('192.168.' + str(octet) + '.64/27')
      nets.append(net)
    random.shuffle(nets)
    result = summarizer.Summarize(nets)
    # summarizes to 192.168.0.64 / 255.255.0.224
    self.assertEqual(result, [summarizer.DSMNet(3232235584, 4294901984)])

  def testSummarizeToAllSpace(self):
    nets = [
        nacaddr.IPv4('0.0.0.0/1'),
        nacaddr.IPv4('128.0.0.0/1'),
    ]
    random.shuffle(nets)
    result = summarizer.Summarize(nets)
    self.assertEqual(result, [summarizer.DSMNet(0, 0)])

  def testNacaddrNetToDSMNet(self):
    nacaddr_net = nacaddr.IPv4('192.168.0.64/27')
    dsm_net = summarizer.DSMNet(3232235584, 4294967264, '')
    self.assertEqual(summarizer._NacaddrNetToDSMNet(nacaddr_net), dsm_net)

  def testToPrettyBinaryFormat(self):
    # 192.168.0.64
    self.assertEqual(summarizer._ToPrettyBinaryFormat(3232235584),
                     '11000000 10101000 00000000 01000000')
    # 8.8.8.8
    self.assertEqual(summarizer._ToPrettyBinaryFormat(134744072),
                     '00001000 00001000 00001000 00001000')
    # 0.0.0.0
    self.assertEqual(summarizer._ToPrettyBinaryFormat(0),
                     '00000000 00000000 00000000 00000000')
    # fc00::1
    self.assertEqual(
        summarizer._ToPrettyBinaryFormat(
            334965454937798799971759379190646833153),
        '11111100 00000000 00000000 00000000 00000000 00000000 00000000 '
        '00000000 00000000 00000000 00000000 00000000 00000000 00000000 '
        '00000000 00000001')

  def testSummarizeDSMONetworks(self):
    fourth_octet = [2, 8, 20, 26, 28, 32, 40, 52, 58, 86, 130, 136, 148,
                    154, 156, 160, 168, 180, 186, 214]
    nets = list()

    for octet3 in range(56, 60):
      for octet4 in fourth_octet:
        nets.append(nacaddr.IPv4('192.168.' + str(octet3) + '.'
                                 + str(octet4) + '/31'))

    result = summarizer.Summarize(nets)
    self.assertEqual(result, [summarizer.DSMNet(3232249858, 4294966398),
                              summarizer.DSMNet(3232249864, 4294966366),
                              summarizer.DSMNet(3232249876, 4294966390),
                              summarizer.DSMNet(3232249882, 4294966366),
                              summarizer.DSMNet(3232249888, 4294966398),
                              summarizer.DSMNet(3232249908, 4294966398),
                              summarizer.DSMNet(3232249942, 4294966398),
                              ])

  def testMergeText(self):
    existing_comment = 'comment that already exists'
    addition = 'addition'

    dsm_net = summarizer.DSMNet(167772160, 4278190080)
    self.assertEqual(dsm_net.MergeText(addition), addition)

    dsm_net = summarizer.DSMNet(167772160, 4278190080, existing_comment)
    self.assertEqual(dsm_net.MergeText(existing_comment), existing_comment)

    dsm_net = summarizer.DSMNet(167772160, 4278190080, existing_comment)
    self.assertEqual(dsm_net.MergeText(addition),
                     existing_comment + ', ' + addition)

  def testOrder(self):
    nets = [
        # not discontinously summarizable with the other two
        nacaddr.IPv4('209.85.147.129/32'),
        # discontinuosly summarizable, but should come before the first one
        nacaddr.IPv4('74.125.20.129/32'),
        nacaddr.IPv4('74.125.21.129/32'),
    ]
    result = summarizer.Summarize(nets)
    self.assertEqual(result, [summarizer.DSMNet(1249711233, 4294967039),
                              summarizer.DSMNet(3512046465, 4294967295)
                              ])


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


# tests/lib/windows_advfirewall_test.py

# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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.
"""Unittest for windows_advfirewall rendering module."""

import datetime
from absl.testing import absltest
from unittest import mock

from capirca.lib import aclgenerator
from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import windows
from capirca.lib import windows_advfirewall


GOOD_HEADER_OUT = """
header {
  comment:: "this is an out test acl"
  target:: windows_advfirewall out inet
}
"""

GOOD_HEADER_IN = """
header {
  comment:: "this is an in test acl"
  target:: windows_advfirewall in inet
}
"""

GOOD_SIMPLE = """
term good-simple {
  protocol:: tcp
  action:: accept
}
"""

GOOD_SIMPLE_WARNING = """
term good-simple-warning {
  protocol:: tcp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_ICMP = """
term good-term-icmp {
  protocol:: icmp
  action:: accept
}
"""

GOOD_TERM_ANYPROTO = """
term good-term-anyproto {
  source-address:: FOO
  destination-address:: FOO
  action:: accept
}
"""

GOOD_TERM_MISCPROTO = """
term good-term-miscproto {
  protocol:: vrrp
  action:: accept
}
"""

# Edge case: protocol value for hopopt is 0
GOOD_TERM_HOPOPT = """
term good-term-hopopt {
  protocol:: hopopt
  action:: accept
}
"""

GOOD_TERM_ICMP_TYPES = """
term good-term-icmp-types {
  protocol:: icmp
  icmp-type:: echo-reply unreachable time-exceeded
  action:: deny
}
"""

GOOD_TERM_ICMPV6 = """
term good-term-icmpv6 {
  protocol:: icmpv6
  action:: accept
}
"""

BAD_TERM_ICMP = """
term test-icmp {
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

BAD_TERM_ACTION = """
term bad-term-action {
  protocol:: icmp
  action:: undefined
}
"""

GOOD_TERM_TCP = """
term good-term-tcp {
  comment:: "Test term 1"
  destination-address:: PROD_NETWRK
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

GOOD_TERM_LOG = """
term good-term-log {
  protocol:: tcp
  logging:: true
  action:: accept
}
"""

EXPIRED_TERM = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

NEXT_TERM = """
term next {
  action:: next
}
"""

NEXT_LOG_TERM = """
term next-log {
  logging:: true
  action:: next
}
"""

EXCEPTION_POLICY = """
header {
  comment:: "Header comment"
  target:: windows_advfirewall out inet
}

term accept-corpdns {
  comment:: "accept-corpdns comment1"
  comment:: "accept-corpdns comment2"
  destination-address:: CORP_ANYCAST_DNS
  destination-port:: DNS
  protocol:: udp
  action:: accept
}

term deny-to-google {
  comment:: "deny-to-google comment"
  destination-address:: INTERNAL
  action:: deny
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'name',
    'option',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request', 'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request', 'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class WindowsAdvFirewallTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def assertTrue(self, strings, result, term):
    for string in strings:
      fullstring = 'netsh advfirewall firewall add rule %s' % (string)
      super().assertIn(
          fullstring, result,
          'did not find "%s" for %s\nGot:\n%s' % (fullstring, term, result))

  def testTcp(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_good-term-tcp enable=yes interfacetype=any dir=out localip=any'
         ' remoteip=10.0.0.0/8 remoteport=25 protocol=tcp action=allow'],
        result,
        'did not find actual term for good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NETWRK')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testIcmp(self):
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + GOOD_TERM_ICMP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_good-term-icmp enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=icmpv4 action=allow'],
        result,
        'did not find actual term for good-term-icmp')

  def testIcmpTypes(self):
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + GOOD_TERM_ICMP_TYPES, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_good-term-icmp-types enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=icmpv4:0,any action=block',
         'name=o_good-term-icmp-types enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=icmpv4:3,any action=block',
         'name=o_good-term-icmp-types enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=icmpv4:11,any action=block'],
        result,
        'did not find actual term for good-term-icmp-types')

  def testBadIcmp(self):
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + BAD_TERM_ICMP, self.naming), EXP_INFO)
    self.assertRaises(aclgenerator.UnsupportedFilterError,
                      str, acl)

  @mock.patch.object(windows.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + EXPIRED_TERM, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired '
        'and will not be rendered.',
        'expired_test', 'out')

  @mock.patch.object(windows.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + EXPIRING_TERM % exp_date.strftime('%Y-%m-%d'),
        self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring',
        'out')

  def testMultiprotocol(self):
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + MULTIPLE_PROTOCOLS_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_multi-proto enable=yes interfacetype=any dir=out localip=any'
         ' remoteip=any protocol=tcp action=allow',
         'name=o_multi-proto enable=yes interfacetype=any dir=out localip=any'
         ' remoteip=any protocol=udp action=allow',
         'name=o_multi-proto enable=yes interfacetype=any dir=out localip=any'
         ' remoteip=any protocol=icmpv4 action=allow'],
        result,
        'did not find actual term for multi-proto')

  def testAnyProtocol(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + GOOD_TERM_ANYPROTO, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_good-term-anyproto enable=yes interfacetype=any dir=out'
         ' localip=10.0.0.0/8 remoteip=10.0.0.0/8 protocol=any action=allow'],
        result,
        '"any" proto')

  def testMiscProtocol(self):
    acl = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_OUT + GOOD_TERM_MISCPROTO + GOOD_TERM_HOPOPT, self.naming),
                                                 EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['name=o_good-term-miscproto enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=112 action=allow',
         'name=o_good-term-hopopt enable=yes interfacetype=any dir=out'
         ' localip=any remoteip=any protocol=0 action=allow'],
        result,
        'explicit miscellaneous proto')

  def testBuildTokens(self):
    pol1 = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_IN + GOOD_SIMPLE, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = windows_advfirewall.WindowsAdvFirewall(policy.ParsePolicy(
        GOOD_HEADER_IN + GOOD_SIMPLE_WARNING, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


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


# tests/lib/windows_ipsec_test.py

# Copyright 2016 Google Inc. All Rights Reserved.
#
# 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.
"""Unittest for windows_ipsec rendering module."""

import datetime
from absl.testing import absltest
from unittest import mock

from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import windows_ipsec


GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: windows_ipsec test-filter
}
"""

GOOD_SIMPLE = """
term good-simple {
  protocol:: tcp
  action:: accept
}
"""

GOOD_SIMPLE_WARNING = """
term good-simple-warning {
  protocol:: tcp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM_ICMP = """
term good-term-icmp {
  protocol:: icmp
  action:: accept
}
"""

BAD_TERM_ICMP = """
term test-icmp {
  icmp-type:: echo-request echo-reply
  action:: accept
}
"""

GOOD_TERM_TCP = """
term good-term-tcp {
  comment:: "Test term 1"
  destination-address:: PROD_NET
  destination-port:: SMTP
  protocol:: tcp
  action:: accept
}
"""

EXPIRED_TERM = """
term expired_test {
  expiration:: 2000-1-1
  action:: deny
}
"""

EXPIRING_TERM = """
term is_expiring {
  expiration:: %s
  action:: accept
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'stateless_reply',
    'name',
    'option',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
}

SUPPORTED_SUB_TOKENS = {'action': {'accept', 'deny'}}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class WindowsIPSecTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  # pylint: disable=invalid-name
  def assertTrue(self, strings, result, term):
    for string in strings:
      fullstring = 'netsh ipsec static add %s' % (string)
      super().assertIn(
          fullstring, result,
          'did not find "%s" for %s' % (fullstring, term))

  def testPolicy(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['policy name=test-filter-policy assign=yes'],
        result,
        'header')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NET')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testTcp(self):
    self.naming.GetNetAddr.return_value = [nacaddr.IP('10.0.0.0/8')]
    self.naming.GetServiceByProto.return_value = ['25']

    acl = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_TCP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['filteraction name=t_good-term-tcp-action action=permit',
         'filter filterlist=t_good-term-tcp-list mirrored=yes srcaddr=any '
         ' dstaddr=10.0.0.0 dstmask=8 dstport=25',
         'rule name=t_good-term-tcp-rule policy=test-filter'
         ' filterlist=t_good-term-tcp-list'
         ' filteraction=t_good-term-tcp-action'],
        result,
        'good-term-tcp')

    self.naming.GetNetAddr.assert_called_once_with('PROD_NET')
    self.naming.GetServiceByProto.assert_called_once_with('SMTP', 'tcp')

  def testIcmp(self):
    acl = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + GOOD_TERM_ICMP, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['filterlist name=t_good-term-icmp-list',
         'filteraction name=t_good-term-icmp-action action=permit',
         'filter filterlist=t_good-term-icmp-list mirrored=yes srcaddr=any '
         ' dstaddr=any',
         'rule name=t_good-term-icmp-rule policy=test-filter'
         ' filterlist=t_good-term-icmp-list'
         ' filteraction=t_good-term-icmp-action'],
        result,
        'good-term-icmp')

  @mock.patch.object(windows_ipsec.logging, 'warning')
  def testExpiredTerm(self, mock_warn):
    windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + EXPIRED_TERM, self.naming), EXP_INFO)

    mock_warn.assert_called_once_with(
        'WARNING: Term %s in policy %s is expired and '
        'will not be rendered.', 'expired_test',
        'test-filter')

  @mock.patch.object(windows_ipsec.logging, 'info')
  def testExpiringTerm(self, mock_info):
    exp_date = datetime.date.today() + datetime.timedelta(weeks=EXP_INFO)
    windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + EXPIRING_TERM % exp_date.strftime('%Y-%m-%d'),
        self.naming), EXP_INFO)

    mock_info.assert_called_once_with(
        'INFO: Term %s in policy %s expires in '
        'less than two weeks.', 'is_expiring',
        'test-filter')

  def testMultiprotocol(self):
    acl = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + MULTIPLE_PROTOCOLS_TERM, self.naming), EXP_INFO)
    result = str(acl)
    self.assertTrue(
        ['filterlist name=t_multi-proto-list',
         'filteraction name=t_multi-proto-action action=permit',
         'filter filterlist=t_multi-proto-list mirrored=yes srcaddr=any '
         ' dstaddr=any  protocol=tcp',
         'filter filterlist=t_multi-proto-list mirrored=yes srcaddr=any '
         ' dstaddr=any  protocol=udp',
         'filter filterlist=t_multi-proto-list mirrored=yes srcaddr=any '
         ' dstaddr=any  protocol=icmp',
         'rule name=t_multi-proto-rule policy=test-filter'
         ' filterlist=t_multi-proto-list filteraction=t_multi-proto-action'],
        result,
        'multi-proto')

  def testBuildTokens(self):
    pol1 = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + GOOD_SIMPLE, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = windows_ipsec.WindowsIPSec(policy.ParsePolicy(
        GOOD_HEADER + GOOD_SIMPLE_WARNING, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)


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


# tests/lib/windows_test.py

# Copyright 2008 Google Inc. All Rights Reserved.
#
# 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.

"""Unittest for windows acl rendering module."""

from absl.testing import absltest
from unittest import mock

from capirca.lib import naming
from capirca.lib import policy
from capirca.lib import windows


GOOD_HEADER = """
header {
  comment:: "this is a test acl"
  target:: windows test-filter
}
"""

MULTIPLE_PROTOCOLS_TERM = """
term multi-proto {
  protocol:: tcp udp icmp
  action:: accept
}
"""

GOOD_WARNING_TERM = """
term good-warning-term {
  protocol:: tcp udp icmp
  policer:: batman
  action:: accept
}
"""

GOOD_TERM = """
term good-term {
  source-port:: FOO
  destination-port:: BAR
  protocol:: tcp
  action:: accept
}
"""

TCP_ESTABLISHED_TERM = """
term tcp-established {
  source-port:: FOO
  destination-port:: BAR
  protocol:: tcp
  option:: tcp-established
  action:: accept
}
"""

UDP_ESTABLISHED_TERM = """
term udp-established-term {
  source-port:: FOO
  destination-port:: BAR
  protocol:: udp
  option:: established
  action:: accept
}
"""

SUPPORTED_TOKENS = {
    'action',
    'comment',
    'destination_address',
    'destination_address_exclude',
    'destination_port',
    'expiration',
    'icmp_type',
    'stateless_reply',
    'name',
    'option',
    'platform',
    'platform_exclude',
    'protocol',
    'source_address',
    'source_address_exclude',
    'source_port',
    'translated',
}

SUPPORTED_SUB_TOKENS = {
    'action': {'accept', 'deny'},
    'icmp_type': {
        'alternate-address',
        'certification-path-advertisement',
        'certification-path-solicitation',
        'conversion-error',
        'destination-unreachable',
        'echo-reply',
        'echo-request', 'mobile-redirect',
        'home-agent-address-discovery-reply',
        'home-agent-address-discovery-request',
        'icmp-node-information-query',
        'icmp-node-information-response',
        'information-request',
        'inverse-neighbor-discovery-advertisement',
        'inverse-neighbor-discovery-solicitation',
        'mask-reply',
        'mask-request', 'information-reply',
        'mobile-prefix-advertisement',
        'mobile-prefix-solicitation',
        'multicast-listener-done',
        'multicast-listener-query',
        'multicast-listener-report',
        'multicast-router-advertisement',
        'multicast-router-solicitation',
        'multicast-router-termination',
        'neighbor-advertisement',
        'neighbor-solicit',
        'packet-too-big',
        'parameter-problem',
        'redirect',
        'redirect-message',
        'router-advertisement',
        'router-renumbering',
        'router-solicit',
        'router-solicitation',
        'source-quench',
        'time-exceeded',
        'timestamp-reply',
        'timestamp-request',
        'unreachable',
        'version-2-multicast-listener-report',
    },
}

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class WindowsGeneratorTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def testBuildTokens(self):
    pol1 = windows.WindowsGenerator(
        policy.ParsePolicy(GOOD_HEADER + MULTIPLE_PROTOCOLS_TERM, self.naming),
        EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testBuildWarningTokens(self):
    pol1 = windows.WindowsGenerator(policy.ParsePolicy(
        GOOD_HEADER + GOOD_WARNING_TERM, self.naming), EXP_INFO)
    st, sst = pol1._BuildTokens()
    self.assertEqual(st, SUPPORTED_TOKENS)
    self.assertEqual(sst, SUPPORTED_SUB_TOKENS)

  def testSkipEstablished(self):
    # self.naming.GetNetAddr.return_value = _IPSET
    self.naming.GetServiceByProto.return_value = ['123']
    pol = windows.WindowsGenerator(policy.ParsePolicy(
        GOOD_HEADER + TCP_ESTABLISHED_TERM + GOOD_TERM, self.naming), EXP_INFO)
    self.assertEqual(len(pol.windows_policies[0][4]), 1)
    pol = windows.WindowsGenerator(policy.ParsePolicy(
        GOOD_HEADER + UDP_ESTABLISHED_TERM + GOOD_TERM, self.naming), EXP_INFO)
    self.assertEqual(len(pol.windows_policies[0][4]), 1)


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


# tests/lib/nsxt_test.py
# Copyright 2023 The Capirca Project Authors All Rights Reserved.
# Copyright 2023 VMware, Inc. SPDX-License-Identifier: Apache-2.0
#
# 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.
"""UnitTest class for nsxt.py."""

import json
from absl.testing import absltest
from unittest import mock

from capirca.lib import nacaddr
from capirca.lib import naming
from capirca.lib import nsxt
from capirca.lib import policy

ICMPV6_TERM = """\
  term test-icmpv6 {
    protocol:: icmpv6
    icmp-type:: echo-request echo-reply
    action:: accept
  }
  """

UDP_POLICY = """\
  header {
    comment:: "Sample inet NSXT filter"
    target:: nsxt INET_FILTER_NAME inet
  }

  term allow-ntp-request {
    comment::"Allow ntp request"
    source-address:: NTP_SERVERS
    source-port:: NTP
    destination-address:: INTERNAL
    destination-port:: NTP
    protocol:: udp
    action:: accept
  }
  """

UDP_NSXT_POLICY = {
  'rules': [{
    'action': 'ALLOW',
    'resource_type': 'Rule',
    'display_name': 'allow-ntp-request',
    'source_groups': ['10.0.0.1/32', '10.0.0.2/32'],
    'destination_groups': ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
    'services': ['ANY'],
    'profiles': ['ANY'],
    'scope': ['ANY'],
    'logged': False,
    'notes': 'Allow ntp request',
    'direction': 'IN_OUT',
    'ip_protocol': 'IPV4_IPV6',
    'service_entries': [{
      'l4_protocol': 'UDP',
      'resource_type': 'L4PortSetServiceEntry',
      'source_ports': ['123-123'],
      'destination_ports': ['123-123']
    }]
  }],
  'resource_type': 'SecurityPolicy',
  'display_name': 'INET_FILTER_NAME',
  'category': 'Application',
  'is_default': 'false',
  'id': 'INET_FILTER_NAME',
  'scope': ['ANY']
}

UDP_RULE = {
  "action": "ALLOW",
  "resource_type": "Rule",
  "display_name": "allow-ntp-request",
  "source_groups": ["10.0.0.1/32", "10.0.0.2/32"],
  "destination_groups": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"],
  "services": ["ANY"],
  "profiles": ["ANY"],
  "scope": ["ANY"],
  "logged": False,
  "notes": "Allow ntp request",
  "direction": "IN_OUT",
  "ip_protocol": "IPV4_IPV6",
  "service_entries": [{
    "l4_protocol": "UDP",
    "resource_type": "L4PortSetServiceEntry",
    "source_ports": ["123-123"],
    "destination_ports": ["123-123"]
  }]
}

ICMPV6_POLICY = """\
  header {
    comment:: "Sample ICMPv6 NSXT filter"
    target:: nsxt INET6_FILTER_NAME inet6
  }

  term test-icmpv6 {
    #destination-address:: WEB_SERVERS
    protocol:: icmpv6
    icmp-type:: echo-request echo-reply
    action:: accept
  }
  """

ICMPV6_RULE = {
  'action': 'ALLOW',
  'resource_type': 'Rule',
  'display_name': 'test-icmpv6',
  'source_groups': ['ANY'],
  'destination_groups': ['ANY'],
  'services': ['ANY'],
  'profiles': ['ANY'],
  'scope': ['ANY'],
  'logged': False,
  'notes': '',
  'direction': 'IN_OUT',
  'ip_protocol': 'IPV4_IPV6',
  'service_entries': [{
    'protocol': 'ICMPv6',
    'resource_type': 'ICMPTypeServiceEntry',
    'icmp_type': 128
  }]
}

UDP_AND_TCP_POLICY = """\
  header {
    comment:: "Sample mixed NSXT filter"
    target:: nsxt MIXED_FILTER_NAME mixed
  }

  term accept-to-honestdns {
    comment:: "Allow name resolution using honestdns."
    destination-address:: GOOGLE_DNS
    destination-port:: DNS
    protocol:: udp
    action:: accept
  }

  term permit-mail-services {
    destination-address:: MAIL_SERVERS
    protocol:: tcp
    destination-port:: MAIL_SERVICES
    action:: accept
  }
  """

UDP_AND_TCP_NSXT_POLICY = {
  'rules': [{
    'action': 'ALLOW',
    'resource_type': 'Rule',
    'display_name': 'accept-to-honestdns',
    'source_groups': ['ANY'],
    'destination_groups': [
      '8.8.4.4/32',
      '8.8.8.8/32',
      '2001:4860:4860::8844/128',
      '2001:4860:4860::8888/128'
    ],
    'services': ['ANY'],
    'profiles': ['ANY'],
    'scope': ['ANY'],
    'logged': False,
    'notes': 'Allow name resolution using honestdns.',
    'direction': 'IN_OUT',
    'ip_protocol': 'IPV4_IPV6',
    'service_entries': [{
      'l4_protocol': 'UDP',
      'resource_type': 'L4PortSetServiceEntry',
      'destination_ports': ['53-53']
    }]
  },
    {
      'action': 'ALLOW',
      'resource_type': 'Rule',
      'display_name': 'permit-mail-services',
      'source_groups': ['ANY'],
      'destination_groups': ['2001:4860:4860::8845'],
      'services': ['ANY'],
      'profiles': ['ANY'],
      'scope': ['ANY'],
      'logged': False,
      'notes': '',
      'direction': 'IN_OUT',
      'ip_protocol': 'IPV4_IPV6',
      'service_entries': [{
        'l4_protocol': 'TCP',
        'resource_type': 'L4PortSetServiceEntry',
        'destination_ports': ['53-53']
      }]
    }],
  'resource_type': 'SecurityPolicy',
  'display_name': 'MIXED_FILTER_NAME',
  'category': 'Application',
  'is_default': 'false',
  'id': 'MIXED_FILTER_NAME',
  'scope': ['ANY']
}

ICMP_POLICY_WITH_SECURITY_GROUP = """\
  header {
    comment:: "Sample filter with Security Group"
    target:: nsxt POLICY_WITH_SECURITY_GROUP_NAME inet 1010 securitygroup \
    securitygroup-Id
  }

  term accept-icmp {
    protocol:: icmp
    action:: accept
  }
  """

ICMP_NSXT_POLICY_WITH_SECURITY_GROUP = {
  'rules': [{
    'action': 'ALLOW',
    'resource_type': 'Rule',
    'display_name': 'accept-icmp',
    'source_groups': ['ANY'],
    'destination_groups': ['ANY'],
    'services': ['ANY'],
    'profiles': ['ANY'],
    'scope': ['ANY'],
    'logged': False,
    'notes': '',
    'direction': 'IN_OUT',
    'ip_protocol': 'IPV4_IPV6',
    'service_entries': [{
      'protocol': 'ICMPv4',
      'resource_type': 'ICMPTypeServiceEntry'
    }]
  }],
  'resource_type': 'SecurityPolicy',
  'display_name': 'POLICY_WITH_SECURITY_GROUP_NAME',
  'category': 'Application',
  'is_default': 'false',
  'id': '1010',
  'scope': ['/infra/domains/default/groups/securitygroup-Id']
}

BAD_HEADER = """\
  header {
    comment:: "Sample NSXT filter3"
    target:: nsxt BAD_HEADER_NAME inet 1011 securitygroup
  }
  """

BAD_HEADER_1 = """\
  header {
    comment:: "Sample NSXT filter4"
    target:: nsxt BAD_HEADER_1_NAME 1012
  }
  """

BAD_HEADER_2 = """\
  header {
    comment:: "Sample NSXT filter5"
    target:: nsxt BAD_HEADER_2_NAME inet securitygroup
  }
  """

BAD_HEADER_3 = """\
  header {
    comment:: "Sample NSXT filter6"
    target:: nsxt BAD_HEADER_3_NAME
  }
  """

BAD_HEADER_4 = """\
  header {
    comment:: "Sample NSXT filter7"
    target:: nsxt BAD_HEADER_3_NAME inet 1234 securitygroup securitygroup \
    securitygroupId1
  }
  """

# Print a info message when a term is set to expire in that many weeks.
# This is normally passed from command line.
EXP_INFO = 2


class TermTest(absltest.TestCase):

  def setUp(self):
    super().setUp()
    self.naming = mock.create_autospec(naming.Naming)

  def test_udp_term(self):
    """Test __init__ and __str__ for udp term defining destination and source addresses and ports"""
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']

    policies = policy.ParsePolicy(UDP_POLICY, self.naming, False)
    af = 4
    pol = policies.filters[0]
    terms = pol[1]
    term = terms[0]

    nsxt_term = nsxt.Term(term, af)
    rule_str = str(nsxt_term)
    rule = json.loads(rule_str)

    self.assertEqual(nsxt_term.af, af)
    self.assertEqual(rule, UDP_RULE)

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('NTP', 'udp')] * 2)

  def test_icmpv6_term(self):
    """Test __init__ and __str__ for term inet6"""
    policies = policy.ParsePolicy(ICMPV6_POLICY, self.naming, False)
    af = 6
    filter_type = 'inet6'
    pol = policies.filters[0]
    terms = pol[1]
    term = terms[0]

    nsxt_term = nsxt.Term(term, filter_type, None, af)
    rule_str = str(nsxt_term)
    rule = json.loads(rule_str)

    self.assertEqual(rule, ICMPV6_RULE)

  def test_udp_policy(self):
    """Test for Nsxt.test_TranslatePolicy."""
    self.naming.GetNetAddr.side_effect = [
        [nacaddr.IP('10.0.0.1'), nacaddr.IP('10.0.0.2')],
        [nacaddr.IP('10.0.0.0/8'), nacaddr.IP('172.16.0.0/12'),
         nacaddr.IP('192.168.0.0/16')]]
    self.naming.GetServiceByProto.return_value = ['123']

    pol = policy.ParsePolicy(UDP_POLICY, self.naming, False)
    nsxt_policy = nsxt.Nsxt(pol, EXP_INFO)
    api_policy = json.loads(str(nsxt_policy))

    self.assertEqual(api_policy, UDP_NSXT_POLICY)
    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('NTP_SERVERS'), mock.call('INTERNAL')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('NTP', 'udp')] * 2)

  def test_udp_and_tcp_policy(self):
    """Test for Nsxt._str_."""
    self.naming.GetNetAddr.side_effect = [
      [
        nacaddr.IP('8.8.4.4'),
        nacaddr.IP('8.8.8.8'),
        nacaddr.IP('2001:4860:4860::8844'),
        nacaddr.IP('2001:4860:4860::8888')
      ],
      nacaddr.IP('2001:4860:4860::8845')]
    self.naming.GetServiceByProto.return_value = ['53']

    pol = policy.ParsePolicy(UDP_AND_TCP_POLICY, self.naming, False)
    nsxt_policy = nsxt.Nsxt(pol, EXP_INFO)
    api_policy = json.loads(str(nsxt_policy))

    self.assertEqual(api_policy, UDP_AND_TCP_NSXT_POLICY)

    self.naming.GetNetAddr.assert_has_calls(
        [mock.call('GOOGLE_DNS'), mock.call('MAIL_SERVERS')])
    self.naming.GetServiceByProto.assert_has_calls(
        [mock.call('DNS', 'udp'), mock.call('MAIL_SERVICES', 'tcp')])

  def test_icmp_policy_with_security_group(self):
    """Test for Nsxt._str_ with security group in scope"""
    pol = policy.ParsePolicy(ICMP_POLICY_WITH_SECURITY_GROUP, self.naming, False)
    nsxt_policy = nsxt.Nsxt(pol, EXP_INFO)
    api_policy = json.loads(str(nsxt_policy))

    self.assertEqual(api_policy, ICMP_NSXT_POLICY_WITH_SECURITY_GROUP)

  def test_bad_header_case_0(self):
    pol = policy.ParsePolicy(BAD_HEADER + ICMPV6_TERM, self.naming, False)
    self.assertRaises(nsxt.UnsupportedNsxtAccessListError,
                      nsxt.Nsxt, pol, EXP_INFO)

  def test_bad_header_case_1(self):
    pol = policy.ParsePolicy(BAD_HEADER_1 + ICMPV6_TERM, self.naming, False)
    self.assertRaises(nsxt.UnsupportedNsxtAccessListError,
                      nsxt.Nsxt, pol, EXP_INFO)

  def test_bad_header_case_2(self):
    pol = policy.ParsePolicy(BAD_HEADER_2 + ICMPV6_TERM, self.naming, False)
    self.assertRaises(nsxt.UnsupportedNsxtAccessListError,
                      nsxt.Nsxt, pol, EXP_INFO)

  def test_bad_header_case_3(self):
    pol = policy.ParsePolicy(BAD_HEADER_3 + ICMPV6_TERM, self.naming, False)
    self.assertRaises(nsxt.UnsupportedNsxtAccessListError,
                      nsxt.Nsxt, pol, EXP_INFO)

  def test_bad_header_case_4(self):
    pol = policy.ParsePolicy(BAD_HEADER_4 + ICMPV6_TERM, self.naming, False)
    self.assertRaises(nsxt.UnsupportedNsxtAccessListError,
                      nsxt.Nsxt, pol, EXP_INFO)


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