diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 8bc92e8..8c4f330 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -7,10 +7,39 @@ on: - 'README.md' jobs: - deploy: + test: runs-on: ubuntu-latest steps: + # Checks-out your repository under $GITHUB_WORKSPACE + - name: Check out repository + uses: actions/checkout@v3 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-access-key-id: ${{ secrets.SLS_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.SLS_AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.9 + + # Install Python dependencies + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + # Run tests + - name: Run tests + run: python -m pytest + deploy: + needs: test + runs-on: ubuntu-latest + steps: - uses: actions/checkout@v1 - name: Set up Node diff --git a/test_data/sample_observation.json b/test_data/sample_observation.json new file mode 100644 index 0000000..c1990ef --- /dev/null +++ b/test_data/sample_observation.json @@ -0,0 +1,305 @@ +{ + "site": "mrc", + "enclosure": "enc1", + "telescope": "0m35", + "observation_type": "NORMAL", + "state": "PENDING", + "id": 583116837, + "request_group_id": 1885213, + "created": "2024-11-01T14:04:10.425775Z", + "modified": "2024-11-01T14:04:10.425770Z", + "start": "2024-11-02T07:22:14Z", + "end": "2024-11-02T07:26:56Z", + "name": "Full Detailed Observation", + "submitter": "tbeccue", + "proposal": "LCOSchedulerTest", + "ipp_value": 1.05, + "priority": 10, + "request": { + "id": 3445199, + "modified": "2024-10-29T14:57:32.128148Z", + "state": "PENDING", + "duration": 282, + "acceptability_threshold": 90.0, + "optimization_type": "TIME", + "observation_note": "", + "extra_params": {}, + "configuration_repeats": 2, + "configurations": [ + { + "id": 10840779, + "configuration_status": 750648588, + "instrument_name": "q461", + "instrument_type": "0M35-QHY461", + "priority": 1, + "repeat_duration": null, + "state": "PENDING", + "summary": {}, + "type": "EXPOSE", + "target": { + "dec": -7.6528696608383, + "epoch": 2000.0, + "extra_params": {}, + "hour_angle": null, + "name": "40 Eridani", + "parallax": 199.608, + "proper_motion_dec": -3421.809, + "proper_motion_ra": -2240.085, + "ra": 63.8179984124771, + "type": "ICRS" + }, + "instrument_configs": [ + { + "exposure_count": 1, + "exposure_time": 15.0, + "extra_params": { + "offset_dec": 1, + "offset_ra": 2, + "rotator_angle": 5 + }, + "mode": "Full", + "optical_elements": { + "filter": "mrc-L" + }, + "rois": [], + "rotator_mode": "RPA" + }, + { + "exposure_count": 2, + "exposure_time": 10.0, + "extra_params": { + "offset_dec": 0, + "offset_ra": 0, + "rotator_angle": 0 + }, + "mode": "Full", + "optical_elements": { + "filter": "mrc-R" + }, + "rois": [], + "rotator_mode": "RPA" + } + ], + "acquisition_config": { + "extra_params": {}, + "mode": "OFF" + }, + "constraints": { + "extra_params": {}, + "max_airmass": 1.6, + "max_lunar_phase": 1.0, + "min_lunar_distance": 30.0 + }, + "extra_params": { + "dither_pattern": "custom" + }, + "guide_camera_name": "mrc-qhy461", + "guiding_config": { + "exposure_time": null, + "extra_params": {}, + "mode": "ON", + "optical_elements": {}, + "optional": true + } + }, + { + "acquisition_config": { + "extra_params": {}, + "mode": "OFF" + }, + "configuration_status": 750648589, + "constraints": { + "extra_params": {}, + "max_airmass": 1.6, + "max_lunar_phase": 1.0, + "min_lunar_distance": 30.0 + }, + "extra_params": { + "dither_pattern": "custom" + }, + "guide_camera_name": "mrc-qhy461", + "guiding_config": { + "exposure_time": null, + "extra_params": {}, + "mode": "ON", + "optical_elements": {}, + "optional": true + }, + "id": 10840780, + "instrument_configs": [ + { + "exposure_count": 1, + "exposure_time": 15.0, + "extra_params": { + "offset_dec": 1, + "offset_ra": 2, + "rotator_angle": 5 + }, + "mode": "Full", + "optical_elements": { + "filter": "mrc-L" + }, + "rois": [], + "rotator_mode": "RPA" + } + ], + "instrument_name": "q461", + "instrument_type": "0M35-QHY461", + "priority": 2, + "repeat_duration": null, + "state": "PENDING", + "summary": {}, + "target": { + "dec": 41.26875, + "epoch": 2000.0, + "extra_params": {}, + "hour_angle": null, + "name": "m31", + "parallax": 0.0, + "proper_motion_dec": 0.0, + "proper_motion_ra": 0.0, + "ra": 10.684708, + "type": "ICRS" + }, + "type": "EXPOSE" + }, + { + "acquisition_config": { + "extra_params": {}, + "mode": "OFF" + }, + "configuration_status": 750648590, + "constraints": { + "extra_params": {}, + "max_airmass": 1.6, + "max_lunar_phase": 1.0, + "min_lunar_distance": 30.0 + }, + "extra_params": { + "dither_pattern": "custom" + }, + "guide_camera_name": "mrc-qhy461", + "guiding_config": { + "exposure_time": null, + "extra_params": {}, + "mode": "ON", + "optical_elements": {}, + "optional": true + }, + "id": 10840779, + "instrument_configs": [ + { + "exposure_count": 1, + "exposure_time": 15.0, + "extra_params": { + "offset_dec": 1, + "offset_ra": 2, + "rotator_angle": 5 + }, + "mode": "Full", + "optical_elements": { + "filter": "mrc-L" + }, + "rois": [], + "rotator_mode": "RPA" + }, + { + "exposure_count": 2, + "exposure_time": 10.0, + "extra_params": { + "offset_dec": 0, + "offset_ra": 0, + "rotator_angle": 0 + }, + "mode": "Full", + "optical_elements": { + "filter": "mrc-R" + }, + "rois": [], + "rotator_mode": "RPA" + } + ], + "instrument_name": "q461", + "instrument_type": "0M35-QHY461", + "priority": 3, + "repeat_duration": null, + "state": "PENDING", + "summary": {}, + "target": { + "dec": -7.6528696608383, + "epoch": 2000.0, + "extra_params": {}, + "hour_angle": null, + "name": "40 Eridani", + "parallax": 199.608, + "proper_motion_dec": -3421.809, + "proper_motion_ra": -2240.085, + "ra": 63.8179984124771, + "type": "ICRS" + }, + "type": "EXPOSE" + }, + { + "acquisition_config": { + "extra_params": {}, + "mode": "OFF" + }, + "configuration_status": 750648591, + "constraints": { + "extra_params": {}, + "max_airmass": 1.6, + "max_lunar_phase": 1.0, + "min_lunar_distance": 30.0 + }, + "extra_params": { + "dither_pattern": "custom" + }, + "guide_camera_name": "mrc-qhy461", + "guiding_config": { + "exposure_time": null, + "extra_params": {}, + "mode": "ON", + "optical_elements": {}, + "optional": true + }, + "id": 10840780, + "instrument_configs": [ + { + "exposure_count": 1, + "exposure_time": 15.0, + "extra_params": { + "offset_dec": 1, + "offset_ra": 2, + "rotator_angle": 5 + }, + "mode": "Full", + "optical_elements": { + "filter": "mrc-L" + }, + "rois": [], + "rotator_mode": "RPA" + } + ], + "instrument_name": "q461", + "instrument_type": "0M35-QHY461", + "priority": 4, + "repeat_duration": null, + "state": "PENDING", + "summary": {}, + "target": { + "dec": 41.26875, + "epoch": 2000.0, + "extra_params": {}, + "hour_angle": null, + "name": "m31", + "parallax": 0.0, + "proper_motion_dec": 0.0, + "proper_motion_ra": 0.0, + "ra": 10.684708, + "type": "ICRS" + }, + "type": "EXPOSE" + } + ] + } +} \ No newline at end of file diff --git a/tests/test_import_schedules.py b/tests/test_import_schedules.py new file mode 100644 index 0000000..e57cde2 --- /dev/null +++ b/tests/test_import_schedules.py @@ -0,0 +1,24 @@ +import json +from import_schedules import Observation + +with open('./test_data/sample_observation.json', 'r') as file: + sample_observation = json.load(file) + +def test_observation_validation(): + o = Observation(sample_observation) + assert Observation.validate_observation_format(sample_observation) + assert Observation.validate_observation_format(o.observation) + +def test_observation_create_calendar(): + o = Observation(sample_observation) + assert not hasattr(o, "calendar_event") + o._translate_to_calendar() + assert hasattr(o, "calendar_event") + +def test_observation_create_project(): + o = Observation(sample_observation) + assert not hasattr(o, "project") + o._translate_to_project() + assert hasattr(o, "project") + + diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..b2421fa --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,112 @@ +import pytest +from utils import create_calendar_event +from utils import get_event_by_id +from utils import get_events_during_time +from utils import get_utc_iso_time + +import datetime +import re +import boto3 +from unittest.mock import patch +from moto import mock_dynamodb + +@pytest.fixture +def mock_calendar_table(): + with mock_dynamodb(): + dynamodb = boto3.resource('dynamodb') + + # Create the calendar table with primary key and GSIs + table = dynamodb.create_table( + TableName='CalendarTable', + KeySchema=[ + {'AttributeName': 'event_id', 'KeyType': 'HASH'}, + {'AttributeName': 'start', 'KeyType': 'RANGE'} + ], + AttributeDefinitions=[ + {'AttributeName': 'event_id', 'AttributeType': 'S'}, + {'AttributeName': 'start', 'AttributeType': 'S'}, + {'AttributeName': 'end', 'AttributeType': 'S'}, + {'AttributeName': 'site', 'AttributeType': 'S'}, + {'AttributeName': 'creator_id', 'AttributeType': 'S'} + ], + GlobalSecondaryIndexes=[ + { + 'IndexName': 'creatorid-end-index', + 'KeySchema': [ + {'AttributeName': 'creator_id', 'KeyType': 'HASH'}, + {'AttributeName': 'end', 'KeyType': 'RANGE'} + ], + 'Projection': {'ProjectionType': 'ALL'}, + 'ProvisionedThroughput': {'ReadCapacityUnits': 1, 'WriteCapacityUnits': 1} + }, + { + 'IndexName': 'site-end-index', + 'KeySchema': [ + {'AttributeName': 'site', 'KeyType': 'HASH'}, + {'AttributeName': 'end', 'KeyType': 'RANGE'} + ], + 'Projection': {'ProjectionType': 'ALL'}, + 'ProvisionedThroughput': {'ReadCapacityUnits': 1, 'WriteCapacityUnits': 1} + } + ], + ProvisionedThroughput={'ReadCapacityUnits': 1, 'WriteCapacityUnits': 1} + ) + + # Insert mock data + table.put_item(Item={ + 'event_id': '1', + 'start': '2024-12-01T09:00:00', + 'end': '2024-12-01T10:00:00', + 'site': 'tst', + 'creator_id': 'testuser' + }) + yield table + +def test_get_utc_iso_time(): + fixed_time = datetime.datetime(2023, 12, 10, 15, 30, 45, tzinfo=datetime.timezone.utc) + + with patch('datetime.datetime') as mock_datetime: + mock_datetime.now.return_value = fixed_time + mock_datetime.timezone = datetime.timezone + + result = get_utc_iso_time() + + # Check the format + assert re.match(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$', result), "Output format is incorrect" + + # Verify the output matches the mocked time + expected = fixed_time.strftime("%Y-%m-%dT%H:%M:%SZ") + assert result == expected, f"Expected {expected}, got {result}" + + +def test_create_calendar_event_success(mock_calendar_table): + # Arrange + event = { + 'event_id': '2', + 'start': '2024-12-02T09:00:00', + 'end': '2024-12-02T10:00:00', + 'site': 'tst', + 'creator_id': 'testuser2' + } + with patch('utils.calendar_table', mock_calendar_table): + # Act + result = create_calendar_event(event) + + # Assert + response = mock_calendar_table.get_item(Key={'event_id': '2', 'start': '2024-12-02T09:00:00'}) + assert 'Item' in response + assert response['Item'] == event + assert result['ResponseMetadata']['HTTPStatusCode'] == 200 + +def test_create_calendar_event_bad_event(mock_calendar_table): + # Arrange + event_missing_id = { + 'start': '2024-12-02T09:00:00', + 'end': '2024-12-02T10:00:00', + 'site': 'tst', + 'creator_id': 'testuser2' + } + with patch('utils.calendar_table', mock_calendar_table): + # Act & Assert + with pytest.raises(Exception, match="ValidationException"): + create_calendar_event(event_missing_id)