diff --git a/.gitignore b/.gitignore index 3aa3450..460222a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,6 @@ env/ *.sha256 terraform.tfstate -test_data/**.csv +data_src/**.csv ./idea/* **/__pycache__/ diff --git a/README.md b/README.md index 967ab38..83ddd1e 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,23 @@ -# Ministry of Justice Template Repository +# Juniper mist integration script -[![repo standards badge](https://img.shields.io/endpoint?labelColor=231f20&color=005ea5&style=for-the-badge&label=MoJ%20Compliant&url=https%3A%2F%2Foperations-engineering-reports.cloud-platform.service.justice.gov.uk%2Fapi%2Fv1%2Fcompliant_public_repositories%2Fendpoint%2Ftemplate-repository&logo=data:image/png;base64,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)](https://operations-engineering-reports.cloud-platform.service.justice.gov.uk/public-report/template-repository) +This repo has been developed by the DevOps Lan&Wi-Fi to automate site creation on juniper mist. -This template repository equips you with the default initial files required for a Ministry of Justice GitHub repository. +## Run script -## Included Files +## Development setup -The repository comes with the following preset files: +### Prerequisites -- LICENSE -- .gitignore -- CODEOWNERS -- dependabot.yml -- GitHub Actions example file -- Ministry of Justice Compliance Badge (Public repositories only) +- Docker +- IDE that integrates with docker (We use IntelliJ in this example) -## Setup Instructions +### Setup -Once you've created your repository using this template, ensure the following steps: +- Run `make build` +- Integrate built docker container with IDE. [here](https://www.jetbrains.com/help/idea/configuring-remote-python-sdks.html#2546d02c) is the example for intelliJ +- mark src directory & test directory within the IDE. [here](https://www.jetbrains.com/help/idea/content-roots.html) -### Update README +# Notes -Edit this README.md file to document your project accurately. Take the time to create a clear, engaging, and informative README.md file. Include information like what your project does, how to install and run it, how to contribute, and any other pertinent details. - -### Update repository description - -After you've created your repository, GitHub provides a brief description field that appears on the top of your repository's main page. This is a summary that gives visitors quick insight into the project. Using this field to provide a succinct overview of your repository is highly recommended. - -This description and your README.md will be one of the first things people see when they visit your repository. It's a good place to make a strong, concise first impression. Remember, this is often visible in search results on GitHub and search engines, so it's also an opportunity to help people discover your project. - -### Grant Team Permissions - -Assign permissions to the appropriate Ministry of Justice teams. Ensure at least one team is granted Admin permissions. Whenever possible, assign permissions to teams rather than individual users. - -### Read about the GitHub repository standards - -Familiarise yourself with the Ministry of Justice GitHub Repository Standards. These standards ensure consistency, maintainability, and best practices across all our repositories. - -You can find the standards [here](https://operations-engineering.service.justice.gov.uk/documentation/services/repository-standards.html). - -Please read and understand these standards thoroughly and enable them when you feel comfortable. - -### Modify the GitHub Standards Badge - -Once you've ensured that all the [GitHub Repository Standards](https://operations-engineering.service.justice.gov.uk/documentation/services/repository-standards.html) have been applied to your repository, it's time to update the Ministry of Justice (MoJ) Compliance Badge located in the README file. - -The badge demonstrates that your repository is compliant with MoJ's standards. Please follow these [instructions](https://operations-engineering.service.justice.gov.uk/documentation/runbooks/services/add-repo-badge.html) to modify the badge URL to reflect the status of your repository correctly. - -**Please note** the badge will not function correctly if your repository is internal or private. In this case, you may remove the badge from your README. - -### Manage Outside Collaborators - -To add an Outside Collaborator to the repository, follow the guidelines detailed [here](https://github.com/ministryofjustice/github-collaborators). - -### Update CODEOWNERS - -(Optional) Modify the CODEOWNERS file to specify the teams or users authorized to approve pull requests. - -### Configure Dependabot - -Adapt the dependabot.yml file to match your project's [dependency manager](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem) and to enable [automated pull requests for package updates](https://docs.github.com/en/code-security/supply-chain-security). - -If your repository is private with no GitHub Advanced Security license, remove the .github/workflows/dependency-review.yml file. +- To see options run `make help` +- If you update [requirements.txt](src/requirements.txt), you will need to rebuild the docker container. diff --git a/test_data/.gitkeep b/data_src/.gitkeep similarity index 100% rename from test_data/.gitkeep rename to data_src/.gitkeep diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..c07a078 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11.5 + +# Set the working directory to /app +WORKDIR /app + +# Copy the current directory contents into the container at /app +COPY ../src/requirements.txt /tmp/requirements.txt + +# Copy entrypoint +COPY docker/entrypoint.sh /entrypoint.sh +COPY src/ /app/src + +# Make entrypoint executable +RUN chmod +x /entrypoint.sh + +# Install any needed packages specified in requirements.txt +RUN pip install --no-cache-dir -r ../tmp/requirements.txt + +# Set the src directory +ENV PYTHONPATH=/app/src + +# Run tests when the container launches +ENTRYPOINT ["/entrypoint.sh"] + diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..cc0f248 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [ "$RUN_UNIT_TESTS" = True ] ; then + python -m unittest discover -s /app -t /app +else + python /app/src/main.py +fi diff --git a/makefile b/makefile new file mode 100644 index 0000000..a3108f9 --- /dev/null +++ b/makefile @@ -0,0 +1,44 @@ +#!make +.DEFAULT_GOAL := help +SHELL := '/bin/bash' + +.PHONY: build +build: ## Build the docker container + docker build -t juniper-mist -f docker/Dockerfile . + +.PHONO: create-dir +make create-dir: ## Creates a directory for end user to put CSV file into + mkdir data_src; + echo "Please put csv file into data_src then run 'make-prod'"; + +.PHONY: run-prod +run-prod: ## Run the python script only mounting the host for csv-file. Format: MIST_API_TOKEN=foo ORG_ID=bar make run-prod + docker run -v $(shell pwd)/data_src:/data_src \ + -e MIST_API_TOKEN=$$MIST_API_TOKEN \ + -e ORG_ID=$$ORG_ID \ + juniper-mist + +.PHONY: run-dev +run-dev: ## Run the python script while mounting the host. This enables using the latest local src code without needing to wait for a container build. Format: MIST_API_TOKEN=foo ORG_ID=bar make run-dev + docker run -v $(shell pwd)/src:/app/src \ + -v $(shell pwd)/data_src:/data_src \ + -e MIST_API_TOKEN=$$MIST_API_TOKEN \ + -e ORG_ID=$$ORG_ID \ + juniper-mist + +.PHONY: tests +tests: ## Run unit tests for the python app + docker run -v $(shell pwd)/src:/app/src \ + -v $(shell pwd)/test:/app/test \ + -e RUN_UNIT_TESTS=True juniper-mist + +.PHONY: shell +shell: ## Make interactive docker container + docker run -it --entrypoint /bin/bash \ + -v $(shell pwd)/src:/app/src \ + -v $(shell pwd)/test:/app/test \ + -v $(shell pwd)/data_src:/data_src \ + -e RUN_UNIT_TESTS=True juniper-mist + +help: + @grep -h -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/geocode.py b/src/geocode.py index 7a20589..de0bf82 100644 --- a/src/geocode.py +++ b/src/geocode.py @@ -7,9 +7,15 @@ def geocode(address) -> str: location = geolocator.geocode(address) try: latitude, longitude = location.latitude, location.longitude - except ValueError: - raise ValueError( - 'geocode unable to find latitude & longitude for {address}'.format(address=address)) + except AttributeError as e: + if "'NoneType' object has no attribute 'latitude'" in str(e): + raise AttributeError( + 'geocode unable to find latitude & longitude for {address}'.format(address=address)) + if "'NoneType' object has no attribute 'longitude'" in str(e): + raise AttributeError( + 'geocode unable to find latitude & longitude for {address}'.format(address=address)) + else: + raise # Re-raise the original AttributeError if the message doesn't match return latitude, longitude @@ -30,7 +36,7 @@ def find_timezone(gps) -> tuple: try: timezone_name = tf_init.timezone_at(lat=latitude, lng=longitude) except ValueError: - raise ValueError('The coordinates were out of bounds {latitude}:{longitude}'.format( + raise ValueError('The coordinates were out of bounds {lat}:{lng}'.format( lat=latitude, lng=longitude)) if timezone_name is None: raise ValueError('GPS coordinates did not match a time_zone') diff --git a/src/main.py b/src/main.py index 4b84078..de7b708 100644 --- a/src/main.py +++ b/src/main.py @@ -5,7 +5,7 @@ # Convert CSV file to JSON object. -def csv_to_json(file_path): +def convert_csv_to_json(file_path): csv_rows = [] with open(file_path) as csvfile: reader = csv.DictReader(csvfile, skipinitialspace=True, quotechar='"') @@ -13,21 +13,14 @@ def csv_to_json(file_path): for row in reader: csv_rows.extend([{title[i]: row[title[i]] - for i in range(len(title))}]) + for i in range(len(title))}]) + if csv_rows == None or csv_rows == []: + raise ValueError('Failed to convert CSV file to JSON. Exiting script.') return csv_rows -if __name__ == '__main__': - - csv_file_path = os.getcwd() + '/../test_data/sites_with_clients.csv' - - # Convert CSV to valid JSON - data = csv_to_json(csv_file_path) - if data == None or data == []: - raise ValueError('Failed to convert CSV file to JSON. Exiting script.') - - # Create each site from the CSV file +def add_geocoding_to_json(data): for d in data: # Variables site_id = None @@ -43,9 +36,21 @@ def csv_to_json(file_path): d['gps'] = gps d['country_code'] = country_code d['time_zone'] = time_zone + return data + + +if __name__ == '__main__': + + csv_file_path = os.getcwd() + '/../data_src/sites_with_clients.csv' + + # Convert CSV to valid JSON + json_data_without_geocoding = convert_csv_to_json(csv_file_path) + + json_data_with_geocoding = add_geocoding_to_json( + json_data_without_geocoding) juniper_script( mist_api_token=os.environ['MIST_API_TOKEN'], org_id=os.environ['ORG_ID'], - data=data + data=json_data_with_geocoding ) diff --git a/src/requirements.txt b/src/requirements.txt index cf38b7e..f6f697c 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,3 +1,3 @@ -requests==2.21.0 +requests==2.31.0 geopy==2.4.1 timezonefinder==6.2.0 \ No newline at end of file diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_geocode.py b/test/test_geocode.py new file mode 100644 index 0000000..480ed44 --- /dev/null +++ b/test/test_geocode.py @@ -0,0 +1,112 @@ +import unittest +from unittest.mock import patch, MagicMock +from src.geocode import geocode, find_timezone, find_country_code + + +class TestGeocode(unittest.TestCase): + + @patch('src.geocode.Nominatim.geocode') + def test_given_list_of_valid_addresses_when_geocode_called_then_return_relevant_list_of_gps_locations(self, mock_nominatim): + # Define a list of addresses and their expected results + addresses = [ + ("40 Mayflower Dr, Plymouth PL2 3DG", (50.3868633, -4.1539256)), + ("102 Petty France, London SW1H 9AJ", + (51.499929300000005, -0.13477761285315926)), + ("London", (51.4893335, -0.14405508452768728)), + ("Met Office, FitzRoy Road, Exeter, Devon, EX1 3PB", + (50.727350349999995, -3.4744726127760086)) + ] + # Mock the geocode method to return the corresponding latitude and longitude + for address, (lat, lon) in addresses: + mock_nominatim.return_value.latitude = lat + mock_nominatim.return_value.longitude = lon + + # Call the geocode function for each address + result = geocode(address) + + # Assert that the result matches the expected latitude and longitude + self.assertEqual(result, (lat, lon)) + + @patch('src.geocode.Nominatim.geocode') + def test_geocode_invalid_address(self, mock_geocode): + # Arrange + address = "Invalid Address" + mock_geocode.return_value = None # Simulate geocode returning None + + # Act & Assert + with self.assertRaises(AttributeError) as context: + geocode(address) + + expected_error_message = 'geocode unable to find latitude & longitude for {address}'.format( + address=address) + self.assertEqual(str(context.exception), expected_error_message) + + @patch('src.geocode.TimezoneFinder') + def test_find_timezone_valid_coordinates(self, mock_timezone_finder): + tf_instance = MagicMock() + tf_instance.timezone_at.return_value = 'America/New_York' + mock_timezone_finder.return_value = tf_instance + + gps_coordinates = (40.7128, -74.0060) + result = find_timezone(gps_coordinates) + + self.assertEqual(result, 'America/New_York') + + @patch('src.geocode.TimezoneFinder') + def test_find_timezone_out_of_bounds(self, mock_timezone_finder): + tf_instance = MagicMock() + tf_instance.timezone_at.side_effect = ValueError( + 'The coordinates were out of bounds 40.7128:-74.0060') + mock_timezone_finder.return_value = tf_instance + + gps_coordinates = (40.7128, -74.0060) + + with self.assertRaises(ValueError) as context: + find_timezone(gps_coordinates) + + self.assertEqual(str(context.exception), + 'The coordinates were out of bounds 40.7128:-74.006') + + @patch('src.geocode.TimezoneFinder') + def test_find_timezone_no_matching_timezone(self, mock_timezone_finder): + tf_instance = MagicMock() + tf_instance.timezone_at.return_value = None + mock_timezone_finder.return_value = tf_instance + + gps_coordinates = (40.7128, -74.0060) + + with self.assertRaises(ValueError) as context: + find_timezone(gps_coordinates) + + self.assertEqual(str(context.exception), + 'GPS coordinates did not match a time_zone') + + +class TestFindCountryCode(unittest.TestCase): + @patch('src.geocode.Nominatim.geocode') + def test_find_country_code_valid_coordinates(self, mock_nominatim): + geolocator_instance = MagicMock() + location_instance = MagicMock() + location_instance.raw = {'address': {'country_code': 'us'}} + geolocator_instance.reverse.return_value = location_instance + mock_nominatim.return_value = geolocator_instance + + gps_coordinates = (40.7128, -74.0060) + result = find_country_code(gps_coordinates) + + self.assertEqual(result, 'US') + + @patch('src.geocode.Nominatim.geocode') + def test_find_country_code_invalid_coordinates(self, mock_nominatim): + geolocator_instance = MagicMock() + geolocator_instance.reverse.side_effect = Exception( + 'Invalid coordinates') + mock_nominatim.return_value = geolocator_instance + + gps_coordinates = (1000.0, 2000.0) # Invalid coordinates + + with self.assertRaises(Exception) as context: + find_country_code(gps_coordinates) + + self.assertEqual(str(context.exception), + 'Must be a coordinate pair or Point') diff --git a/test/test_juniper.py b/test/test_juniper.py new file mode 100644 index 0000000..42cbdc9 --- /dev/null +++ b/test/test_juniper.py @@ -0,0 +1,56 @@ +import unittest +from unittest.mock import patch, MagicMock +from src.juniper import juniper_script, Admin + + +class TestJuniperScript(unittest.TestCase): + + @patch('src.juniper.Admin.post') + @patch('src.juniper.Admin.put') + def test_juniper_script(self, mock_put, mock_post): + # Mock Mist API responses + mock_post.return_value = {'id': '123', 'name': 'TestSite'} + mock_put.return_value = {'status': 'success'} + + # Sample input data + data = [ + {'Site Name': 'TestSite', 'Site Address': '123 Main St', + 'gps': [1.23, 4.56], 'country_code': 'US', 'time_zone': 'UTC', + 'Enable GovWifi': 'true', 'Enable MoJWifi': 'false', + 'Wired NACS Radius Key': 'key1', 'GovWifi Radius Key': 'key2'} + ] + + # Call the function + juniper_script(data, mist_api_token='your_token', org_id='your_org_id') + + # Assertions + mock_post.assert_called_once_with('/api/v1/orgs/your_org_id/sites', { + 'name': 'TestSite', + 'address': '123 Main St', + 'latlng': {'lat': 1.23, 'lng': 4.56}, + 'country_code': 'US', + 'timezone': 'UTC' + }) + + mock_put.assert_called_once_with('/api/v1/sites/123/setting', { + 'vars': { + 'Enable GovWifi': 'true', + 'Enable MoJWifi': 'false', + 'Wired NACS Radius Key': 'key1', + 'GovWifi Radius Key': 'key2' + } + }) + + def test_juniper_script_missing_token(self): + # Test when mist_api_token is missing + with self.assertRaises(SystemExit) as cm: + juniper_script([], org_id='your_org_id') + + self.assertEqual(cm.exception.code, 1) + + def test_juniper_script_missing_org_id(self): + # Test when org_id is missing + with self.assertRaises(SystemExit) as cm: + juniper_script([], mist_api_token='your_token') + + self.assertEqual(cm.exception.code, 1) diff --git a/test/test_main.py b/test/test_main.py new file mode 100644 index 0000000..e2b06a9 --- /dev/null +++ b/test/test_main.py @@ -0,0 +1,92 @@ +import unittest +import tempfile +import csv +from unittest.mock import patch +from src.main import convert_csv_to_json, add_geocoding_to_json + + +class TestCsvToJson(unittest.TestCase): + + def setUp(self): + # Create a temporary CSV file for testing + self.csv_data = [ + {'Site Name': 'Test location 1', 'Site Address': '40 Mayflower Dr, Plymouth PL2 3DG', 'Enable GovWifi': ' "TRUE"', + 'Enable MoJWifi': ' "FALSE"', 'GovWifi Radius Key': '00000DD0000BC0EEE000', 'Wired NACS Radius Key': '00000DD0000BC0EEE000'}, + {'Site Name': 'Test location 2', 'Site Address': '102 Petty France, London SW1H 9AJ', 'Enable GovWifi': ' "TRUE"', + 'Enable MoJWifi': ' "FALSE"', 'GovWifi Radius Key': '0D0E0DDE000BC0EEE000', 'Wired NACS Radius Key': '00000DD0000BC0EEE000'}, + {'Site Name': 'Test location 3', 'Site Address': 'Met Office, FitzRoy Road, Exeter, Devon, EX1 3PB', 'Enable GovWifi': ' "TRUE"', + 'Enable MoJWifi': ' "FALSE"', 'GovWifi Radius Key': '0D0E0DDE080BC0EEE000', 'Wired NACS Radius Key': '00000DD0000BC0EEE000'} + ] + self.csv_file = tempfile.NamedTemporaryFile( + mode='w', delete=False, newline='', suffix='.csv') + self.csv_writer = csv.DictWriter(self.csv_file, fieldnames=[ + 'Site Name', + 'Site Address', + 'Enable GovWifi', + 'Enable MoJWifi', + 'GovWifi Radius Key', + 'Wired NACS Radius Key' + ]) + self.csv_writer.writeheader() + self.csv_writer.writerows(self.csv_data) + self.csv_file.close() + + def test_convert_csv_to_json_valid_csv(self): + expected_json = self.csv_data + actual_json = convert_csv_to_json(self.csv_file.name) + self.assertEqual(actual_json, expected_json) + + def test_given_csv_file_when_csv_file_empty_then_raise_value_error(self): + empty_csv_file = tempfile.NamedTemporaryFile( + mode='w', delete=False, newline='', suffix='.csv') + empty_csv_file.close() + with self.assertRaises(ValueError) as error: + convert_csv_to_json(empty_csv_file.name) + self.assertEqual(str(error.exception), + 'Failed to convert CSV file to JSON. Exiting script.') + + def test_given_file_path_when_csv_file_not_found_then_raise_FileNotFoundError(self): + # Test if the function handles a nonexistent CSV file correctly + nonexistent_file_path = 'nonexistent.csv' + with self.assertRaises(FileNotFoundError): + convert_csv_to_json(nonexistent_file_path) + + +class TestAddGeocodingToJson(unittest.TestCase): + + @patch('src.main.geocode', side_effect=[ + {'latitude': 50.3868633, 'longitude': -4.1539256}, + {'latitude': 51.499929300000005, 'longitude': -0.13477761285315926}, + {'latitude': 50.727350349999995, 'longitude': -3.4744726127760086}, + ]) + @patch('src.main.find_country_code', return_value='GB') + @patch('src.main.find_timezone', return_value='Europe/London') + def test_given_site_name_and_site_address_in_json_format_when_function_called_then_add_geocode_country_code_and_time_zone( + self, + find_timezone, + mock_find_country_code, + mock_geocode + ): + # Test if the function adds geocoding information correctly + data = [ + {'Site Name': 'Site1', 'Site Address': '40 Mayflower Dr, Plymouth PL2 3DG'}, + {'Site Name': 'Site2', 'Site Address': '102 Petty France, London SW1H 9AJ'}, + {'Site Name': 'Site3', + 'Site Address': 'Met Office, FitzRoy Road, Exeter, Devon, EX1 3PB'} + ] + + expected_data = [ + {'Site Name': 'Site1', 'Site Address': '40 Mayflower Dr, Plymouth PL2 3DG', 'gps': { + 'latitude': 50.3868633, 'longitude': -4.1539256}, 'country_code': 'GB', 'time_zone': 'Europe/London'}, + {'Site Name': 'Site2', 'Site Address': '102 Petty France, London SW1H 9AJ', 'gps': { + 'latitude': 51.499929300000005, 'longitude': -0.13477761285315926}, 'country_code': 'GB', 'time_zone': 'Europe/London'}, + {'Site Name': 'Site3', 'Site Address': 'Met Office, FitzRoy Road, Exeter, Devon, EX1 3PB', 'gps': { + 'latitude': 50.727350349999995, 'longitude': -3.4744726127760086}, 'country_code': 'GB', 'time_zone': 'Europe/London'} + ] + + actual_data = add_geocoding_to_json(data) + + self.assertEqual(actual_data, expected_data) + find_timezone.assert_called() + mock_find_country_code.assert_called() + mock_geocode.assert_called()