|
| 1 | +import io |
| 2 | +import json |
| 3 | +from unittest.mock import MagicMock, patch |
| 4 | + |
| 5 | +import pytest |
| 6 | +from botocore.exceptions import ClientError |
| 7 | +from migrate_v1_perms_by_app import ( |
| 8 | + CONSUMER_OR_PRODUCER, |
| 9 | + _read_and_transform, |
| 10 | + migrate_v1_perms_by_app, |
| 11 | +) |
| 12 | + |
| 13 | +ENV = "dev" |
| 14 | +FOLDER = "my-app-folder" |
| 15 | +BUCKET = f"nhsd-nrlf--{ENV}-authorization-store" |
| 16 | + |
| 17 | +SAMPLE_V1_PERMS = [ |
| 18 | + "http://snomed.info/sct|736253002", |
| 19 | + "http://snomed.info/sct|1363501000000100", |
| 20 | + "http://snomed.info/sct|736366004", |
| 21 | +] |
| 22 | + |
| 23 | +# --------------------------------------------------------------------------- |
| 24 | +# Helper functions |
| 25 | +# --------------------------------------------------------------------------- |
| 26 | + |
| 27 | + |
| 28 | +def _make_client_error(code: str, message: str) -> ClientError: |
| 29 | + return ClientError( |
| 30 | + {"Error": {"Code": code, "Message": message}}, |
| 31 | + operation_name="S3Operation", |
| 32 | + ) |
| 33 | + |
| 34 | + |
| 35 | +def _mock_s3_client_with_response(data_to_return: bytes) -> MagicMock: |
| 36 | + s3 = MagicMock() |
| 37 | + s3.get_object.return_value = {"Body": io.BytesIO(data_to_return)} |
| 38 | + return s3 |
| 39 | + |
| 40 | + |
| 41 | +# --------------------------------------------------------------------------- |
| 42 | +# Unit tests for _read_and_transform |
| 43 | +# --------------------------------------------------------------------------- |
| 44 | + |
| 45 | +FILE_PATH = f"{FOLDER}/perms.json" |
| 46 | + |
| 47 | + |
| 48 | +def test_read_and_transform_returns_wrapped_json_and_count(): |
| 49 | + s3 = _mock_s3_client_with_response(json.dumps(SAMPLE_V1_PERMS).encode()) |
| 50 | + |
| 51 | + body, count = _read_and_transform(s3, BUCKET, FILE_PATH) |
| 52 | + |
| 53 | + assert count == len(SAMPLE_V1_PERMS) |
| 54 | + assert json.loads(body) == {"types": SAMPLE_V1_PERMS} |
| 55 | + s3.get_object.assert_called_once_with(Bucket=BUCKET, Key=FILE_PATH) |
| 56 | + |
| 57 | + |
| 58 | +def test_read_and_transform_empty_list(): |
| 59 | + s3 = _mock_s3_client_with_response(b"[]") |
| 60 | + |
| 61 | + body, count = _read_and_transform(s3, BUCKET, FILE_PATH) |
| 62 | + |
| 63 | + assert count == 0 |
| 64 | + assert json.loads(body) == {"types": []} |
| 65 | + |
| 66 | + |
| 67 | +def test_read_and_transform_raises_value_error_for_non_list(): |
| 68 | + s3 = _mock_s3_client_with_response(b'{"key": "value"}') |
| 69 | + |
| 70 | + with pytest.raises(ValueError, match="Expected a JSON array, got dict"): |
| 71 | + _read_and_transform(s3, BUCKET, FILE_PATH) |
| 72 | + |
| 73 | + |
| 74 | +def test_read_and_transform_raises_runtime_error_on_client_error(): |
| 75 | + s3 = MagicMock() |
| 76 | + s3.get_object.side_effect = _make_client_error( |
| 77 | + "NoSuchKey", "The specified key does not exist" |
| 78 | + ) |
| 79 | + |
| 80 | + with pytest.raises( |
| 81 | + RuntimeError, |
| 82 | + match=f"Failed to read s3://{BUCKET}/{FILE_PATH}.*The specified key does not exist", |
| 83 | + ): |
| 84 | + _read_and_transform(s3, BUCKET, FILE_PATH) |
| 85 | + |
| 86 | + |
| 87 | +# --------------------------------------------------------------------------- |
| 88 | +# Unit tests for migrate_v1_perms_by_app |
| 89 | +# --------------------------------------------------------------------------- |
| 90 | + |
| 91 | +MODULE = "migrate_v1_perms_by_app" |
| 92 | + |
| 93 | +TRANSFORMED_BODY = '{"types": ["http://snomed.info/sct|736253002"]}' |
| 94 | +ENTRY_COUNT = 1 |
| 95 | + |
| 96 | + |
| 97 | +@patch(f"{MODULE}._write_v2_consumer_and_producer_files") |
| 98 | +@patch(f"{MODULE}._read_and_transform") |
| 99 | +@patch(f"{MODULE}._list_json_files") |
| 100 | +@patch(f"{MODULE}._get_s3_client") |
| 101 | +@patch(f"{MODULE}._get_bucket_name") |
| 102 | +def test_migrate_processes_each_file( |
| 103 | + mock_bucket, mock_s3, mock_list, mock_transform, mock_write |
| 104 | +): |
| 105 | + mock_bucket.return_value = BUCKET |
| 106 | + s3 = MagicMock() |
| 107 | + mock_s3.return_value = s3 |
| 108 | + mock_list.return_value = [f"{FOLDER}/a.json", f"{FOLDER}/b.json"] |
| 109 | + mock_transform.return_value = (TRANSFORMED_BODY, ENTRY_COUNT) |
| 110 | + |
| 111 | + migrate_v1_perms_by_app(ENV, FOLDER) |
| 112 | + |
| 113 | + mock_bucket.assert_called_once_with(ENV) |
| 114 | + mock_s3.assert_called_once_with(ENV) |
| 115 | + mock_list.assert_called_once_with(s3, BUCKET, FOLDER) |
| 116 | + assert mock_transform.call_count == 2 |
| 117 | + assert mock_write.call_count == 2 |
| 118 | + mock_write.assert_any_call( |
| 119 | + s3, BUCKET, f"{FOLDER}/a.json", TRANSFORMED_BODY, ENTRY_COUNT |
| 120 | + ) |
| 121 | + mock_write.assert_any_call( |
| 122 | + s3, BUCKET, f"{FOLDER}/b.json", TRANSFORMED_BODY, ENTRY_COUNT |
| 123 | + ) |
| 124 | + |
| 125 | + |
| 126 | +@patch(f"{MODULE}._write_v2_consumer_and_producer_files") |
| 127 | +@patch(f"{MODULE}._read_and_transform") |
| 128 | +@patch(f"{MODULE}._list_json_files") |
| 129 | +@patch(f"{MODULE}._get_s3_client") |
| 130 | +@patch(f"{MODULE}._get_bucket_name") |
| 131 | +def test_migrate_no_files_skips_transform_and_write( |
| 132 | + mock_bucket, mock_s3, mock_list, mock_transform, mock_write |
| 133 | +): |
| 134 | + mock_bucket.return_value = BUCKET |
| 135 | + mock_s3.return_value = MagicMock() |
| 136 | + mock_list.return_value = [] |
| 137 | + |
| 138 | + migrate_v1_perms_by_app(ENV, FOLDER) |
| 139 | + |
| 140 | + mock_transform.assert_not_called() |
| 141 | + mock_write.assert_not_called() |
0 commit comments