Merge branch 'develop-6crm' into feature-NEWDWH2021-720

This commit is contained in:
Y_SAKAI 2022-09-22 15:17:09 +09:00
commit 714c14bb12
210 changed files with 16259 additions and 1 deletions

7
.gitignore vendored
View File

@ -7,4 +7,9 @@ node_modules/
__pycache__/
# StepFunctionsステートメント定義変換後のフォルダ
stepfunctions/*/build
stepfunctions/*/build
**/.vscode/settings.json
# python test
.coverage
.report/

25
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
// IntelliSense 使
//
// : https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: データ取り込みローカル実行",
"type": "python",
"request": "launch",
// windows\
"program": "ecs\\dataimport\\dataimport\\controller.py",
"console": "integratedTerminal",
"justMyCode": true,
"envFile": "${workspaceFolder}/.env"
},
{
"name": "Python: Attach using Process Id",
"type": "python",
"request": "attach",
"processId": "${command:pickProcess}",
"justMyCode": true
}
]
}

13
ecs/Dockerfile/Pipfile Normal file
View File

@ -0,0 +1,13 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
boto3 = "*"
pymysql = "*"
[dev-packages]
[requires]
python_version = "3.9"

85
ecs/Dockerfile/Pipfile.lock generated Normal file
View File

@ -0,0 +1,85 @@
{
"_meta": {
"hash": {
"sha256": "7c69c0c237f231fcd67984f1d1b171c2eebfe00ed1877894bbbe77e201862057"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.9"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"boto3": {
"hashes": [
"sha256:490f5e88f5551b33ae3019a37412158b76426d63d1fb910968ade9b6a024e5fe",
"sha256:e284705da36faa668c715ae1f74ebbff4320dbfbe3a733df3a8ab076d1ed1226"
],
"index": "pypi",
"version": "==1.24.14"
},
"botocore": {
"hashes": [
"sha256:bb56fa77b8fa1ec367c2e16dee62d60000451aac5140dcce3ebddc167fd5c593",
"sha256:df1e9b208ff93daac7c645b0b04fb6dccd7f20262eae24d87941727025cbeece"
],
"markers": "python_version >= '3.7'",
"version": "==1.27.14"
},
"jmespath": {
"hashes": [
"sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
"sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
],
"markers": "python_version >= '3.7'",
"version": "==1.0.1"
},
"pymysql": {
"hashes": [
"sha256:41fc3a0c5013d5f039639442321185532e3e2c8924687abe6537de157d403641",
"sha256:816927a350f38d56072aeca5dfb10221fe1dc653745853d30a216637f5d7ad36"
],
"index": "pypi",
"version": "==1.0.2"
},
"python-dateutil": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"version": "==2.8.2"
},
"s3transfer": {
"hashes": [
"sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd",
"sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"
],
"markers": "python_version >= '3.7'",
"version": "==0.6.0"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
"version": "==1.16.0"
},
"urllib3": {
"hashes": [
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
"sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.9"
}
},
"develop": {}
}

View File

@ -0,0 +1,11 @@
tests/*
.coverage
.env
.env.example
.report/*
.vscode/*
.pytest_cache/*
*/__pychache__/*
Dockerfile
pytest.ini
README.md

View File

@ -0,0 +1,17 @@
CRM_AUTH_DOMAIN=test
CRM_USER_NAME=test
CRM_USER_PASSWORD=test
CRM_USER_SECURITY_TOKEN=test
CRM_CONFIG_BUCKET=test
CRM_BACKUP_BUCKET=test
IMPORT_DATA_BUCKET=test
OBJECT_INFO_FOLDER=test
OBJECT_INFO_FILENAME=test
PROCESS_RESULT_FOLDER=test
PROCESS_RESULT_FILENAME=test
LAST_FETCH_DATE_FOLDER=test
CRM_IMPORT_DATA_FOLDER=test
LAST_FETCH_DATE_BACKUP_FOLDER=test
RESPONSE_JSON_BACKUP_FOLDER=test
CRM_IMPORT_DATA_BACKUP_FOLDER=test
LOG_LEVEL=INFO

16
ecs/crm-datafetch/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
//
"program": "main.py",
"console": "integratedTerminal",
"justMyCode": true,
//
"envFile": "${workspaceFolder}/.env",
}
]
}

View File

@ -0,0 +1,17 @@
{
"Generate Test docstring": {
"scope": "python",
"prefix": "\"\"\"\"\"\"",
"body": [
"\"\"\"",
"Cases:",
" $1",
"Arranges:",
" $2",
"Expects:",
" $3",
"\"\"\""
],
"description": "Test docstring (User Snippets)"
}
}

View File

@ -0,0 +1,22 @@
{
"[python]": {
"editor.defaultFormatter": null,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"python.linting.lintOnSave": true,
"python.linting.enabled": true,
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.linting.flake8Args": ["--max-line-length=150", "--ignore=F541"],
"python.formatting.provider": "autopep8",
"python.formatting.autopep8Args": ["--max-line-length", "150"],
"python.testing.pytestArgs": [
"tests"
// "--walk-through" //
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

View File

@ -0,0 +1,19 @@
FROM python:3.8
ENV TZ="Asia/Tokyo"
WORKDIR /usr/src/app
COPY Pipfile Pipfile.lock ./
RUN \
apt update -y && \
# パッケージのセキュリティアップデートのみを適用するコマンド
apt install -y unattended-upgrades && \
unattended-upgrades && \
pip install pipenv --no-cache-dir && \
pipenv install --system --deploy && \
pip uninstall -y pipenv virtualenv-clone virtualenv
COPY main.py ./
COPY src ./src
CMD [ "python", "./main.py" ]

26
ecs/crm-datafetch/Pipfile Normal file
View File

@ -0,0 +1,26 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[scripts]
test = "pytest tests/"
"test:cov" = "pytest --cov=src --cov-branch --cov-report=term-missing tests/"
"test:report" = "pytest --cov=src --cov-branch --cov-report=term-missing --html=.report/unit_test/test_result.html tests/"
"test:walk-through" = "pytest tests/test_walk_through.py --walk-through --cov-report=term-missing --html=.report/walk_through/test_result.html"
[packages]
boto3 = "*"
simple-salesforce = "*"
tenacity = "*"
[dev-packages]
autopep8 = "*"
flake8 = "*"
pytest = "*"
pytest-cov = "*"
pytest-html = "*"
moto = "*"
[requires]
python_version = "3.8"

842
ecs/crm-datafetch/Pipfile.lock generated Normal file
View File

@ -0,0 +1,842 @@
{
"_meta": {
"hash": {
"sha256": "7006de596d6123ecd56760b584ab75430fa6bcfc0ecd3fdf49f08368ff53477d"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.8"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"attrs": {
"hashes": [
"sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6",
"sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"
],
"markers": "python_version >= '3.5'",
"version": "==22.1.0"
},
"authlib": {
"hashes": [
"sha256:1286e2d5ef5bfe5a11cc2d0a0d1031f0393f6ce4d61f5121cfe87fa0054e98bd",
"sha256:6e74a4846ac36dfc882b3cc2fbd3d9eb410a627f2f2dc11771276655345223b1"
],
"version": "==1.0.1"
},
"boto3": {
"hashes": [
"sha256:aec404d06690a0ca806592efc6bbe30a9ac88fa2ad73b6d907f7794a8b3b1459",
"sha256:df3d6ef02304bd7c3711090936c092d5db735dda109decac1e236c3ef7fdb7af"
],
"index": "pypi",
"version": "==1.24.42"
},
"botocore": {
"hashes": [
"sha256:38a180a6666c5a9b069a75ec3cf374ff2a64c3e90c9f24a916858bcdeb04456d",
"sha256:f8e6c2f69a9d577fb9c69e4e74f49f4315a48decee0e7dc88b6e470772110884"
],
"markers": "python_version >= '3.7'",
"version": "==1.27.42"
},
"cached-property": {
"hashes": [
"sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130",
"sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0"
],
"version": "==1.5.2"
},
"certifi": {
"hashes": [
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
],
"markers": "python_version >= '3.6'",
"version": "==2022.6.15"
},
"cffi": {
"hashes": [
"sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5",
"sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef",
"sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104",
"sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426",
"sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405",
"sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375",
"sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a",
"sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e",
"sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc",
"sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf",
"sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185",
"sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497",
"sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3",
"sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35",
"sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c",
"sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83",
"sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21",
"sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca",
"sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984",
"sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac",
"sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd",
"sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee",
"sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a",
"sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2",
"sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192",
"sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7",
"sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585",
"sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f",
"sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e",
"sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27",
"sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b",
"sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e",
"sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e",
"sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d",
"sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c",
"sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415",
"sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82",
"sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02",
"sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314",
"sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325",
"sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c",
"sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3",
"sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914",
"sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045",
"sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d",
"sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9",
"sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5",
"sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2",
"sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c",
"sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3",
"sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2",
"sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8",
"sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d",
"sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d",
"sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9",
"sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162",
"sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76",
"sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4",
"sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e",
"sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9",
"sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6",
"sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b",
"sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01",
"sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"
],
"version": "==1.15.1"
},
"charset-normalizer": {
"hashes": [
"sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5",
"sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"
],
"markers": "python_version >= '3.6'",
"version": "==2.1.0"
},
"cryptography": {
"hashes": [
"sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59",
"sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596",
"sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3",
"sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5",
"sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab",
"sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884",
"sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82",
"sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b",
"sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441",
"sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa",
"sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d",
"sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b",
"sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a",
"sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6",
"sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157",
"sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280",
"sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282",
"sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67",
"sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8",
"sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046",
"sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327",
"sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"
],
"markers": "python_version >= '3.6'",
"version": "==37.0.4"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3.5'",
"version": "==3.3"
},
"isodate": {
"hashes": [
"sha256:0751eece944162659049d35f4f549ed815792b38793f07cf73381c1c87cbed96",
"sha256:48c5881de7e8b0a0d648cb024c8062dc84e7b840ed81e864c7614fd3c127bde9"
],
"version": "==0.6.1"
},
"jmespath": {
"hashes": [
"sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
"sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
],
"markers": "python_version >= '3.7'",
"version": "==1.0.1"
},
"lxml": {
"hashes": [
"sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318",
"sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c",
"sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b",
"sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000",
"sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73",
"sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d",
"sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb",
"sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8",
"sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2",
"sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345",
"sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94",
"sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e",
"sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b",
"sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc",
"sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a",
"sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9",
"sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc",
"sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387",
"sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb",
"sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7",
"sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4",
"sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97",
"sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67",
"sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627",
"sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7",
"sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd",
"sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3",
"sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7",
"sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130",
"sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b",
"sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036",
"sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785",
"sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca",
"sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91",
"sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc",
"sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536",
"sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391",
"sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3",
"sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d",
"sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21",
"sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3",
"sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d",
"sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29",
"sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715",
"sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed",
"sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25",
"sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c",
"sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785",
"sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837",
"sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4",
"sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b",
"sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2",
"sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067",
"sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448",
"sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d",
"sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2",
"sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc",
"sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c",
"sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5",
"sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84",
"sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8",
"sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf",
"sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7",
"sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e",
"sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb",
"sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b",
"sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3",
"sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad",
"sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8",
"sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.9.1"
},
"platformdirs": {
"hashes": [
"sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788",
"sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"
],
"markers": "python_version >= '3.7'",
"version": "==2.5.2"
},
"pycparser": {
"hashes": [
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
"sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
],
"version": "==2.21"
},
"python-dateutil": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2"
},
"pytz": {
"hashes": [
"sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7",
"sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"
],
"version": "==2022.1"
},
"requests": {
"hashes": [
"sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983",
"sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"
],
"markers": "python_version >= '3.7' and python_version < '4'",
"version": "==2.28.1"
},
"requests-file": {
"hashes": [
"sha256:07d74208d3389d01c38ab89ef403af0cfec63957d53a0081d8eca738d0247d8e",
"sha256:dfe5dae75c12481f68ba353183c53a65e6044c923e64c24b2209f6c7570ca953"
],
"version": "==1.5.1"
},
"requests-toolbelt": {
"hashes": [
"sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
"sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
],
"version": "==0.9.1"
},
"s3transfer": {
"hashes": [
"sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd",
"sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"
],
"markers": "python_version >= '3.7'",
"version": "==0.6.0"
},
"simple-salesforce": {
"hashes": [
"sha256:15d6943e52252c9cc28e1779803354f2a36c88b72056499e07eb06cd652f149c",
"sha256:7931038081c445e9459ddc014aaf7f540b1131a31596956cb5d7c0e7b7e0c4cb"
],
"index": "pypi",
"version": "==1.12.1"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"tenacity": {
"hashes": [
"sha256:43242a20e3e73291a28bcbcacfd6e000b02d3857a9a9fff56b297a27afdc932f",
"sha256:f78f4ea81b0fabc06728c11dc2a8c01277bfc5181b321a4770471902e3eb844a"
],
"index": "pypi",
"version": "==8.0.1"
},
"urllib3": {
"hashes": [
"sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc",
"sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'",
"version": "==1.26.11"
},
"zeep": {
"hashes": [
"sha256:5867f2eadd6b028d9751f4155af590d3aaf9280e3a0ed5e15a53343921c956e5",
"sha256:81c491092b71f5b276de8c63dfd452be3f322622c48a54f3a497cf913bdfb2f4"
],
"markers": "python_version >= '3.6'",
"version": "==4.1.0"
}
},
"develop": {
"attrs": {
"hashes": [
"sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6",
"sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"
],
"markers": "python_version >= '3.5'",
"version": "==22.1.0"
},
"autopep8": {
"hashes": [
"sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979",
"sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f"
],
"index": "pypi",
"version": "==1.6.0"
},
"boto3": {
"hashes": [
"sha256:aec404d06690a0ca806592efc6bbe30a9ac88fa2ad73b6d907f7794a8b3b1459",
"sha256:df3d6ef02304bd7c3711090936c092d5db735dda109decac1e236c3ef7fdb7af"
],
"index": "pypi",
"version": "==1.24.42"
},
"botocore": {
"hashes": [
"sha256:38a180a6666c5a9b069a75ec3cf374ff2a64c3e90c9f24a916858bcdeb04456d",
"sha256:f8e6c2f69a9d577fb9c69e4e74f49f4315a48decee0e7dc88b6e470772110884"
],
"markers": "python_version >= '3.7'",
"version": "==1.27.42"
},
"certifi": {
"hashes": [
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
],
"markers": "python_version >= '3.6'",
"version": "==2022.6.15"
},
"cffi": {
"hashes": [
"sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5",
"sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef",
"sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104",
"sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426",
"sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405",
"sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375",
"sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a",
"sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e",
"sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc",
"sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf",
"sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185",
"sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497",
"sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3",
"sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35",
"sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c",
"sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83",
"sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21",
"sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca",
"sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984",
"sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac",
"sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd",
"sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee",
"sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a",
"sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2",
"sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192",
"sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7",
"sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585",
"sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f",
"sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e",
"sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27",
"sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b",
"sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e",
"sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e",
"sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d",
"sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c",
"sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415",
"sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82",
"sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02",
"sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314",
"sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325",
"sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c",
"sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3",
"sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914",
"sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045",
"sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d",
"sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9",
"sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5",
"sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2",
"sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c",
"sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3",
"sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2",
"sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8",
"sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d",
"sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d",
"sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9",
"sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162",
"sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76",
"sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4",
"sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e",
"sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9",
"sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6",
"sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b",
"sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01",
"sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"
],
"version": "==1.15.1"
},
"charset-normalizer": {
"hashes": [
"sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5",
"sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"
],
"markers": "python_version >= '3.6'",
"version": "==2.1.0"
},
"coverage": {
"extras": [
"toml"
],
"hashes": [
"sha256:0895ea6e6f7f9939166cc835df8fa4599e2d9b759b02d1521b574e13b859ac32",
"sha256:0f211df2cba951ffcae210ee00e54921ab42e2b64e0bf2c0befc977377fb09b7",
"sha256:147605e1702d996279bb3cc3b164f408698850011210d133a2cb96a73a2f7996",
"sha256:24b04d305ea172ccb21bee5bacd559383cba2c6fcdef85b7701cf2de4188aa55",
"sha256:25b7ec944f114f70803d6529394b64f8749e93cbfac0fe6c5ea1b7e6c14e8a46",
"sha256:2b20286c2b726f94e766e86a3fddb7b7e37af5d0c635bdfa7e4399bc523563de",
"sha256:2dff52b3e7f76ada36f82124703f4953186d9029d00d6287f17c68a75e2e6039",
"sha256:2f8553878a24b00d5ab04b7a92a2af50409247ca5c4b7a2bf4eabe94ed20d3ee",
"sha256:3def6791adf580d66f025223078dc84c64696a26f174131059ce8e91452584e1",
"sha256:422fa44070b42fef9fb8dabd5af03861708cdd6deb69463adc2130b7bf81332f",
"sha256:4f89d8e03c8a3757aae65570d14033e8edf192ee9298303db15955cadcff0c63",
"sha256:5336e0352c0b12c7e72727d50ff02557005f79a0b8dcad9219c7c4940a930083",
"sha256:54d8d0e073a7f238f0666d3c7c0d37469b2aa43311e4024c925ee14f5d5a1cbe",
"sha256:5ef42e1db047ca42827a85e34abe973971c635f83aed49611b7f3ab49d0130f0",
"sha256:5f65e5d3ff2d895dab76b1faca4586b970a99b5d4b24e9aafffc0ce94a6022d6",
"sha256:6c3ccfe89c36f3e5b9837b9ee507472310164f352c9fe332120b764c9d60adbe",
"sha256:6d0b48aff8e9720bdec315d67723f0babd936a7211dc5df453ddf76f89c59933",
"sha256:6fe75dcfcb889b6800f072f2af5a331342d63d0c1b3d2bf0f7b4f6c353e8c9c0",
"sha256:79419370d6a637cb18553ecb25228893966bd7935a9120fa454e7076f13b627c",
"sha256:7bb00521ab4f99fdce2d5c05a91bddc0280f0afaee0e0a00425e28e209d4af07",
"sha256:80db4a47a199c4563d4a25919ff29c97c87569130375beca3483b41ad5f698e8",
"sha256:866ebf42b4c5dbafd64455b0a1cd5aa7b4837a894809413b930026c91e18090b",
"sha256:8af6c26ba8df6338e57bedbf916d76bdae6308e57fc8f14397f03b5da8622b4e",
"sha256:a13772c19619118903d65a91f1d5fea84be494d12fd406d06c849b00d31bf120",
"sha256:a697977157adc052284a7160569b36a8bbec09db3c3220642e6323b47cec090f",
"sha256:a9032f9b7d38bdf882ac9f66ebde3afb8145f0d4c24b2e600bc4c6304aafb87e",
"sha256:b5e28db9199dd3833cc8a07fa6cf429a01227b5d429facb56eccd765050c26cd",
"sha256:c77943ef768276b61c96a3eb854eba55633c7a3fddf0a79f82805f232326d33f",
"sha256:d230d333b0be8042ac34808ad722eabba30036232e7a6fb3e317c49f61c93386",
"sha256:d4548be38a1c810d79e097a38107b6bf2ff42151900e47d49635be69943763d8",
"sha256:d4e7ced84a11c10160c0697a6cc0b214a5d7ab21dfec1cd46e89fbf77cc66fae",
"sha256:d56f105592188ce7a797b2bd94b4a8cb2e36d5d9b0d8a1d2060ff2a71e6b9bbc",
"sha256:d714af0bdba67739598849c9f18efdcc5a0412f4993914a0ec5ce0f1e864d783",
"sha256:d774d9e97007b018a651eadc1b3970ed20237395527e22cbeb743d8e73e0563d",
"sha256:e0524adb49c716ca763dbc1d27bedce36b14f33e6b8af6dba56886476b42957c",
"sha256:e2618cb2cf5a7cc8d698306e42ebcacd02fb7ef8cfc18485c59394152c70be97",
"sha256:e36750fbbc422c1c46c9d13b937ab437138b998fe74a635ec88989afb57a3978",
"sha256:edfdabe7aa4f97ed2b9dd5dde52d2bb29cb466993bb9d612ddd10d0085a683cf",
"sha256:f22325010d8824594820d6ce84fa830838f581a7fd86a9235f0d2ed6deb61e29",
"sha256:f23876b018dfa5d3e98e96f5644b109090f16a4acb22064e0f06933663005d39",
"sha256:f7bd0ffbcd03dc39490a1f40b2669cc414fae0c4e16b77bb26806a4d0b7d1452"
],
"markers": "python_version >= '3.7'",
"version": "==6.4.2"
},
"cryptography": {
"hashes": [
"sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59",
"sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596",
"sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3",
"sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5",
"sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab",
"sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884",
"sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82",
"sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b",
"sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441",
"sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa",
"sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d",
"sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b",
"sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a",
"sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6",
"sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157",
"sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280",
"sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282",
"sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67",
"sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8",
"sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046",
"sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327",
"sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"
],
"markers": "python_version >= '3.6'",
"version": "==37.0.4"
},
"flake8": {
"hashes": [
"sha256:44e3ecd719bba1cb2ae65d1b54212cc9df4f5db15ac271f8856e5e6c2eebefed",
"sha256:9c51d3d1426379fd444d3b79eabbeb887849368bd053039066439523d8486961"
],
"index": "pypi",
"version": "==5.0.1"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3.5'",
"version": "==3.3"
},
"iniconfig": {
"hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
],
"version": "==1.1.1"
},
"jinja2": {
"hashes": [
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
],
"markers": "python_version >= '3.7'",
"version": "==3.1.2"
},
"jmespath": {
"hashes": [
"sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980",
"sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"
],
"markers": "python_version >= '3.7'",
"version": "==1.0.1"
},
"markupsafe": {
"hashes": [
"sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003",
"sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88",
"sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5",
"sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7",
"sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a",
"sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603",
"sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1",
"sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135",
"sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247",
"sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6",
"sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601",
"sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77",
"sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02",
"sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e",
"sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63",
"sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f",
"sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980",
"sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b",
"sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812",
"sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff",
"sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96",
"sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1",
"sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925",
"sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a",
"sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6",
"sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e",
"sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f",
"sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4",
"sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f",
"sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3",
"sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c",
"sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a",
"sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417",
"sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a",
"sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a",
"sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37",
"sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452",
"sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933",
"sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a",
"sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.1"
},
"mccabe": {
"hashes": [
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
],
"markers": "python_version >= '3.6'",
"version": "==0.7.0"
},
"moto": {
"hashes": [
"sha256:8bb8e267d9b948509d4739d81d995615a193d2c459f5c0a979aaeb0d3bd4b381",
"sha256:cbe8ad8a949f519771e5d25b670738604757fb67cd474d75d14c20677582e81f"
],
"index": "pypi",
"version": "==3.1.16"
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
},
"pluggy": {
"hashes": [
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
],
"markers": "python_version >= '3.6'",
"version": "==1.0.0"
},
"py": {
"hashes": [
"sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
"sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.11.0"
},
"pycodestyle": {
"hashes": [
"sha256:289cdc0969d589d90752582bef6dff57c5fbc6949ee8b013ad6d6449a8ae9437",
"sha256:beaba44501f89d785be791c9462553f06958a221d166c64e1f107320f839acc2"
],
"markers": "python_version >= '3.6'",
"version": "==2.9.0"
},
"pycparser": {
"hashes": [
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
"sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
],
"version": "==2.21"
},
"pyflakes": {
"hashes": [
"sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2",
"sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"
],
"markers": "python_version >= '3.6'",
"version": "==2.5.0"
},
"pyparsing": {
"hashes": [
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
"sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
],
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.9"
},
"pytest": {
"hashes": [
"sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c",
"sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"
],
"index": "pypi",
"version": "==7.1.2"
},
"pytest-cov": {
"hashes": [
"sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6",
"sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"
],
"index": "pypi",
"version": "==3.0.0"
},
"pytest-html": {
"hashes": [
"sha256:3ee1cf319c913d19fe53aeb0bc400e7b0bc2dbeb477553733db1dad12eb75ee3",
"sha256:b7f82f123936a3f4d2950bc993c2c1ca09ce262c9ae12f9ac763a2401380b455"
],
"index": "pypi",
"version": "==3.1.1"
},
"pytest-metadata": {
"hashes": [
"sha256:39261ee0086f17649b180baf2a8633e1922a4c4b6fcc28a2de7d8127a82541bf",
"sha256:fcd2f416f15be295943527b3c8ba16a44ae5a7141939c90c3dc5ce9d167cf2a5"
],
"markers": "python_version >= '3.7' and python_version < '4'",
"version": "==2.0.2"
},
"python-dateutil": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2"
},
"pytz": {
"hashes": [
"sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7",
"sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"
],
"version": "==2022.1"
},
"requests": {
"hashes": [
"sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983",
"sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"
],
"markers": "python_version >= '3.7' and python_version < '4'",
"version": "==2.28.1"
},
"responses": {
"hashes": [
"sha256:2dcc863ba63963c0c3d9ee3fa9507cbe36b7d7b0fccb4f0bdfd9e96c539b1487",
"sha256:b82502eb5f09a0289d8e209e7bad71ef3978334f56d09b444253d5ad67bf5253"
],
"markers": "python_version >= '3.7'",
"version": "==0.21.0"
},
"s3transfer": {
"hashes": [
"sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd",
"sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"
],
"markers": "python_version >= '3.7'",
"version": "==0.6.0"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"tomli": {
"hashes": [
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.1"
},
"urllib3": {
"hashes": [
"sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc",
"sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'",
"version": "==1.26.11"
},
"werkzeug": {
"hashes": [
"sha256:4d7013ef96fd197d1cdeb03e066c6c5a491ccb44758a5b2b91137319383e5a5a",
"sha256:7e1db6a5ba6b9a8be061e47e900456355b8714c0f238b0313f53afce1a55a79a"
],
"markers": "python_version >= '3.7'",
"version": "==2.2.1"
},
"xmltodict": {
"hashes": [
"sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56",
"sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"
],
"markers": "python_version >= '3.4'",
"version": "==0.13.0"
}
}
}

208
ecs/crm-datafetch/README.md Normal file
View File

@ -0,0 +1,208 @@
# CRMデータ連携 データ取得処理 ECSタスク
## 前提事項
### ツールのバージョン
- Python 3.8.x
- PipEnv(Pythonの依存関係管理用モジュール)
### 開発環境
- Visual Studio Code
## 開発環境構築
※下記の操作は基本的にVSCode上で行います。
- [ファイル]-[フォルダーを開く]から、当フォルダを選択して開く
- [Wiki | Pythonの環境構築](https://nds-tyo.backlog.com/alias/wiki/1874930)にて、pyenvの導入まで完了させる
- **pyenvの導入はマストではないが、Pythonのバージョンが前提のバージョンと同一であることを確認して開発を進めてください**
- **確認しながら開発するのは煩わしいため、導入を強く推奨します。**
- ローカルのPythonでPipEnvをインストールする
```sh
pip install pipenv
```
- pipenvの仮想環境と依存パッケージをインストール。このとき、初回実行にはpythonの仮想環境のパスがターミナルに表示されるため、控えておく
```sh
# 開発用パッケージも含めてインストール
pipenv install --dev
```
- VSCodeのコマンドパレットを[表示]-[コマンドパレット]から開き、`Python: Select interpreter`を選択して実行する
- Pythonの実行環境を聞かれるため、先に控えたパスと一致するものを選択する
- 出てこない場合、一度VSCodeを閉じて再度開き直す
- 当フォルダ直下の`.vscode`フォルダ内にある`recommend_settings.json`をコピーし、同フォルダ内に`settings.json`を作成する
## ローカルでの起動方法
- 当フォルダ直下の`.vscode`フォルダ内に`launch.json`を作成する
- 以下のJSONを入力して保存する
```json
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
// エントリーポイントのファイルに変更すること
"program": "<エントリーポイントになるファイル>",
"console": "integratedTerminal",
"justMyCode": true,
// 環境変数が必要な場合に読み込む環境変数ファイル
"envFile": "${workspaceFolder}/.env",
}
]
}
```
- 環境変数が必要な場合、直接設定するか、上記JSONの`"envFile"`に設定されたパスに`.env`ファイルを作成し、環境変数を入力する
- キーボードの「F5」キーを押して起動する
- デバッグモードで実行されるため、適当なところにブレークポイントを置いてデバッグすることができる
## ファイル/フォルダ構成
`[〇〇処理]モジュール`と記載されているファイルは、設計書に記載のシートと一致したPythonファイルです
```text
.
├── Dockerfile -- Dockerイメージを作成するためのファイル
├── Pipfile -- Pipenv(Pythonの仮想環境管理モジュール)で、依存関係を管理するためのファイル
├── Pipfile.lock -- Pipenvでインストールされた依存関係のバージョン固定ファイル
├── README.md -- README
├── main.py -- CRMデータ取得処理のエントリーポイント
├── src/ -- プロダクトコード置き場
│ ├── aws/ -- AWSのリソース操作関連のモジュール置き場
│ ├── backup_crm_csv_data_process.py -- [CSVバックアップ処理]モジュール
│ ├── backup_crm_data_process.py -- [CRM電文データバックアップ処理]モジュール
│ ├── check_object_info_process.py -- [オブジェクト情報形式チェック処理]モジュール
│ ├── config/ -- 設定ファイル関連のモジュール置き場
│ ├── controller.py -- [コントロール処理]モジュール
│ ├── convert_crm_csv_data_process.py -- [CSV変換処理]モジュール
│ ├── converter/ -- CSV変換処理で実際に変換を行うモジュール置き場
│ ├── copy_crm_csv_data_process.py -- [CSVアップロード処理]モジュール
│ ├── error/ -- 処理エラー発生時カスタム例外モジュール置き場
│ ├── fetch_crm_data_process.py -- [CRMデータ取得処理]モジュール
│ ├── parser/ -- JSON設定ファイル読み込み処理モジュール置き場
│ ├── prepare_data_fetch_process.py -- [データ取得準備処理]モジュール
│ ├── salesforce/ -- SalesforceのAPIリクエストモジュール置き場
│ ├── set_datetime_period_process.py -- [データ取得期間設定処理]モジュール
│ ├── system_var/ -- 環境変数と定数ファイル置き場
│ ├── upload_last_fetch_datetime_process.py -- [前回取得日時ファイル更新処理]モジュール
│ ├── upload_result_data_process.py -- [取得処理実施結果アップロード処理]モジュール
│ └── util/ -- ユーティリティモジュール置き場
│ ├── counter_object.py -- リトライ判定のためのカウントアップクラス
│ ├── dict_checker.py -- 辞書型値オブジェクトの設定値チェック用クラス
│ ├── execute_datetime.py -- 取得処理開始年月日時分秒の管理クラス
│ └── logger.py -- ログ管理クラス
└── tests/ -- テストコード置き場
├── test_utils/ -- テストコードで共通的に使用できる関数群
├── aws/ -- AWS操作モジュールのテスト
├── ... -- src配下のモジュール構成と同じ階層にテストコードを追加していく
├── conftest.py -- pytestのフィクスチャやフックを管理するファイル
└── docstring_parser.py -- pytest-htmlのレポート出力用のヘルパー
```
## 単体テストについて
### 前提
- Pytestを使用する
- <https://pypi.org/project/pytest/>
- カバレッジも取得したいため、pytest-covも使う
- <https://pypi.org/project/pytest-cov/>
- レポートを出力するため、pytest-htmlを使う
- <https://pypi.org/project/pytest-html/>
- S3をモック化したいため、motoをつかう
- <https://www.learnaws.org/2020/12/01/test-aws-code/>
- CRMはテスト用の環境を使いたいため、newdwh_opeのアドレスでDeveloper組織を登録する
### テスト環境構築
- Pipenvの仮想環境下で、以下のコマンドを実行する
```sh
pipenv install --dev
```
- `.env.example`をコピーし、同じ階層に`.env`を作成する
- `.env`の以下に示す環境変数の値をDeveloper組織のものに書き換える
- CRM_AUTH_DOMAIN
- CRM_USER_NAME
- CRM_USER_PASSWORD
- CRM_USER_SECURITY_TOKEN
- 以下のコマンドを実行して単体テストを起動する
```sh
pipenv run test:cov
```
#### 一気通貫テストを実行する場合の設定
- `.env`の以下に示す環境変数の値をメルク様提供のFullSandboxのものに書き換える
- CRM_AUTH_DOMAIN
- CRM_USER_NAME
- CRM_USER_PASSWORD
- CRM_USER_SECURITY_TOKEN
- 以下のコマンドを実行して単体テストを起動する
```sh
pipenv run test:walk-through
```
#### 拡張機能Python Test Explorer UIを導入する場合
- VSCodeの拡張機能メニューから、「Python Test Explorer for Visual Studio Code」をインストール
- コマンドパレットから「Python Configure Tests」を選択、「Pytest」を選択
- テストメニュー(フラスコのマーク)から、テストを実行することができるようになる
#### 各コマンドの説明
- `pipenv run test`
- pytestを使用してテストを実行する
- `tests`フォルダに配置されているテストモジュールを対象に、単体テストを実行する
- `pipenv run test:cov`
- pytestのテスト終了時にカバレッジを収集する
- 標準出力とカバレッジファイル(`.coverage`)に出力される
- `pipenv run test:report`
- pytestのテスト終了時にテスト結果をHTMLで出力する
- `.report/unit_test/test_result.html`が出力される
- `test:walk-through`
- 一気通貫テストが実行される
- 上記のテスト以外はスキップされる
- `.report/walk_through/test_result.html`に結果のHTMLが出力される
## 単体テストの追加方法
- `tests`フォルダ内に、`src`フォルダの構成と同じようにフォルダを作り、`test_<テスト対象のモジュール名.py>`というファイルを作成する
- 例:`src/aws/s3.py`をテストする場合は`tests/aws/test_s3.py`というファイル名にする
- テスト関数はクラスにまとめて、テストスイートとする
- テスト関数にはドキュメントコメントを付け、テスト観点・準備作業・期待値を記載すること
- 上記が出力されるスニペットを用意してある。
- `""""""`と入力し、「Test docstring (User Snippets)」を選択し、ドキュメントコメントを挿入できる
```python
from src.aws.s3 import S3Resource
class TestS3Resource:
def test_get_object(self, s3_test, s3_):
"""
Cases:
S3からオブジェクトが取得できるか
Arranges:
- S3をモック化する
- 期待値となるファイルを配置する
Expects:
オブジェクトが取得でき、期待値と正しいこと
"""
# more code...
```

View File

@ -0,0 +1,8 @@
from src.controller import controller
"""CRMデータ取得処理のエントリーポイント"""
if __name__ == '__main__':
try:
exit(controller())
except Exception:
exit(0)

View File

@ -0,0 +1,3 @@
[pytest]
log_format = %(levelname)s %(asctime)s %(message)s
log_date_format = %Y-%m-%d %H:%M:%S

View File

View File

View File

@ -0,0 +1,101 @@
import json
import boto3
from src.system_var.constants import (AWS_RESOURCE_S3, S3_CHAR_CODE,
S3_RESPONSE_BODY)
from src.system_var.environments import (CRM_BACKUP_BUCKET, CRM_CONFIG_BUCKET,
CRM_IMPORT_DATA_BACKUP_FOLDER,
CRM_IMPORT_DATA_FOLDER,
IMPORT_DATA_BUCKET,
LAST_FETCH_DATE_FOLDER,
OBJECT_INFO_FILENAME,
OBJECT_INFO_FOLDER,
PROCESS_RESULT_FOLDER,
RESPONSE_JSON_BACKUP_FOLDER)
class S3Resource:
def __init__(self, bucket_name: str) -> None:
self.__s3_resource = boto3.resource(AWS_RESOURCE_S3)
self.__s3_bucket = self.__s3_resource.Bucket(bucket_name)
def get_object(self, object_key: str) -> str:
response = self.__s3_bucket.Object(object_key).get()
body = response[S3_RESPONSE_BODY].read()
return body.decode(S3_CHAR_CODE)
def put_object(self, object_key: str, data: str) -> None:
s3_object = self.__s3_bucket.Object(object_key)
s3_object.put(Body=data.encode(S3_CHAR_CODE), ContentEncoding=S3_CHAR_CODE)
return
def copy(self, src_bucket: str, src_key: str, dest_bucket: str, dest_key: str) -> None:
copy_source = {'Bucket': src_bucket, 'Key': src_key}
self.__s3_resource.meta.client.copy(copy_source, dest_bucket, dest_key)
return
class ConfigBucket:
__s3_resource: S3Resource = None
def __init__(self) -> None:
self.__s3_resource = S3Resource(CRM_CONFIG_BUCKET)
def __str__(self) -> str:
return CRM_CONFIG_BUCKET
def get_object_info_file(self) -> str:
return self.__s3_resource.get_object(f'{OBJECT_INFO_FOLDER}/{OBJECT_INFO_FILENAME}')
def get_last_fetch_datetime_file(self, file_path: str) -> str:
return self.__s3_resource.get_object(f'{LAST_FETCH_DATE_FOLDER}/{file_path}')
def put_last_fetch_datetime_file(self, file_path: str, data: str) -> None:
self.__s3_resource.put_object(
f'{LAST_FETCH_DATE_FOLDER}/{file_path}', data)
return
class DataBucket:
__s3_resource: S3Resource = None
def __init__(self) -> None:
self.__s3_resource = S3Resource(IMPORT_DATA_BUCKET)
def __str__(self) -> str:
return IMPORT_DATA_BUCKET
def put_csv(self, file_path: str, data: str) -> None:
object_key = f'{CRM_IMPORT_DATA_FOLDER}/{file_path}'
self.__s3_resource.put_object(object_key, data)
return
def put_csv_from(self, src_bucket: str, src_key: str):
dest_filename = src_key.split('/')[-1]
self.__s3_resource.copy(src_bucket, src_key, str(self), f'{CRM_IMPORT_DATA_FOLDER}/{dest_filename}')
return
class BackupBucket:
__s3_resource: S3Resource = None
def __init__(self) -> None:
self.__s3_resource = S3Resource(CRM_BACKUP_BUCKET)
def __str__(self) -> str:
return CRM_BACKUP_BUCKET
def put_response_json(self, file_path: str, data: dict) -> None:
object_key = f'{RESPONSE_JSON_BACKUP_FOLDER}/{file_path}'
self.__s3_resource.put_object(object_key, json.dumps(data, ensure_ascii=False))
return
def put_csv(self, file_path: str, data: str) -> None:
object_key = f'{CRM_IMPORT_DATA_BACKUP_FOLDER}/{file_path}'
self.__s3_resource.put_object(object_key, data)
return
def put_result_json(self, file_path: str, data: dict) -> None:
object_key = f'{PROCESS_RESULT_FOLDER}/{file_path}'
self.__s3_resource.put_object(object_key, json.dumps(data, ensure_ascii=False))
return

View File

@ -0,0 +1,47 @@
from src.aws.s3 import BackupBucket
from src.config.objects import TargetObject
from src.error.exceptions import FileUploadException
from src.system_var.constants import CSVBK_JP_NAME
from src.util.execute_datetime import ExecuteDateTime
from src.util.logger import logger_instance as logger
def backup_crm_csv_data_process(target_object: TargetObject, execute_datetime: ExecuteDateTime, csv_string: str):
"""CSVバックアップ処理
Args:
target_object (TargetObject): 取得対象オブジェクト情報インスタンス
execute_datetime (ExecuteDateTime): 実行日時取得インスタンス
csv_string (str): csvデータ
Raises:
FileUploadException: S3のファイルアップロード失敗
"""
# ① CSVバックアップ処理の開始ログを出力する
target_object_name = target_object.object_name
upload_file_name = target_object.upload_file_name
logger.info(
f'I-CSVBK-01 [{target_object_name}] のCSVデータのバックアップ処理を開始します ファイル名:[{upload_file_name}.csv]')
try:
# ② CRMバックアップ保管用バケットに、変換後のCSVデータのバックアップを保管する
backup_bucket = BackupBucket()
backup_bucket.put_csv(
f'{execute_datetime.to_path()}/{upload_file_name}.csv', csv_string)
logger.debug(
f'D-CSVBK-02 [{target_object_name}] のCSVデータバックアップ 正常終了')
except Exception as e:
raise FileUploadException(
'E-CSVBK-01',
CSVBK_JP_NAME, f'[{target_object_name}] CSVデータのバックアップに失敗しました ファイル名:[{upload_file_name}.csv] エラー内容:[{e}]')
# ③ CSVバックアップ処理の終了ログを出力する
logger.info(
f'I-CSVBK-03 [{target_object_name}] のCSVデータのバックアップ処理を終了します')
# ④ 次の処理へ移行する
return

View File

@ -0,0 +1,45 @@
from src.aws.s3 import BackupBucket
from src.config.objects import TargetObject
from src.error.exceptions import FileUploadException
from src.system_var.constants import RESBK_JP_NAME
from src.util.execute_datetime import ExecuteDateTime
from src.util.logger import logger_instance as logger
def backup_crm_data_process(target_object: TargetObject, sf_object_dict: dict, execute_datetime: ExecuteDateTime):
"""CRM電文データバックアップ処理
Args:
target_object (TargetObject): 取得対象オブジェクト名
sf_object_dict (dict): Salesforceオブジェクトデータ
execute_datetime (ExecuteDateTime): 実行日時取得インスタンス
Raises:
FileUploadException: S3のファイルアップロード失敗
"""
object_name = target_object.object_name
# ① CRM電文データバックアップ処理の開始ログを出力する
logger.info(f'I-RESBK-01 [{object_name}] のCRM電文データバックアップ処理を開始します')
try:
# ② CRMバックアップ保管用バケットに、CRMから取得したJSONの電文データのバックアップを保管する
file_name = f'{execute_datetime.to_path()}/{target_object.upload_file_name}.json'
backup_bucket = BackupBucket()
backup_bucket.put_response_json(file_name, sf_object_dict)
logger.debug(f'D-RESBK-02 [{object_name}] のJSONデータバックアップ 正常終了')
except Exception as e:
raise FileUploadException(
'E-RESBK-01',
RESBK_JP_NAME,
f'[{object_name}] 電文データのバックアップに失敗しました ファイル名:[{target_object.upload_file_name}.json] エラー内容:[{e}]'
)
# ③ CRM電文データバックアップ処理の終了ログを出力する
logger.info(f'I-RESBK-03 [{object_name}] のCRM電文データバックアップ処理を終了します')
# ④ 次の処理へ移行する
return

View File

@ -0,0 +1,37 @@
from src.config.objects import TargetObject
from src.error.exceptions import InvalidConfigException
from src.system_var.constants import CHK_JP_NAME
from src.util.execute_datetime import ExecuteDateTime
from src.util.logger import logger_instance as logger
def check_object_info_process(object_info: dict, execute_datetime: ExecuteDateTime):
"""オブジェクト情報形式チェック処理
Args:
object_info (dict): 取得対象オブジェクト情報
execute_datetime (ExecuteDateTime): 実行日時取得インスタンス
Raises:
InvalidConfigException: オブジェクト情報定義が不正だった場合
Returns:
target_object: 取得対象オブジェクト情報インスタンス
"""
# ① オブジェクト情報形式チェック処理開始ログを出力する
logger.info('I-CHK-01 オブジェクト情報形式チェック処理を開始します')
try:
# ② オブジェクト情報形式チェック
target_object = TargetObject(object_info, execute_datetime)
except Exception as e:
raise InvalidConfigException(
'E-CHK-01', CHK_JP_NAME, f'オブジェクト情報形式チェック処理が失敗しました エラー内容:[{e}]')
# ③ チェック処理終了ログを出力する
logger.info('I-CHK-02 オブジェクト情報形式チェック処理を終了します')
# ④ 次の処理へ移行する
return target_object

View File

View File

@ -0,0 +1,147 @@
from src.system_var.constants import (COLUMNS_KEY, COLUMNS_TYPE,
DATE_PATTERN_EXPECTED_YYYYMMDDTHHMMSSTZ,
DATE_PATTERN_YYYYMMDDTHHMMSSTZ,
DATETIME_COLUMN_DEFAULT_VALUE,
DATETIME_COLUMN_KEY,
DATETIME_COLUMN_TYPE, IS_SKIP_KEY,
IS_SKIP_TYPE,
IS_UPDATE_LAST_FETCH_DATETIME_KEY,
IS_UPDATE_LAST_FETCH_DATETIME_TYPE,
LAST_FETCH_DATETIME_FILE_NAME_KEY,
LAST_FETCH_DATETIME_FILE_NAME_TYPE,
LAST_FETCH_DATETIME_FROM_KEY,
LAST_FETCH_DATETIME_FROM_TYPE,
LAST_FETCH_DATETIME_TO_KEY,
LAST_FETCH_DATETIME_TO_TYPE,
OBJECT_NAME_KEY, OBJECT_NAME_TYPE,
OBJECTS_KEY, OBJECTS_TYPE,
UPLOAD_FILE_NAME_KEY,
UPLOAD_FILE_NAME_TYPE)
from src.util.dict_checker import DictChecker
from src.util.execute_datetime import ExecuteDateTime
class FetchTargetObjects():
def __init__(self, object_info_file_dict) -> None:
self.__objects = object_info_file_dict
self.__dict_checker = DictChecker(self.__objects)
self.__validate()
self.__i = 0
def __iter__(self):
return self
def __next__(self):
if self.__i == len(self.__objects[OBJECTS_KEY]):
raise StopIteration()
value = self.__objects[OBJECTS_KEY][self.__i]
self.__i += 1
return value
def __validate(self) -> None:
self.__dict_checker.assert_key_exist(OBJECTS_KEY)
self.__dict_checker.assert_data_type(OBJECTS_KEY, OBJECTS_TYPE)
class TargetObject():
def __init__(self, object_info, execute_datetime: ExecuteDateTime) -> None:
self.__dict_checker = DictChecker(object_info)
self.__object_info = object_info
self.__execute_datetime = execute_datetime
self.__validate()
def __validate(self) -> None:
self.__validate_required_properties()
self.__validate_optional_properties()
return
def __validate_required_properties(self) -> None:
self.__dict_checker.assert_key_exist(OBJECT_NAME_KEY)
self.__dict_checker.assert_data_type(OBJECT_NAME_KEY, OBJECT_NAME_TYPE)
self.__dict_checker.assert_key_exist(COLUMNS_KEY)
self.__dict_checker.assert_data_type(COLUMNS_KEY, COLUMNS_TYPE)
self.__dict_checker.assert_list_empty(COLUMNS_KEY)
return
def __validate_optional_properties(self) -> None:
if self.__dict_checker.check_key_exist(IS_SKIP_KEY):
self.__dict_checker.assert_data_type(IS_SKIP_KEY, IS_SKIP_TYPE)
if self.__dict_checker.check_key_exist(IS_UPDATE_LAST_FETCH_DATETIME_KEY):
self.__dict_checker.assert_data_type(IS_UPDATE_LAST_FETCH_DATETIME_KEY, IS_UPDATE_LAST_FETCH_DATETIME_TYPE)
if self.__dict_checker.check_key_exist(LAST_FETCH_DATETIME_FILE_NAME_KEY):
self.__dict_checker.assert_data_type(LAST_FETCH_DATETIME_FILE_NAME_KEY, LAST_FETCH_DATETIME_FILE_NAME_TYPE)
if self.__dict_checker.check_key_exist(UPLOAD_FILE_NAME_KEY):
self.__dict_checker.assert_data_type(UPLOAD_FILE_NAME_KEY, UPLOAD_FILE_NAME_TYPE)
if self.__dict_checker.check_key_exist(DATETIME_COLUMN_KEY):
self.__dict_checker.assert_data_type(DATETIME_COLUMN_KEY, DATETIME_COLUMN_TYPE)
return
@property
def object_name(self) -> str:
return self.__object_info[OBJECT_NAME_KEY]
@property
def columns(self) -> list:
return self.__object_info[COLUMNS_KEY]
@property
def is_skip(self) -> bool:
return self.__object_info[IS_SKIP_KEY] if self.__dict_checker.check_key_exist(IS_SKIP_KEY) else False
@property
def is_update_last_fetch_datetime(self) -> bool:
if self.__dict_checker.check_key_exist(IS_UPDATE_LAST_FETCH_DATETIME_KEY):
return self.__object_info[IS_UPDATE_LAST_FETCH_DATETIME_KEY]
return True
@property
def last_fetch_datetime_file_name(self) -> str:
if self.__dict_checker.check_key_exist(LAST_FETCH_DATETIME_FILE_NAME_KEY):
return self.__object_info[LAST_FETCH_DATETIME_FILE_NAME_KEY]
return f'{self.__object_info[OBJECT_NAME_KEY]}.json'
@property
def upload_file_name(self) -> str:
if self.__dict_checker.check_key_exist(UPLOAD_FILE_NAME_KEY):
return self.__object_info[UPLOAD_FILE_NAME_KEY].format(execute_datetime=self.__execute_datetime.format_date())
return f'CRM_{self.__object_info[OBJECT_NAME_KEY]}_{self.__execute_datetime.format_date()}'
@property
def datetime_column(self) -> str:
return self.__object_info[DATETIME_COLUMN_KEY] if self.__dict_checker.check_key_exist(DATETIME_COLUMN_KEY) else DATETIME_COLUMN_DEFAULT_VALUE
class LastFetchDatetime():
def __init__(self, last_fetch_datetime_file_dict, execute_datetime) -> None:
self.__dict_checker = DictChecker(last_fetch_datetime_file_dict)
self.__execute_datetime = execute_datetime
self.__last_fetch_datetime_file_dict = last_fetch_datetime_file_dict
self.__validate()
def __validate(self) -> None:
self.__dict_checker.assert_key_exist(LAST_FETCH_DATETIME_FROM_KEY)
self.__dict_checker.assert_data_type(LAST_FETCH_DATETIME_FROM_KEY, LAST_FETCH_DATETIME_FROM_TYPE)
self.__dict_checker.assert_match_pattern(LAST_FETCH_DATETIME_FROM_KEY, DATE_PATTERN_YYYYMMDDTHHMMSSTZ,
DATE_PATTERN_EXPECTED_YYYYMMDDTHHMMSSTZ)
if self.__dict_checker.check_key_exist(LAST_FETCH_DATETIME_TO_KEY):
self.__dict_checker.assert_data_type(LAST_FETCH_DATETIME_TO_KEY, LAST_FETCH_DATETIME_TO_TYPE)
self.__dict_checker.assert_match_pattern(LAST_FETCH_DATETIME_TO_KEY, DATE_PATTERN_YYYYMMDDTHHMMSSTZ,
DATE_PATTERN_EXPECTED_YYYYMMDDTHHMMSSTZ)
return
@property
def last_fetch_datetime_from(self) -> str:
return self.__last_fetch_datetime_file_dict[LAST_FETCH_DATETIME_FROM_KEY]
@property
def last_fetch_datetime_to(self) -> str:
if self.__dict_checker.check_key_exist(LAST_FETCH_DATETIME_TO_KEY):
return self.__last_fetch_datetime_file_dict[LAST_FETCH_DATETIME_TO_KEY]
return str(self.__execute_datetime)

View File

@ -0,0 +1,202 @@
import gc
from src.backup_crm_csv_data_process import backup_crm_csv_data_process
from src.backup_crm_data_process import backup_crm_data_process
from src.check_object_info_process import check_object_info_process
from src.config.objects import FetchTargetObjects
from src.convert_crm_csv_data_process import convert_crm_csv_data_process
from src.copy_crm_csv_data_process import copy_crm_csv_data_process
from src.error.exceptions import MeDaCaCRMDataFetchException
from src.fetch_crm_data_process import fetch_crm_data_process
from src.prepare_data_fetch_process import prepare_data_fetch_process
from src.set_datetime_period_process import set_datetime_period_process
from src.system_var.constants import OBJECT_NAME_KEY
from src.upload_last_fetch_datetime_process import \
upload_last_fetch_datetime_process
from src.upload_result_data_process import upload_result_data_process
from src.util.execute_datetime import ExecuteDateTime
from src.util.logger import logger_instance as logger
def controller() -> None:
"""コントロール処理"""
try:
# ① CRMデータ取得処理開始ログを出力する
logger.info('I-CTRL-01 CRMデータ取得処理を開始します')
# ② データ取得準備処理を呼び出す
logger.info('I-CTRL-02 データ取得準備処理呼び出し')
fetch_target_objects, execute_datetime, process_result = prepare_data_fetch_process()
# ③ object_infoのobjectsキーの値の件数分ループする
logger.info('I-CTRL-03 取得対象オブジェクトのループ処理開始')
process_result = _fetch_crm_data(fetch_target_objects, execute_datetime, process_result)
# ④ すべてのオブジェクトの処理が完了したことと、オブジェクト毎の処理結果をログに出力する
logger.info(f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{process_result}]')
# 最終結果が0件(1件も処理されていない)の場合、ログ出力して処理を終了する
if len(process_result.keys()) == 0:
logger.info('I-CTRL-21 処理対象のデータが存在しませんでした')
return 0
# ⑤ 取得処理実施結果アップロード処理を呼び出す
logger.info('I-CTRL-18 CRM_取得処理実施結果ファイルアップロード処理開始')
upload_result_data_process(process_result, execute_datetime)
# ⑥ 最終結果をチェックし、チェック結果をログに出力
_check_process_result(process_result)
return 0
except MeDaCaCRMDataFetchException as e:
logger.error(f'E-ERR-01 [{e.func_name}]でエラーが発生したため、処理を終了します')
logger.exception(f'{e.error_id} {e}')
raise e
except Exception as e:
logger.exception(f'E-ERR-02 予期せぬエラーが発生したため、処理を終了します エラー内容: [{e}]')
raise e
finally:
# ⑦ CRMデータ取得処理終了ログを出力する
logger.info('I-CTRL-20 CRMデータ取得処理を終了します')
def _fetch_crm_data(fetch_target_objects: FetchTargetObjects, execute_datetime: ExecuteDateTime, process_result: dict):
"""取得対象オブジェクト情報をループし、1オブジェクトごとのデータを取得する
Args:
fetch_target_objects (FetchTargetObjects): CRMオブジェクト情報インスタンス
execute_datetime (ExecuteDateTime): 実行日時取得インスタンス
process_result (dict): 取得処理実行結果辞書オブジェクト
Returns:
process_result: 取得処理実行結果辞書オブジェクト
"""
for object_info in fetch_target_objects:
try:
# 1処理結果出力用のマップの初期化
process_result[object_info.get(OBJECT_NAME_KEY)] = 'fail'
_fetch_crm_data_per_object(object_info, execute_datetime)
# 16処理結果出力用のマップの更新
process_result[object_info.get(OBJECT_NAME_KEY)] = 'success'
except MeDaCaCRMDataFetchException as e:
logger.info(f'{e.error_id} {e}')
logger.info(
f'I-ERR-03 [{object_info.get(OBJECT_NAME_KEY)}] の[{e.func_name}]でエラーが発生しました 次のオブジェクトの処理に移行します', exc_info=True)
continue
except Exception as e:
logger.info(
f'I-ERR-04 [{object_info.get(OBJECT_NAME_KEY)}] の処理中に予期せぬエラーが発生しました 次のオブジェクトの処理に移行します エラー内容: [{e}]', exc_info=True)
continue
return process_result
def _fetch_crm_data_per_object(object_info: dict, execute_datetime: ExecuteDateTime) -> None:
"""オブジェクトごとにCRMのデータを取得し、取込フォルダにアップロードする
Args:
object_info (dict): 取得対象オブジェクト情報
execute_datetime (ExecuteDateTime): 実行日時取得インスタンス
"""
# 2. 対象オブジェクト情報の内容をログに出力する
logger.debug(f'D-CTRL-04 対象のオブジェクト情報を出力します オブジェクト情報:[{object_info}]')
# 3. オブジェクト情報形式チェック処理を呼び出す
logger.info('I-CTRL-05 オブジェクト情報形式チェック処理呼び出し')
target_object = check_object_info_process(object_info, execute_datetime)
target_object_name = target_object.object_name
# 4. 処理対象のオブジェクト名をログ出力する
logger.info(
f'I-CTRL-06 [{target_object_name}]のデータ取得を開始します')
# 5. オブジェクト情報.is_skipがTrueの場合、次のオブジェクトの処理に移行する
if target_object.is_skip is True:
logger.info(
f'I-CTRL-07 [{target_object_name}]のデータ取得処理をスキップします')
return
# 6. データ取得期間設定処理を呼び出す
logger.info(
f'I-CTRL-08 [{target_object_name}]のデータ取得期間設定処理呼び出し')
last_fetch_datetime = set_datetime_period_process(target_object, execute_datetime)
# 7. CRMデータ取得処理を呼び出す
logger.info(
f'I-CTRL-09 [{target_object_name}]のデータ取得処理呼び出し')
crm_data_response = fetch_crm_data_process(target_object, last_fetch_datetime)
# 取得件数が0件の場合、次のオブジェクトの処理に移行する
if len(crm_data_response) == 0:
logger.info(
f'I-CTRL-22 [{target_object_name}]のレコード件数が0件のため、ファイルアップロードをスキップします')
return
# 8. 出力ファイル名をログ出力する
logger.info(
f'I-CTRL-10 [{target_object_name}] の出力ファイル名は [{target_object.upload_file_name}] となります')
# 9. CRM電文データバックアップ処理を呼び出す
logger.info(
f'I-CTRL-11 [{target_object_name}] CRM電文データバックアップ処理呼び出し')
backup_crm_data_process(target_object, crm_data_response, execute_datetime)
# 10. CSV変換処理を呼び出す
logger.info(
f'I-CTRL-12 [{target_object.object_name}] CSV変換処理呼び出し')
csv_string = convert_crm_csv_data_process(target_object, crm_data_response)
# 11. CSVバックアップ処理を呼び出す
logger.info(
f'I-CTRL-13 [{target_object_name}] CSVデータバックアップ処理呼び出し')
backup_crm_csv_data_process(target_object, execute_datetime, csv_string)
# 12. CSVアップロード処理を呼び出す
logger.info(
f'I-CTRL-14 [{target_object_name}] CSVデータアップロード処理呼び出し')
copy_crm_csv_data_process(target_object, execute_datetime)
# 13. メモリ解放
del crm_data_response
del csv_string
gc.collect()
# 14. 前回取得日時ファイル更新処理を呼びだす
logger.info(
f'I-CTRL-15 [{target_object_name}] 前回取得日時ファイル更新処理呼び出し')
upload_last_fetch_datetime_process(target_object, last_fetch_datetime)
# 15. 1オブジェクトのアップロードが完了した旨をログに出力する
logger.info(f'I-CTRL-16 [{target_object_name}] 処理正常終了')
return
def _check_process_result(process_result: dict) -> None:
"""取得処理結果がすべて成功か、一部失敗しているかを判定し、ログ出力する
Args:
process_result (dict): 取得処理結果辞書オブジェクト
"""
if not all([v == 'success' for v in process_result.values()]):
logger.error('E-CTRL-01 一部のデータ取得に失敗しています 詳細はログをご確認ください')
return
logger.info('I-CTRL-19 すべてのデータの取得に成功しました')
return

View File

@ -0,0 +1,42 @@
from src.config.objects import TargetObject
from src.converter.converter import CSVStringConverter
from src.error.exceptions import DataConvertException
from src.system_var.constants import CONV_JP_NAME
from src.util.logger import logger_instance as logger
def convert_crm_csv_data_process(target_object: TargetObject, crm_data_response: dict):
"""CSV変換処理
Args:
target_object (TargetObject): 取得対象オブジェクト情報インスタンス
crm_data_response (dict): Salesforceオブジェクトデータ
Raises:
DataConvertException: データ変換が失敗した場合
Returns:
csv_string: csvデータ
"""
# ① CSV変換処理の開始ログを出力する
target_object_name = target_object.object_name
logger.info(f'I-CONV-01 [{target_object_name}] のCSV変換処理を開始します')
try:
# ② CSV変換
csv_string_converter = CSVStringConverter(target_object, crm_data_response)
csv_string = csv_string_converter.convert()
logger.debug(f'D-CONV-02 [{target_object_name}] のCSV変換処理 正常終了')
except Exception as e:
raise DataConvertException(
'E-CONV-01', CONV_JP_NAME, f'[{target_object_name}] CSV変換に失敗しました エラー内容:[{e}]')
# ③ CSV変換処理の終了ログを出力する
logger.info(f'I-CONV-03 [{target_object_name}] のCSV変換処理を終了します')
# ④ 次の処理へ移行する
return csv_string

View File

@ -0,0 +1,81 @@
import json
import re
from collections import OrderedDict
from datetime import datetime
from dateutil.tz import gettz
from src.system_var.constants import (CRM_DATETIME_FORMAT, CSV_FALSE_VALUE,
CSV_TRUE_VALUE,
DATE_PATTERN_YYYYMMDDHHMMSSFFF_UTC,
YYYYMMDDHHMMSS)
from src.system_var.environments import CONVERT_TZ
class ConvertStrategyFactory:
def __init__(self) -> None:
self.__none_value_convert_strategy = NoneValueConvertStrategy()
self.__boolean_convert_strategy = BooleanConvertStrategy()
self.__datetime_convert_strategy = DatetimeConvertStrategy()
self.__int_convert_strategy = IntConvertStrategy()
self.__string_convert_strategy = StringConvertStrategy()
self.__dict_convert_strategy = DictConvertStrategy()
def create(self, value):
if value is None:
convert_strategy = self.__none_value_convert_strategy
elif type(value) == bool:
convert_strategy = self.__boolean_convert_strategy
elif type(value) == str and re.fullmatch(DATE_PATTERN_YYYYMMDDHHMMSSFFF_UTC, value):
convert_strategy = self.__datetime_convert_strategy
elif type(value) == int:
convert_strategy = self.__int_convert_strategy
elif type(value) == dict or type(value) == OrderedDict:
convert_strategy = self.__dict_convert_strategy
else:
convert_strategy = self.__string_convert_strategy
return convert_strategy
class NoneValueConvertStrategy:
def convert_value(self, convert_value: None) -> str:
"""Noneを''空文字に変換する処理"""
return ''
class BooleanConvertStrategy:
def convert_value(self, convert_value: str) -> bool:
"""booleanを数値に変換する処理"""
return CSV_TRUE_VALUE if convert_value is True else CSV_FALSE_VALUE
class DatetimeConvertStrategy:
def convert_value(self, convert_value: str) -> str:
"""UTCのdatetime文字列をJSTの日時文字列に変換する処理"""
# データ登録処理がJSTとして登録するため、変換処理内で事前にJSTの日時文字列に変換する
return datetime.strptime(convert_value, CRM_DATETIME_FORMAT).astimezone(gettz(CONVERT_TZ)).strftime(YYYYMMDDHHMMSS)
class IntConvertStrategy:
def convert_value(self, convert_value: int):
"""int型を変換せずに返す処理"""
# ConvertStrategyFactoryにて型チェックを行っているため値を変換せずに返す
return convert_value
class StringConvertStrategy:
def convert_value(self, convert_value: str):
"""string型を変換せずに返す処理"""
# ConvertStrategyFactoryにて型チェックを行っているため値を変換せずに返す
return convert_value
class DictConvertStrategy:
def convert_value(self, convert_value: dict):
"""dict型の項目を文字列に変換して返す処理"""
return json.dumps(convert_value, ensure_ascii=False)

View File

@ -0,0 +1,75 @@
import csv
import io
from src.config.objects import TargetObject
from src.converter.convert_strategy import ConvertStrategyFactory
class CSVStringConverter:
def __init__(self, target_object: TargetObject, sf_object_jsons: dict) -> None:
self.__target_object = target_object
self.__sf_object_jsons = sf_object_jsons
self.__convert_strategy_factory = ConvertStrategyFactory()
def convert(self) -> str:
extracted_sf_object_jsons = self.__extract_sf_object_jsons()
csv_data = self.__convert_to_csv(extracted_sf_object_jsons)
csv_string = self.__write_csv_string(csv_data)
return csv_string
def __extract_sf_object_jsons(self) -> list:
try:
extracted_sf_object_jsons = []
for sf_object_json in self.__sf_object_jsons:
extracted_sf_object_jsons.append(
self.__extract_necessary_props_from(sf_object_json))
return extracted_sf_object_jsons
except Exception as e:
raise Exception('必要なjsonのデータ成形に失敗しました', e)
def __extract_necessary_props_from(self, sf_object_json) -> dict:
clone_sf_object = {**sf_object_json}
del clone_sf_object['attributes']
uppercase_key_sf_object = {
k.upper(): v for k, v in clone_sf_object.items()}
return uppercase_key_sf_object
def __convert_to_csv(self, extracted_sf_object_jsons) -> list:
try:
columns = self.__target_object.columns
csv_data = []
for i, json_object in enumerate(extracted_sf_object_jsons, 1):
csv_row = []
for column in columns:
v = json_object[column.upper()]
convert_strategy = self.__convert_strategy_factory.create(v)
converted_value = convert_strategy.convert_value(v)
csv_row.append(converted_value)
csv_data.append(csv_row)
return csv_data
except Exception as e:
raise Exception(
f'CSV変換に失敗しました カラム名:[{column}] 行番号: [{i}] エラー内容:[{e}]')
def __write_csv_string(self, csv_data) -> str:
try:
with io.StringIO(newline='') as string_stream:
writer = csv.writer(string_stream, delimiter=',', lineterminator='\r\n',
doublequote=True, quotechar='"', quoting=csv.QUOTE_ALL, strict=True)
writer.writerow(self.__target_object.columns)
writer.writerows(csv_data)
csv_value = string_stream.getvalue()
return csv_value
except Exception as e:
raise Exception('CSVデータの出力に失敗しました', e)

View File

@ -0,0 +1,46 @@
from src.aws.s3 import BackupBucket, DataBucket
from src.config.objects import TargetObject
from src.error.exceptions import FileUploadException
from src.system_var.constants import UPLD_JP_NAME
from src.system_var.environments import CRM_IMPORT_DATA_BACKUP_FOLDER
from src.util.execute_datetime import ExecuteDateTime
from src.util.logger import logger_instance as logger
def copy_crm_csv_data_process(target_object: TargetObject, execute_datetime: ExecuteDateTime):
"""CSVアップロード処理
Args:
target_object (TargetObject): 取得対象オブジェクト情報インスタンス
execute_datetime (ExecuteDateTime): 実行日時取得インスタンス
Raises:
FileUploadException: S3のファイルアップロード失敗
"""
# ① CSVデータアップロード処理の開始ログを出力する
target_object_name = target_object.object_name
upload_file_name = target_object.upload_file_name
logger.info(
f'I-UPLD-01 [{target_object_name}] のCSVデータアップロード処理を開始します ファイル名:[{upload_file_name}.csv]')
try:
# ② CRMバックアップ保管用バケットに保管した変換後のCSVデータをデータ取込バケットにコピーする
data_bucket = DataBucket()
backup_bucket = BackupBucket()
data_bucket.put_csv_from(str(backup_bucket), f'{CRM_IMPORT_DATA_BACKUP_FOLDER}/{execute_datetime.to_path()}/{upload_file_name}.csv')
logger.debug(
f'D-UPLD-02 [{target_object_name}] のCSVデータアップロード 正常終了')
except Exception as e:
raise FileUploadException(
'E-UPLD-01', UPLD_JP_NAME, f'[{target_object_name}] CSVデータのアップロードに失敗しました ファイル名:[{upload_file_name}.csv] エラー内容:[{e}]')
# ③ CSVデータアップロード処理の終了ログを出力する
logger.info(
f'I-UPLD-03 [{target_object_name}] のCSVデータのアップロード処理を終了します')
# ④ 次の処理へ移行する
return

View File

View File

@ -0,0 +1,35 @@
from abc import ABCMeta
class MeDaCaCRMDataFetchException(Exception, metaclass=ABCMeta):
"""MeDaCaシステム固有のカスタムエラークラス"""
def __init__(self, error_id: str, func_name: str, message: str) -> None:
super().__init__(message)
self.func_name = func_name
self.error_id = error_id
class FileNotFoundException(MeDaCaCRMDataFetchException):
"""S3のファイルが見つからない場合の例外"""
pass
class FileUploadException(MeDaCaCRMDataFetchException):
"""S3のファイルアップロード失敗の例外"""
pass
class InvalidConfigException(MeDaCaCRMDataFetchException):
"""Configのバリデーションチェック失敗の例外"""
pass
class DataConvertException(MeDaCaCRMDataFetchException):
"""データ変換が失敗した場合の例外"""
pass
class SalesforceAPIException(MeDaCaCRMDataFetchException):
"""SalesforceのAPI実行失敗が発生した場合の例外"""
pass

View File

@ -0,0 +1,147 @@
from requests.exceptions import ConnectTimeout, ReadTimeout
from tenacity import retry, stop_after_attempt
from tenacity.wait import wait_exponential
from src.config.objects import LastFetchDatetime, TargetObject
from src.error.exceptions import SalesforceAPIException
from src.salesforce.salesforce_api import SalesforceApiClient
from src.salesforce.soql_builder import SOQLBuilder
from src.system_var.constants import FETCH_JP_NAME
from src.system_var.environments import (
CRM_AUTH_TIMEOUT, CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT,
CRM_FETCH_RECORD_RETRY_INTERVAL, CRM_FETCH_RECORD_RETRY_MAX_INTERVAL,
CRM_FETCH_RECORD_RETRY_MIN_INTERVAL, CRM_FETCH_RECORD_TIMEOUT,
CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT,
CRM_GET_RECORD_COUNT_RETRY_INTERVAL,
CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL,
CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL, CRM_GET_RECORD_COUNT_TIMEOUT)
from src.util.counter_object import CounterObject
from src.util.logger import logger_instance as logger
def fetch_crm_data_process(target_object: TargetObject, last_fetch_datetime: LastFetchDatetime):
"""CRMデータ取得処理
Args:
target_object (TargetObject): 取得対象オブジェクト情報インスタンス
last_fetch_datetime (LastFetchDatetime): データ取得期間設定インスタンス
Raises:
SalesforceAPIException: SalesforceのAPI実行失敗が発生した場合
Returns:
crm_data_response: Salesforceオブジェクトデータ
"""
# ① CRMデータ取得処理開始ログを出力する
logger.info(
f'I-FETCH-01 [{target_object.object_name}] のCRMからのデータ取得処理を開始します')
target_object_name = target_object.object_name
# リトライ回数判定用のカウンタオブジェクトを生成
# @retryデコレータを利用した関数のリトライ処理で、基本データ型だとリトライ回数をカウントすることができないため、オブジェクト化する
count_counter = CounterObject(1)
data_counter = CounterObject(1)
try:
# ② 取得対象オブジェクトの取得期間内のレコード件数を取得する
logger.info(f'I-FETCH-02 [{target_object_name}] の件数取得を開始します')
soql_builder = SOQLBuilder(target_object, last_fetch_datetime)
count_soql = soql_builder.create_count_soql()
record_count = fetch_record_count_retry(count_soql, target_object_name, count_counter)
logger.info(f'I-FETCH-03 [{target_object_name}] の件数:[{record_count}]')
except Exception as e:
raise SalesforceAPIException(
'E-FETCH-01', FETCH_JP_NAME, f'[{target_object_name}] の件数取得に失敗しました エラー内容:[{e}]')
try:
# ③ 取得対象オブジェクトのレコードを取得する
logger.info(f'I-FETCH-04 [{target_object_name}] のレコード取得を開始します')
fetch_soql = soql_builder.create_fetch_soql()
crm_data_response = fetch_sf_data_retry(fetch_soql, target_object_name, data_counter)
logger.info(f'I-FETCH-05 [{target_object_name}] のレコード取得が成功しました')
except Exception as e:
raise SalesforceAPIException(
'E-FETCH-02', FETCH_JP_NAME, f'[{target_object_name}] のレコード取得に失敗しました エラー内容:[{e}]')
# ④ CRMデータ取得処理終了ログを出力する
logger.info(f'I-FETCH-06 [{target_object_name}] のCRMからのデータ取得処理を終了します')
# ⑤ 次の処理へ移行する
return crm_data_response
@retry(
wait=wait_exponential(multiplier=CRM_GET_RECORD_COUNT_RETRY_INTERVAL,
min=CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL, max=CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL),
stop=stop_after_attempt(CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT))
def fetch_record_count_retry(soql: str, target_object_name: str, count_counter: CounterObject):
try:
salesforce_api_client = SalesforceApiClient()
return salesforce_api_client.fetch_sf_count(soql)
except ConnectTimeout as e:
# 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
if count_counter.describe() < CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT:
count_counter.increment(1)
logger.warning(f'W-FETCH-01 CRMの接続処理がタイムアウトしため、リトライします[{CRM_AUTH_TIMEOUT}] エラー内容:[{e}]')
raise e
except ReadTimeout as e:
# 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
if count_counter.describe() < CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT:
count_counter.increment(1)
logger.warning(
f'W-FETCH-02 [{target_object_name}] の件数取得処理がタイムアウトしたため、リトライします:[{CRM_GET_RECORD_COUNT_TIMEOUT}] エラー内容:[{e}]')
raise e
except Exception as e:
# 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
if count_counter.describe() < CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT:
count_counter.increment(1)
logger.warning(
f'W-FETCH-03 [{target_object_name}] の件数取得に失敗したため、リトライします エラー内容:[{e}]')
raise e
@retry(
wait=wait_exponential(multiplier=CRM_FETCH_RECORD_RETRY_INTERVAL,
min=CRM_FETCH_RECORD_RETRY_MIN_INTERVAL, max=CRM_FETCH_RECORD_RETRY_MAX_INTERVAL),
stop=stop_after_attempt(CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT))
def fetch_sf_data_retry(soql: str, target_object_name: str, data_counter: CounterObject):
try:
salesforce_api_client = SalesforceApiClient()
return salesforce_api_client.fetch_sf_data(soql)
except ConnectTimeout as e:
# 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
if data_counter.describe() < CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT:
data_counter.increment(1)
logger.warning(f'W-FETCH-04 CRMの接続処理がタイムアウトしため、リトライします[{CRM_AUTH_TIMEOUT}] エラー内容:[{e}]')
raise e
except ReadTimeout as e:
# 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
if data_counter.describe() < CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT:
data_counter.increment(1)
logger.warning(
f'W-FETCH-05 [{target_object_name}] のレコード取得処理がタイムアウトしたため、リトライします:[{CRM_FETCH_RECORD_TIMEOUT}] エラー内容:[{e}]')
raise e
except Exception as e:
# 「リトライします」のメッセージ出力後、リトライせず例外終了になってしまうことを防ぐため、カウンタによる回数の判定を行う
if data_counter.describe() < CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT:
data_counter.increment(1)
logger.warning(
f'W-FETCH-06 [{target_object_name}] のレコード取得に失敗したため、リトライします エラー内容:[{e}]')
raise e

View File

View File

@ -0,0 +1,17 @@
import json
import re
from src.system_var.constants import EXCLUDE_SYMBOL
class JsonParser():
def __init__(self, json_str) -> None:
self.__json_str = json_str
def parse(self) -> dict:
for symbol in EXCLUDE_SYMBOL:
# コメントアウトシンボルを含む部分を置き換える正規表現
replace_comment_regex = rf'\s(?!\"){symbol}[\s\S]*?.*'
self.__json_str = re.sub(replace_comment_regex, '', self.__json_str)
return json.loads(self.__json_str)

View File

@ -0,0 +1,82 @@
from src.aws.s3 import ConfigBucket
from src.config.objects import FetchTargetObjects
from src.error.exceptions import FileNotFoundException, InvalidConfigException
from src.parser.json_parser import JsonParser
from src.system_var.constants import PRE_JP_NAME
from src.system_var.environments import (CRM_CONFIG_BUCKET,
OBJECT_INFO_FILENAME,
OBJECT_INFO_FOLDER)
from src.util.execute_datetime import ExecuteDateTime
from src.util.logger import logger_instance as logger
def prepare_data_fetch_process():
"""データ取得準備処理
Raises:
FileNotFoundException: S3上のファイルが存在しない場合
InvalidConfigException: オブジェクト情報定義が不正だった場合
Returns:
fetch_target_objects : CRMオブジェクト情報インスタンス
execute_datetime : 実行日時取得インスタンス
process_result : 取得処理実行結果辞書オブジェクト
"""
# ① データ取得準備処理の開始ログを出力する
logger.info('I-PRE-01 データ取得準備処理を開始します')
# ② 取得処理開始年月日時分秒を控える
execute_datetime = ExecuteDateTime()
logger.info(f'I-PRE-02 データ取得処理開始日時:{execute_datetime}')
try:
# ③ S3 設定ファイル保管用バケットから、CRM_取得オブジェクト情報ファイルを取得する
object_info_file_s3_path = f's3://{CRM_CONFIG_BUCKET}/{OBJECT_INFO_FOLDER}/{OBJECT_INFO_FILENAME}'
logger.info(
f'I-PRE-03 CRM_取得オブジェクト情報ファイルの取得開始します ファイルパス:[{object_info_file_s3_path}]')
config_bucket = ConfigBucket()
object_info_file_str = config_bucket.get_object_info_file()
logger.debug('D-PRE-04 CRM_取得オブジェクト情報ファイルの取得成功しました')
except Exception as e:
raise FileNotFoundException(
'E-PRE-01', PRE_JP_NAME, f'CRM_取得オブジェクト情報ファイルが存在しません ファイル名:[{OBJECT_INFO_FILENAME}] エラー内容:[{e}]')
try:
# ④ CRM_取得オブジェクト情報ファイルをパースし、メモリ上に展開する
logger.debug('D-PRE-05 CRM_取得オブジェクト情報ファイルをパースします')
json_parser = JsonParser(object_info_file_str)
object_info_file_dict = json_parser.parse()
logger.debug('D-PRE-06 CRM_取得オブジェクト情報ファイルのパースに成功しました')
except Exception as e:
raise InvalidConfigException(
'E-PRE-02', PRE_JP_NAME, f'CRM_取得オブジェクト情報ファイルのパースに失敗しました エラー内容:[{e}]')
# ⑤ メモリ上のCRM_取得オブジェクト情報のキーobjectsの形式チェックを行う
try:
logger.debug('D-PRE-07 CRM_取得オブジェクト情報ファイルの形式チェックを開始します')
fetch_target_objects = FetchTargetObjects(object_info_file_dict)
logger.debug('D-PRE-08 CRM_取得オブジェクト情報ファイルの形式チェック 正常終了')
except Exception as e:
raise InvalidConfigException(
'E-PRE-03', PRE_JP_NAME, f'CRM_取得オブジェクト情報ファイルの形式チェックに失敗しました ファイル名:[{OBJECT_INFO_FILENAME}] エラー内容:[{e}]')
# ⑥ 処理結果出力用のマップを初期化
process_result = {}
# ⑦ データ取得準備処理の終了ログを出力する
logger.info('I-PRE-09 データ取得準備処理を終了します')
# ⑧ 次の処理へ移行する
return (fetch_target_objects, execute_datetime, process_result)

View File

@ -0,0 +1,28 @@
from simple_salesforce import Salesforce
from src.system_var.environments import (CRM_AUTH_DOMAIN, CRM_AUTH_TIMEOUT,
CRM_FETCH_RECORD_TIMEOUT,
CRM_GET_RECORD_COUNT_TIMEOUT,
CRM_USER_NAME, CRM_USER_PASSWORD,
CRM_USER_SECURITY_TOKEN)
class SalesforceApiClient():
def __init__(self) -> None:
self.__sf = Salesforce(username=CRM_USER_NAME, password=CRM_USER_PASSWORD,
security_token=CRM_USER_SECURITY_TOKEN,
domain=CRM_AUTH_DOMAIN
)
def __query(self, soql, include_deleted=True, conn_timeout=100, read_timeout=300):
return self.__sf.query(soql, include_deleted, timeout=(float(conn_timeout), float(read_timeout)))
def __query_all(self, soql, include_deleted=True, conn_timeout=100, read_timeout=300):
return self.__sf.query_all(soql, include_deleted, timeout=(float(conn_timeout), float(read_timeout)))
def fetch_sf_count(self, soql: str):
count_res = self.__query(soql, conn_timeout=CRM_AUTH_TIMEOUT, read_timeout=CRM_GET_RECORD_COUNT_TIMEOUT)
return count_res.get('records')[0].get('expr0')
def fetch_sf_data(self, soql: str):
data_res = self.__query_all(soql, conn_timeout=CRM_AUTH_TIMEOUT, read_timeout=CRM_FETCH_RECORD_TIMEOUT)
return data_res.get('records')

View File

@ -0,0 +1,41 @@
import textwrap
from src.config.objects import LastFetchDatetime, TargetObject
from src.system_var.environments import FETCH_LIMIT_CLAUSE
class SOQLBuilder:
def __init__(self, target_object: TargetObject, last_fetch_datetime: LastFetchDatetime) -> None:
self.__SELECT_SOQL = textwrap.dedent("""\
SELECT {column_or_expression} FROM {object_name}
WHERE {datetime_column} > {last_fetch_datetime_from}
AND {datetime_column} <= {last_fetch_datetime_to}
{limit_clause}
""")
self.__target_object = target_object
self.__last_fetch_datetime = last_fetch_datetime
def create_count_soql(self):
count_soql = self.__SELECT_SOQL.format(
column_or_expression='COUNT(Id)',
object_name=self.__target_object.object_name,
last_fetch_datetime_from=self.__last_fetch_datetime.last_fetch_datetime_from,
last_fetch_datetime_to=self.__last_fetch_datetime.last_fetch_datetime_to,
datetime_column=self.__target_object.datetime_column,
limit_clause=''
)
return count_soql
def create_fetch_soql(self):
columns = ','.join(self.__target_object.columns)
fetch_soql = self.__SELECT_SOQL.format(
column_or_expression=columns,
object_name=self.__target_object.object_name,
last_fetch_datetime_from=self.__last_fetch_datetime.last_fetch_datetime_from,
last_fetch_datetime_to=self.__last_fetch_datetime.last_fetch_datetime_to,
datetime_column=self.__target_object.datetime_column,
limit_clause=FETCH_LIMIT_CLAUSE
)
return fetch_soql

View File

@ -0,0 +1,69 @@
from src.aws.s3 import ConfigBucket
from src.config.objects import LastFetchDatetime, TargetObject
from src.error.exceptions import FileNotFoundException, InvalidConfigException
from src.parser.json_parser import JsonParser
from src.system_var.constants import DATE_JP_NAME
from src.system_var.environments import (CRM_CONFIG_BUCKET,
LAST_FETCH_DATE_FOLDER)
from src.util.execute_datetime import ExecuteDateTime
from src.util.logger import logger_instance as logger
def set_datetime_period_process(target_object: TargetObject, execute_datetime: ExecuteDateTime):
"""データ取得期間設定処理
Args:
target_object (TargetObject): 取得対象オブジェクト情報インスタンス
execute_datetime (ExecuteDateTime): 実行日時取得インスタンス
Raises:
FileNotFoundException: S3上のファイルが存在しない場合
InvalidConfigException: オブジェクト情報定義が不正だった場合
Returns:
last_fetch_datetime: データ取得期間設定インスタンス
"""
# ① データ取得期間設定処理の開始ログを出力する
logger.info(
f'I-DATE-01 [{target_object.object_name}] のデータ取得期間設定処理を開始します')
try:
# ② S3 設定ファイル保管用バケットから、前回取得日時ファイルを取得する
logger.info(
f'I-DATE-02 前回取得日時ファイルの取得開始します ファイルパス:[s3://{CRM_CONFIG_BUCKET}/{LAST_FETCH_DATE_FOLDER}/{target_object.last_fetch_datetime_file_name}]')
s3_config_bucket = ConfigBucket()
last_fetch_datetime_file_str = s3_config_bucket.get_last_fetch_datetime_file(
target_object.last_fetch_datetime_file_name)
logger.info(f'I-DATE-03 前回取得日時ファイルの取得成功しました')
except Exception as e:
raise FileNotFoundException(
'E-DATE-01', DATE_JP_NAME, f'前回取得日時ファイルが存在しません ファイル名:[{target_object.last_fetch_datetime_file_name}] エラー内容:[{e}]')
try:
# ③ 取得した前回取得日時ファイルの形式チェックを行う
# ④ データの取得期間を設定する
logger.debug(f'D-DATE-04 前回取得日時ファイルの形式チェックを開始します')
json_parser = JsonParser(last_fetch_datetime_file_str)
last_fetch_datetime_file_dict = json_parser.parse()
last_fetch_datetime = LastFetchDatetime(last_fetch_datetime_file_dict, execute_datetime)
logger.debug(f'D-DATE-05 前回取得日時ファイルの形式チェック 正常終了')
logger.info(
f'I-DATE-06 取得範囲 From: [{last_fetch_datetime.last_fetch_datetime_from}] To: [{last_fetch_datetime.last_fetch_datetime_to}]')
except Exception as e:
raise InvalidConfigException(
'E-DATE-02', DATE_JP_NAME, f'前回取得日時ファイルの形式チェック処理が失敗しました エラー内容:[{e}]')
# ⑤ データ取得準備処理の終了ログを出力する
logger.info(
f'I-DATE-07 [{target_object.object_name}] のデータ取得期間設定処理を終了します')
# ⑥ 次の処理へ移行する
return last_fetch_datetime

View File

@ -0,0 +1,100 @@
# environments(task settings file)
LOG_LEVEL = 'LOG_LEVEL' # ログ出力レベル。DEBUG, INFO, WARNING, ERRORの4つから指定する
CRM_AUTH_TIMEOUT = 'CRM_AUTH_TIMEOUT' # CRMへの認証処理のタイムアウト秒数
CRM_AUTH_MAX_RETRY_ATTEMPT = 'CRM_AUTH_MAX_RETRY_ATTEMPT' # CRMへの認証処理の最大リトライ試行回数
CRM_AUTH_RETRY_INTERVAL = 'CRM_AUTH_RETRY_INTERVAL' # CRMへの認証処理のリトライ時の初回待ち秒数
CRM_AUTH_RETRY_MIN_INTERVAL = 'CRM_AUTH_RETRY_MIN_INTERVAL' # CRMへの認証処理のリトライ時の最小待ち秒数
CRM_AUTH_RETRY_MAX_INTERVAL = 'CRM_AUTH_RETRY_MAX_INTERVAL' # CRMへの認証処理のリトライ時の最大待ち秒数
CRM_GET_RECORD_COUNT_TIMEOUT = 'CRM_GET_RECORD_COUNT_TIMEOUT' # CRMのレコード件数取得処理のタイムアウト秒数
CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT = 'CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT' # CRMのレコード件数取得処理の最大リトライ試行回数
CRM_GET_RECORD_COUNT_RETRY_INTERVAL = 'CRM_GET_RECORD_COUNT_RETRY_INTERVAL' # CRMのレコード件数取得処理のリトライ時の初回待ち秒数
CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL = 'CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL' # CRMのレコード件数取得処理のリトライ時の最小待ち秒数
CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL = 'CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL' # CRMのレコード件数取得処理のリトライ時の最大待ち秒数
CRM_FETCH_RECORD_TIMEOUT = 'CRM_FETCH_RECORD_TIMEOUT' # CRMのレコード取得処理のタイムアウト秒数
CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT = 'CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT' # CRMのレコード取得処理の最大リトライ試行回数
CRM_FETCH_RECORD_RETRY_INTERVAL = 'CRM_FETCH_RECORD_RETRY_INTERVAL' # CRMのレコード取得処理のリトライ時の初回待ち秒数
CRM_FETCH_RECORD_RETRY_MIN_INTERVAL = 'CRM_FETCH_RECORD_RETRY_MIN_INTERVAL' # CRMのレコード取得処理のリトライ時の最小待ち秒数
CRM_FETCH_RECORD_RETRY_MAX_INTERVAL = 'CRM_FETCH_RECORD_RETRY_MAX_INTERVAL' # CRMのレコード取得処理のリトライ時の最大待ち秒数
CONVERT_TZ = 'CONVERT_TZ' # CRMデータの日時を変換するときのタイムゾーン
# environments(ECS Task Environment)
CRM_AUTH_DOMAIN = 'CRM_AUTH_DOMAIN' # CRMのAPI実行のための認証エンドポイントのドメイン
CRM_USER_NAME = 'CRM_USER_NAME' # CRMのAPI実行用ユーザ名
CRM_USER_PASSWORD = 'CRM_USER_PASSWORD' # CRMのAPI実行用ユーザパスワード
CRM_USER_SECURITY_TOKEN = 'CRM_USER_SECURITY_TOKEN' # CRMのAPI実行用ユーザのセキュリティトークン
CRM_CONFIG_BUCKET = 'CRM_CONFIG_BUCKET' # CRMデータ取得用の設定ファイルを格納するバケット名
CRM_BACKUP_BUCKET = 'CRM_BACKUP_BUCKET' # CRMのバックアップデータを格納するバケット名
IMPORT_DATA_BUCKET = 'IMPORT_DATA_BUCKET' # CRMの取込データを格納するバケット名
OBJECT_INFO_FOLDER = 'OBJECT_INFO_FOLDER' # CRM取得対象オブジェクトの情報を格納するフォルダパス
OBJECT_INFO_FILENAME = 'OBJECT_INFO_FILENAME' # CRM取得対象オブジェクトの情報のファイル名
PROCESS_RESULT_FOLDER = 'PROCESS_RESULT_FOLDER' # CRMデータ取得結果を格納するフォルダパス
PROCESS_RESULT_FILENAME = 'PROCESS_RESULT_FILENAME' # CRMデータ取得結果を格納するファイル名
LAST_FETCH_DATE_FOLDER = 'LAST_FETCH_DATE_FOLDER' # CRMからの最終取得日時ファイルを格納するフォルダパス
CRM_IMPORT_DATA_FOLDER = 'CRM_IMPORT_DATA_FOLDER' # CRMから取得し、取込用に変換したデータを格納するフォルダ
LAST_FETCH_DATE_BACKUP_FOLDER = 'LAST_FETCH_DATE_BACKUP_FOLDER' # CRMからの最終取得日時ファイルのバックアップを格納するフォルダパス
RESPONSE_JSON_BACKUP_FOLDER = 'RESPONSE_JSON_BACKUP_FOLDER' # CRMから取得した生データのバックアップを格納するフォルダパス
CRM_IMPORT_DATA_BACKUP_FOLDER = 'CRM_IMPORT_DATA_BACKUP_FOLDER' # CRMから取得し、取込用に変換したデータのバックアップを格納するフォルダ
# 時刻フォーマット
# .000ZはUTCを表す。ミリ秒までの考慮は不要なので固定で指定
YYYYMMDDTHHMMSSTZ = '%Y-%m-%dT%H:%M:%S.000Z'
CRM_DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.000%z'
YYYYMMDDHHMMSS = '%Y-%m-%d %H:%M:%S'
MILLISEC_FORMAT = '000Z'
# aws
AWS_RESOURCE_S3 = 's3'
S3_RESPONSE_BODY = 'Body'
S3_CHAR_CODE = 'utf-8'
# 正規表現チェック
EXCLUDE_SYMBOL = ['#', '/']
DATE_PATTERN_YYYYMMDDTHHMMSSTZ = r'[12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.000Z'
DATE_PATTERN_EXPECTED_YYYYMMDDTHHMMSSTZ = 'YYYY-MM-DDTHH:MM:SS.000Z'
DATE_PATTERN_YYYYMMDDHHMMSSFFF_UTC = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000\+0000'
# logger
LOG_FORMAT = '[%(levelname)s]\t%(asctime)s\t%(message)s\n'
LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
LOG_LEVEL_INFO = 'INFO'
# 処理名
PROCESS_JP_NAME = 'コントロール処理'
PRE_JP_NAME = 'データ取得準備処理'
CHK_JP_NAME = 'オブジェクト情報形式チェック処理'
DATE_JP_NAME = 'データ取得期間設定処理'
FETCH_JP_NAME = 'CRMデータ取得処理'
RESBK_JP_NAME = 'CRM電文データバックアップ処理'
CONV_JP_NAME = 'CSV変換処理'
CSVBK_JP_NAME = 'CSVバックアップ処理'
UPLD_JP_NAME = 'CSVアップロード処理'
UPD_JP_NAME = '前回取得日時ファイル更新'
END_JP_NAME = '取得処理実施結果アップロード処理'
# CSVチェック
CSV_TRUE_VALUE = 1
CSV_FALSE_VALUE = 0
# オブジェクト変数
OBJECTS_KEY = 'objects'
OBJECTS_TYPE = list
OBJECT_NAME_KEY = 'object_name'
OBJECT_NAME_TYPE = str
COLUMNS_KEY = 'columns'
COLUMNS_TYPE = list
IS_SKIP_KEY = 'is_skip'
IS_SKIP_TYPE = bool
IS_UPDATE_LAST_FETCH_DATETIME_KEY = 'is_update_last_fetch_datetime'
IS_UPDATE_LAST_FETCH_DATETIME_TYPE = bool
LAST_FETCH_DATETIME_FILE_NAME_KEY = 'last_fetch_datetime_file_name'
LAST_FETCH_DATETIME_FILE_NAME_TYPE = str
UPLOAD_FILE_NAME_KEY = 'upload_file_name'
UPLOAD_FILE_NAME_TYPE = str
DATETIME_COLUMN_KEY = 'datetime_column'
DATETIME_COLUMN_TYPE = str
DATETIME_COLUMN_DEFAULT_VALUE = 'SystemModstamp'
LAST_FETCH_DATETIME_TO_KEY = 'last_fetch_datetime_to'
LAST_FETCH_DATETIME_TO_TYPE = str
LAST_FETCH_DATETIME_FROM_KEY = 'last_fetch_datetime_from'
LAST_FETCH_DATETIME_FROM_TYPE = str

View File

@ -0,0 +1,65 @@
import os
import src.system_var.constants as constants
# environments(task settings file)
# ログ出力レベル。DEBUG, INFO, WARNING, ERRORの4つから指定する
LOG_LEVEL = os.environ.get(constants.LOG_LEVEL, constants.LOG_LEVEL_INFO)
# CRMへの認証処理のタイムアウト秒数
CRM_AUTH_TIMEOUT = int(os.environ.get(constants.CRM_AUTH_TIMEOUT, 100))
# CRMのレコード件数取得処理のタイムアウト秒数
CRM_GET_RECORD_COUNT_TIMEOUT = int(os.environ.get(constants.CRM_GET_RECORD_COUNT_TIMEOUT, 300))
# CRMのレコード件数取得処理の最大リトライ試行回数(処理の実施総回数)
CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT = int(os.environ.get(constants.CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT, 4))
# CRMのレコード件数取得処理のリトライ時の初回待ち秒数
CRM_GET_RECORD_COUNT_RETRY_INTERVAL = int(os.environ.get(constants.CRM_GET_RECORD_COUNT_RETRY_INTERVAL, 5))
# CRMのレコード件数取得処理のリトライ時の最小待ち秒数
CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL = int(os.environ.get(constants.CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL, 5))
# CRMのレコード件数取得処理のリトライ時の最大待ち秒数
CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL = int(os.environ.get(constants.CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL, 50))
# CRMのレコード取得処理のタイムアウト秒数
CRM_FETCH_RECORD_TIMEOUT = int(os.environ.get(constants.CRM_FETCH_RECORD_TIMEOUT, 300))
# CRMのレコード取得処理の最大リトライ試行回数(処理の実施総回数)
CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT = int(os.environ.get(constants.CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT, 4))
# CRMのレコード取得処理のリトライ時の初回待ち秒数
CRM_FETCH_RECORD_RETRY_INTERVAL = int(os.environ.get(constants.CRM_FETCH_RECORD_RETRY_INTERVAL, 5))
# CRMのレコード取得処理のリトライ時の最小待ち秒数
CRM_FETCH_RECORD_RETRY_MIN_INTERVAL = int(os.environ.get(constants.CRM_FETCH_RECORD_RETRY_MIN_INTERVAL, 5))
# CRMのレコード取得処理のリトライ時の最大待ち秒数
CRM_FETCH_RECORD_RETRY_MAX_INTERVAL = int(os.environ.get(constants.CRM_FETCH_RECORD_RETRY_MAX_INTERVAL, 50))
# CRMデータの日時を変換するときのタイムゾーン
CONVERT_TZ = os.environ.get(constants.CONVERT_TZ, 'Asia/Tokyo')
# environments(ECS Task Environment)
# CRMのAPI実行のための認証エンドポイントのドメイン
CRM_AUTH_DOMAIN = os.environ[constants.CRM_AUTH_DOMAIN]
# CRMのAPI実行用ユーザ名
CRM_USER_NAME = os.environ[constants.CRM_USER_NAME]
# CRMのAPI実行用ユーザパスワード
CRM_USER_PASSWORD = os.environ[constants.CRM_USER_PASSWORD]
# CRMのAPI実行用ユーザのセキュリティトークン
CRM_USER_SECURITY_TOKEN = os.environ[constants.CRM_USER_SECURITY_TOKEN]
# CRMデータ取得用の設定ファイルを格納するバケット名
CRM_CONFIG_BUCKET = os.environ[constants.CRM_CONFIG_BUCKET]
# CRMのバックアップデータを格納するバケット名
CRM_BACKUP_BUCKET = os.environ[constants.CRM_BACKUP_BUCKET]
# CRMの取込データを格納するバケット名
IMPORT_DATA_BUCKET = os.environ[constants.IMPORT_DATA_BUCKET]
# CRM取得対象オブジェクトの情報を格納するフォルダパス
OBJECT_INFO_FOLDER = os.environ.get(constants.OBJECT_INFO_FOLDER, 'crm/object_info')
# CRM取得対象オブジェクトの情報のファイル名
OBJECT_INFO_FILENAME = os.environ[constants.OBJECT_INFO_FILENAME]
# CRMデータ取得結果を格納するフォルダパス
PROCESS_RESULT_FOLDER = os.environ.get(constants.PROCESS_RESULT_FOLDER, 'data_import')
# CRMデータ取得結果を格納するファイル名
PROCESS_RESULT_FILENAME = os.environ.get(constants.PROCESS_RESULT_FILENAME, 'process_result.json')
# CRMからの最終取得日時ファイルを格納するフォルダパス
LAST_FETCH_DATE_FOLDER = os.environ.get(constants.LAST_FETCH_DATE_FOLDER, 'crm/last_fetch_datetime')
# CRMから取得し、取込用に変換したデータを格納するフォルダ
CRM_IMPORT_DATA_FOLDER = os.environ.get(constants.CRM_IMPORT_DATA_FOLDER, 'crm/target')
# CRMから取得した生データのバックアップを格納するフォルダパス
RESPONSE_JSON_BACKUP_FOLDER = os.environ.get(constants.RESPONSE_JSON_BACKUP_FOLDER, 'response_json')
# CRMから取得し、取込用に変換したデータのバックアップを格納するフォルダ
CRM_IMPORT_DATA_BACKUP_FOLDER = os.environ.get(constants.CRM_IMPORT_DATA_BACKUP_FOLDER, 'data_import')
# 一気通貫テスト用、取得件数を絞るためのSOQL句
FETCH_LIMIT_CLAUSE = os.environ.get('FETCH_LIMIT_CLAUSE', '')

View File

@ -0,0 +1,57 @@
import json
from src.aws.s3 import ConfigBucket
from src.config.objects import LastFetchDatetime, TargetObject
from src.error.exceptions import FileUploadException
from src.system_var.constants import UPD_JP_NAME
from src.util.logger import logger_instance as logger
def upload_last_fetch_datetime_process(target_object: TargetObject, last_fetch_datetime: LastFetchDatetime):
"""前回取得日時ファイル更新
Args:
target_object (TargetObject): 取得対象オブジェクト情報インスタンス
last_fetch_datetime (LastFetchDatetime): データ取得期間設定インスタンス
Raises:
FileUploadException: S3のファイルアップロード失敗
"""
# ① 前回取得日時ファイル更新処理の開始ログを出力する
logger.info(
f'I-UPD-01 [{target_object.object_name}] の前回取得日時ファイルの更新処理を開始します')
try:
if target_object.is_update_last_fetch_datetime is False:
# ② オブジェクト情報.is_update_last_fetch_datetimeがfalseの場合、以降の処理をスキップする
logger.info(
f'I-UPD-02 [{target_object.object_name}] の前回取得日時ファイルの更新処理をスキップします')
return
# ③ 前回取得日時ファイル.last_fetch_datetime_fromに取得処理開始年月日時分秒を設定する
# 前回取得日時ファイル.last_fetch_datetime_toに空文字を設定する
last_fetch_datetime_dict = {
'last_fetch_datetime_from': last_fetch_datetime.last_fetch_datetime_to,
'last_fetch_datetime_to': ''
}
config_bucket = ConfigBucket()
config_bucket.put_last_fetch_datetime_file(
target_object.last_fetch_datetime_file_name, json.dumps(last_fetch_datetime_dict))
logger.info(
f'D-UPD-03 [{target_object.object_name}] の前回取得日時ファイル更新処理 正常終了')
except Exception as e:
raise FileUploadException(
'E-UPD-01',
UPD_JP_NAME,
f'[{target_object.object_name}] 前回処理日時ファイルのアップロードに失敗しました ファイル名:[{target_object.last_fetch_datetime_file_name}] エラー内容:[{e}]')
# ④ 前回取得日時ファイル更新処理の終了ログを出力する
logger.info(
f'I-UPD-04 [{target_object.object_name}] の前回取得日時ファイルの更新処理を終了します')
# ⑤ 次の処理へ移行する
return

View File

@ -0,0 +1,39 @@
from src.aws.s3 import BackupBucket
from src.error.exceptions import FileUploadException
from src.system_var.constants import END_JP_NAME
from src.system_var.environments import PROCESS_RESULT_FILENAME
from src.util.execute_datetime import ExecuteDateTime
from src.util.logger import logger_instance as logger
def upload_result_data_process(process_result: dict, execute_datetime: ExecuteDateTime):
"""取得処理実施結果アップロード処理
Args:
process_result (dict): 取得処理実行結果辞書オブジェクト
execute_datetime (ExecuteDateTime): 実行日時取得インスタンス
Raises:
FileUploadException: S3のファイルアップロード失敗
"""
# ① 取得処理実施結果アップロード処理のログを出力する
logger.info(
f'I-END-01 取得処理実施結果アップロード処理を開始します')
try:
# ② CRMバックアップ保管用バケットに、取得処理実施結果のJSONデータを保管する
backup_bucket = BackupBucket()
backup_bucket.put_result_json(
f'{execute_datetime.to_path()}/{PROCESS_RESULT_FILENAME}', process_result)
logger.debug(f'D-END-02 取得処理実施結果アップロード 正常終了')
except Exception as e:
raise FileUploadException(
'E-END-01', END_JP_NAME, f'取得処理実施結果のアップロードに失敗しました ファイル名:[{PROCESS_RESULT_FILENAME}] エラー内容:[{e}]')
# ③ 取得処理実施結果アップロード処理の終了ログを出力する
logger.info(f'I-END-03 取得処理実施結果アップロード処理を終了します')
return

View File

View File

@ -0,0 +1,14 @@
class CounterObject:
def __init__(self, base_num=1) -> None:
self.__counter = int(base_num)
def describe(self) -> int:
return self.__counter
def increment(self, num=1) -> int:
self.__counter += num
return self.__counter
def decrement(self, num=1) -> int:
self.__counter -= num
return self.__counter

View File

@ -0,0 +1,51 @@
import re
class DictChecker:
def __init__(self, object_dict: dict) -> None:
self.__object_dict = object_dict
def is_empty(self, check_key):
"""辞書型バリュー空文字チェック"""
return self.__object_dict[check_key] == '' or self.__object_dict[check_key] is None
def is_list_empty(self, check_key):
"""list型データ存在チェック"""
return len(self.__object_dict[check_key]) == 0
def check_key_exist(self, check_key: str) -> bool:
"""辞書型キー存在チェック"""
return check_key in self.__object_dict and not self.is_empty(check_key)
def check_data_type(self, check_key: str, check_type: type) -> bool:
"""辞書型バリュー型チェック"""
return isinstance(self.__object_dict[check_key], check_type)
def check_match_pattern(self, regex_str: str, check_key: str) -> bool:
"""辞書型バリュー正規表現チェック"""
return True if re.fullmatch(regex_str, self.__object_dict[check_key]) else False
def assert_key_exist(self, check_key: str) -> None:
"""辞書型キー存在検査"""
if not self.check_key_exist(check_key):
raise Exception(f'{check_key}」キーは必須です')
return
def assert_data_type(self, check_key: str, check_type: type) -> None:
"""バリュー型検査"""
if not self.check_data_type(check_key, check_type):
raise Exception(f'{check_key}」キーの値は「{check_type}」でなければなりません')
return
def assert_match_pattern(self, check_key: str, regex_str: str, expected_str: str):
"""正規表現検査"""
if not self.check_match_pattern(regex_str, check_key):
raise Exception(f'{check_key}」キーの値の正規表現チェックに失敗しました 「{expected_str}」形式である必要があります')
return
def assert_list_empty(self, check_key: str):
if self.is_list_empty(check_key):
raise Exception(f'{check_key}」キーのリストの値は必須です')

View File

@ -0,0 +1,17 @@
from datetime import datetime, timezone
from src.system_var.constants import MILLISEC_FORMAT, YYYYMMDDTHHMMSSTZ
class ExecuteDateTime:
def __init__(self):
self.__execute_datetime = datetime.now(timezone.utc).strftime(YYYYMMDDTHHMMSSTZ)
def __str__(self) -> str:
return self.__execute_datetime
def to_path(self) -> str:
return self.__execute_datetime.rstrip(MILLISEC_FORMAT).translate(str.maketrans({'-': '/', 'T': '/', ':': None, '.': None}))
def format_date(self) -> str:
return self.__execute_datetime.rstrip(MILLISEC_FORMAT).translate(str.maketrans({'-': None, 'T': None, ':': None, '.': None}))

View File

@ -0,0 +1,37 @@
import logging
from src.system_var.environments import LOG_LEVEL
# boto3関連モジュールのログレベルを事前に個別指定し、モジュール内のDEBUGログの表示を抑止する
for name in ["boto3", "botocore", "s3transfer", "urllib3"]:
logging.getLogger(name).setLevel(logging.WARNING)
class Logger():
__logger: logging.Logger
def __init__(self):
self.__logger = logging.getLogger()
level = logging.getLevelName(LOG_LEVEL)
if not isinstance(level, int):
level = logging.INFO
self.__logger.setLevel(level)
if not self.__logger.hasHandlers():
handler = logging.StreamHandler()
self.__logger.addHandler(handler)
formatter = logging.Formatter(
'[%(levelname)s]\t%(asctime)s\t%(message)s\n',
'%Y-%m-%d %H:%M:%S'
)
for handler in self.__logger.handlers:
handler.setFormatter(formatter)
def get_logger(self) -> logging.Logger:
return self.__logger
logger_instance = Logger().get_logger()

View File

View File

View File

@ -0,0 +1,487 @@
import pytest
from src.aws.s3 import BackupBucket, ConfigBucket, DataBucket, S3Resource
@pytest.fixture
def bucket_name():
return 'test-bucket'
@pytest.fixture
def s3_test(s3_client, bucket_name):
s3_client.create_bucket(Bucket=bucket_name)
yield
class TestS3Resource:
def test_get_object(self, s3_test, s3_client, bucket_name):
"""
Cases:
- S3からオブジェクトが取得できるか
Arranges:
- S3をモック化する
- 期待値となるファイルを配置する
Expects:
- オブジェクトが取得でき期待値と正しいこと
"""
# Arrange
s3_client.put_object(Bucket=bucket_name, Key='hogehoge/test.txt', Body=b'aaaaaaaaaaaaaaa')
# Act
sut = S3Resource(bucket_name)
actual = sut.get_object('hogehoge/test.txt')
# Assert
assert actual == 'aaaaaaaaaaaaaaa'
def test_raise_get_object(self, s3_test, s3_client, bucket_name):
"""
Cases:
- オブジェクトのキーが存在しない場合エラーになること
Arranges:
- S3をモック化する
Expects:
- オブジェクト取得時にエラーになること
"""
# Arrange
# Act
sut = S3Resource(bucket_name)
with pytest.raises(Exception):
# Assert
sut.get_object('hogehoge/test.txt')
def test_put_object(self, s3_test, s3_client, bucket_name):
"""
Cases:
- S3にオブジェクトをPUTできるか
Arranges:
- S3をモック化する
Expects:
- PUTされたファイルが存在する
- PUTされたファイルの内容が期待値と一致する
"""
sut = S3Resource(bucket_name)
sut.put_object('hogehoge/test.txt', 'aaaaaaaaaaaaaaa')
actual = s3_client.get_object(Bucket=bucket_name, Key='hogehoge/test.txt')
assert actual['Body'].read().decode('utf-8') == 'aaaaaaaaaaaaaaa'
def test_raise_put_object(self, s3_client, bucket_name):
"""
Cases:
- バケットが存在しない場合エラーになること
Arranges:
- S3をモック化する
- バケットを作成しない
Expects:
- オブジェクト登録時にエラーになること
"""
# Arrange
# Act
sut = S3Resource(bucket_name)
with pytest.raises(Exception):
# Assert
sut.put_object('hogehoge/test.txt', 'aaaaaaaaaaaaaaa')
def test_copy(self, s3_test, s3_client, bucket_name):
"""
Cases:
- S3内のオブジェクトを別バケットにコピーできるか
Arranges:
- S3をモック化する
- コピー先バケットを作成する
- 期待値となるファイルをコピー元バケットに配置する
Expects:
- コピーされたファイルが存在する
- コピーされたファイルの内容が期待値と一致する
"""
for_copy_bucket = 'for-copy-bucket'
s3_client.create_bucket(Bucket=for_copy_bucket)
s3_client.put_object(Bucket=bucket_name, Key='hogehoge/test.txt', Body=b'aaaaaaaaaaaaaaa')
sut = S3Resource(bucket_name)
sut.copy(bucket_name, 'hogehoge/test.txt', for_copy_bucket, 'test.txt')
actual = s3_client.get_object(Bucket=for_copy_bucket, Key='test.txt')
assert actual['Body'].read() == b'aaaaaaaaaaaaaaa'
def test_raise_copy(self, s3_client, s3_test, bucket_name):
"""
Cases:
- コピー対象のキーが存在しない場合エラーになること
Arranges:
- S3をモック化する
Expects:
- オブジェクト登録時にエラーになること
"""
# Arrange
# Act
sut = S3Resource(bucket_name)
with pytest.raises(Exception):
# Assert
sut.copy(bucket_name, 'hogehoge/test.txt', 'for_copy_bucket', 'test.txt')
def test_raise_init_no_provide_bucket_name(self):
"""
Cases:
- バケット名を指定しない場合例外となること
Arranges:
Expects:
- インスタンス生成時に例外が発生すること
"""
with pytest.raises(Exception) as e:
S3Resource()
assert e.value.args[0] == "__init__() missing 1 required positional argument: 'bucket_name'"
class TestConfigBucket:
def test_get_object_info_file(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- オブジェクト情報ファイルが取得できること
Arranges:
- 環境変数を置き換える
- オブジェクト情報ファイルを配置する
Expects:
- オブジェクト情報ファイルが文字列として取得でき期待値と一致する
"""
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.OBJECT_INFO_FOLDER', 'crm')
monkeypatch.setattr('src.aws.s3.OBJECT_INFO_FILENAME', 'objects.json')
s3_client.put_object(Bucket=bucket_name, Key=f'crm/objects.json', Body=b'aaaaaaaaaaaaaaa')
sut = ConfigBucket()
actual = sut.get_object_info_file()
assert actual == 'aaaaaaaaaaaaaaa'
def test_raise_get_object_info_file(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- オブジェクト情報ファイルが存在しない場合エラーになること
Arranges:
- s3_testフィクスチャを引数に指定しない
- 環境変数を置き換える
Expects:
- オブジェクト情報ファイルが取得できず例外が発生する
"""
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.OBJECT_INFO_FOLDER', 'crm')
monkeypatch.setattr('src.aws.s3.OBJECT_INFO_FILENAME', 'objects.json')
sut = ConfigBucket()
with pytest.raises(Exception):
sut.get_object_info_file()
def test_get_last_fetch_datetime_file(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- オブジェクト最終更新日時ファイルが取得できること
Arranges:
- S3をモック化する
- オブジェクト最終更新日時ファイルを配置する
Expects:
- オブジェクト最終更新日時ファイルが文字列として取得でき期待値と一致する
"""
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.LAST_FETCH_DATE_FOLDER', 'crm')
s3_client.put_object(Bucket=bucket_name, Key=f'crm/Object.json', Body=b'aaaaaaaaaaaaaaa')
sut = ConfigBucket()
actual = sut.get_last_fetch_datetime_file('Object.json')
assert actual == 'aaaaaaaaaaaaaaa'
def test_raise_get_last_fetch_datetime_file(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- オブジェクト最終更新日時ファイルが存在しない場合エラーになること
Arranges:
- s3_testフィクスチャを引数に指定しない
- 環境変数を置き換える
Expects:
- オブジェクト最終更新日時ファイルが取得できず例外が発生する
"""
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.LAST_FETCH_DATE_FOLDER', 'crm')
sut = ConfigBucket()
with pytest.raises(Exception):
sut.get_last_fetch_datetime_file('Object.json')
def test_put_last_fetch_datetime_file(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- オブジェクト最終更新日時ファイルをPUTできること
Arranges:
- S3をモック化する
- 環境変数をテスト用の値に置き換える
Expects:
- オブジェクト最終更新日時ファイルが存在する
"""
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.LAST_FETCH_DATE_FOLDER', 'crm')
sut = ConfigBucket()
sut.put_last_fetch_datetime_file('Object.json', 'aaaaaaaaaaaaaaa')
actual = s3_client.get_object(Bucket=bucket_name, Key=f'crm/Object.json')
assert actual['Body'].read().decode('utf-8') == 'aaaaaaaaaaaaaaa'
def test_raise_put_last_fetch_datetime_file(self, s3_client, bucket_name, monkeypatch):
"""
Cases:
- オブジェクト最終更新日時ファイルをPUTできない場合エラーになること
Arranges:
- s3_testフィクスチャを引数に指定しない
- 環境変数を置き換える
Expects:
- オブジェクト最終更新日時ファイルをPUTできず例外が発生する
"""
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.LAST_FETCH_DATE_FOLDER', 'crm')
sut = ConfigBucket()
with pytest.raises(Exception):
sut.put_last_fetch_datetime_file('Object.json', 'aaaaaaaaaaaaaaa')
def test_config_bucket_str(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- 設定ファイル配置バケットを文字列化したときバケット名が取得できること
Arranges:
- 環境変数をテスト用の値に置き換える
Expects:
- 環境変数の設定ファイル配置バケット名と一致する
"""
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
sut = ConfigBucket()
assert str(sut) == bucket_name
class TestDataBucket:
def test_put_csv(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- CSVファイルをPUTできること
Arranges:
- S3をモック化する
- 環境変数をテスト用の値に置き換える
Expects:
- PUTしたファイルが存在する
"""
monkeypatch.setattr('src.aws.s3.IMPORT_DATA_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.CRM_IMPORT_DATA_FOLDER', 'crm/target')
sut = DataBucket()
sut.put_csv('test.csv', 'test,test,test')
actual = s3_client.get_object(Bucket=bucket_name, Key=f'crm/target/test.csv')
assert actual['Body'].read().decode('utf-8') == 'test,test,test'
def test_raise_put_csv(self, s3_client, bucket_name, monkeypatch):
"""
Cases:
- CSVファイルをPUTできずエラーになること
Arranges:
- s3_testフィクスチャを引数に指定しない
- 環境変数を置き換える
Expects:
- CSVファイルをPUTできず例外が発生する
"""
monkeypatch.setattr('src.aws.s3.IMPORT_DATA_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.CRM_IMPORT_DATA_FOLDER', 'crm/target')
sut = DataBucket()
with pytest.raises(Exception):
sut.put_csv('test.csv', 'test,test,test')
def test_put_csv_from(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- 他のバケットからCSVファイルをコピーできること
Arranges:
- S3をモック化する
- コピー先バケットを作成する
- 期待値となるファイルコピー元バケットに配置する
- 環境変数をテスト用の値に置き換える
Expects:
- コピーしたファイルが存在する
"""
for_copy_bucket = 'for-copy-bucket'
s3_client.create_bucket(Bucket=for_copy_bucket)
s3_client.put_object(Bucket=bucket_name, Key='hogehoge/test.csv', Body=b'test,test,test')
monkeypatch.setattr('src.aws.s3.IMPORT_DATA_BUCKET', for_copy_bucket)
monkeypatch.setattr('src.aws.s3.CRM_IMPORT_DATA_FOLDER', 'crm/target')
sut = DataBucket()
sut.put_csv_from(bucket_name, 'hogehoge/test.csv')
actual = s3_client.get_object(Bucket=for_copy_bucket, Key=f'crm/target/test.csv')
assert actual['Body'].read().decode('utf-8') == 'test,test,test'
def test_raise_put_csv_from(self, s3_client, bucket_name, monkeypatch):
"""
Cases:
- 他のバケットからCSVファイルをコピーできずエラーになること
Arranges:
- s3_testフィクスチャを引数に指定しない
- 環境変数を置き換える
Expects:
- コピーできず例外が発生する
"""
monkeypatch.setattr('src.aws.s3.IMPORT_DATA_BUCKET', 'for_copy_bucket')
monkeypatch.setattr('src.aws.s3.CRM_IMPORT_DATA_FOLDER', 'crm/target')
sut = DataBucket()
with pytest.raises(Exception):
sut.put_csv_from(bucket_name, 'hogehoge/test.csv')
def test_data_bucket_str(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- データ登録バケットを文字列化したときバケット名が取得できること
Arranges:
- 環境変数をテスト用の値に置き換える
Expects:
- 環境変数のデータ登録バケット名と一致する
"""
monkeypatch.setattr('src.aws.s3.IMPORT_DATA_BUCKET', bucket_name)
sut = DataBucket()
assert str(sut) == bucket_name
class TestBackupBucket:
def test_put_csv(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- CSVファイルをPUTできること
Arranges:
- S3をモック化する
- 環境変数をテスト用の値に置き換える
Expects:
- PUTしたファイルが存在する
"""
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.CRM_IMPORT_DATA_BACKUP_FOLDER', 'data_import')
sut = BackupBucket()
sut.put_csv('test.csv', 'test,test,test')
actual = s3_client.get_object(Bucket=bucket_name, Key=f'data_import/test.csv')
assert actual['Body'].read().decode('utf-8') == 'test,test,test'
def test_raise_put_csv_from(self, s3_client, bucket_name, monkeypatch):
"""
Cases:
- CSVファイルをPUTできずエラーになること
Arranges:
- s3_testフィクスチャを引数に指定しない
- 環境変数を置き換える
Expects:
- CSVファイルがPUTできず例外が発生する
"""
monkeypatch.setattr('src.aws.s3.IMPORT_DATA_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.CRM_IMPORT_DATA_FOLDER', 'crm/target')
sut = BackupBucket()
with pytest.raises(Exception):
sut.put_csv('test.csv', 'test,test,test')
def test_put_response_json(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- JSONファイルをPUTできること
Arranges:
- S3をモック化する
- 環境変数をテスト用の値に置き換える
Expects:
- PUTしたファイルが存在する
"""
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.RESPONSE_JSON_BACKUP_FOLDER', 'response_json')
sut = BackupBucket()
sut.put_response_json('test.json', {"test": "test"})
actual = s3_client.get_object(Bucket=bucket_name, Key=f'response_json/test.json')
assert actual['Body'].read().decode('utf-8') == '{"test": "test"}'
def test_raise_response_json(self, s3_client, bucket_name, monkeypatch):
"""
Cases:
- JSONファイルをPUTできずエラーになること
Arranges:
- s3_testフィクスチャを引数に指定しない
- 環境変数を置き換える
Expects:
- JSONファイルをPUTできず例外が発生する
"""
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.RESPONSE_JSON_BACKUP_FOLDER', 'response_json')
sut = BackupBucket()
with pytest.raises(Exception):
sut.put_response_json('test.json', {"test": "test"})
def test_put_result_json(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- 結果のJSONファイルをPUTできること
Arranges:
- S3をモック化する
- 環境変数をテスト用の値に置き換える
Expects:
- PUTしたファイルが存在する
"""
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.PROCESS_RESULT_FOLDER', 'data_import')
sut = BackupBucket()
sut.put_result_json('result.json', {"test": "test"})
actual = s3_client.get_object(Bucket=bucket_name, Key=f'data_import/result.json')
assert actual['Body'].read().decode('utf-8') == '{"test": "test"}'
def test_raise_result_json(self, s3_client, bucket_name, monkeypatch):
"""
Cases:
- 結果のJSONファイルをPUTできずエラーになること
Arranges:
- s3_testフィクスチャを引数に指定しない
- 環境変数を置き換える
Expects:
- 結果のJSONファイルをPUTできず例外が発生する
"""
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.PROCESS_RESULT_FOLDER', 'data_import')
sut = BackupBucket()
with pytest.raises(Exception):
sut.put_result_json('result.json', {"test": "test"})
def test_backup_bucket_str(self, s3_test, s3_client, bucket_name, monkeypatch):
"""
Cases:
- CRMデータバックアップバケットを文字列化したときバケット名が取得できること
Arranges:
- 環境変数をテスト用の値に置き換える
Expects:
- 環境変数のCRMデータバックアップバケット名と一致する
"""
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', bucket_name)
sut = BackupBucket()
assert str(sut) == bucket_name

View File

@ -0,0 +1,467 @@
import pytest
from src.config.objects import FetchTargetObjects
from src.parser.json_parser import JsonParser
class TestFetchTargetObjects():
def test_constructor(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータに対してキーがあるかまたキーの型が正しいかをチェック
Arranges:
- オブジェクト情報文字列を準備する
- オブジェクト情報を辞書型にパースする
Expects:
- 例外が発生しないこと
"""
# Arranges
fetch_objects = '''{
"objects": [
{
"object_name": "AccountShare",
"columns": [
"Id",
"AccountId",
"UserOrGroupId",
"AccountAccessLevel",
"OpportunityAccessLevel",
"CaseAccessLevel",
"ContactAccessLevel",
"RowCause",
"LastModifiedDate",
"LastModifiedById",
"IsDeleted"
],
"is_skip": false,
"is_update_last_fetch_datetime": true,
"datetime_column": "LastModifiedDate"
},
{
"object_name": "Contact",
"columns": [
"Id",
"IsDeleted",
"MasterRecordId",
"AccountId",
"IsPersonAccount",
"LastName",
"FirstName",
"Salutation",
"Name",
"OtherStreet",
"OtherCity",
"OtherState",
"OtherPostalCode",
"OtherCountry",
"OtherLatitude",
"OtherLongitude",
"OtherGeocodeAccuracy",
"OtherAddress",
"MailingStreet",
"MailingCity",
"MailingState",
"MailingPostalCode",
"MailingCountry",
"MailingLatitude",
"MailingLongitude",
"MailingGeocodeAccuracy",
"MailingAddress",
"Phone",
"Fax",
"MobilePhone",
"HomePhone",
"OtherPhone",
"AssistantPhone",
"ReportsToId",
"Email",
"Title",
"Department",
"AssistantName",
"Birthdate",
"Description",
"OwnerId",
"HasOptedOutOfEmail",
"HasOptedOutOfFax",
"DoNotCall",
"CreatedDate",
"CreatedById",
"LastModifiedDate",
"LastModifiedById",
"SystemModstamp",
"LastActivityDate",
"LastCURequestDate",
"LastCUUpdateDate",
"MayEdit",
"IsLocked",
"LastViewedDate",
"LastReferencedDate",
"EmailBouncedReason",
"EmailBouncedDate",
"IsEmailBounced",
"PhotoUrl",
"Jigsaw",
"JigsawContactId",
"IndividualId",
"Mobile_ID_vod__c",
"H1Insights__H1_NPI_Value_for_Testing__c",
"H1Insights__H1_Person_ID__c",
"H1Insights__H1_Request_Status__c",
"H1Insights__H1_URL__c",
"H1Insights__NPI_Number__c",
"H1Insights__NPI_Number_for_H1_Insights__c",
"MSJ_Marketing_Cloud_Integration__c"
],
"is_skip": false,
"is_update_last_fetch_datetime": true
},
{
"object_name": "Territory2",
"columns": [
"Id",
"Name",
"Territory2TypeId",
"Territory2ModelId",
"ParentTerritory2Id",
"Description",
"ForecastUserId",
"AccountAccessLevel",
"OpportunityAccessLevel",
"CaseAccessLevel",
"ContactAccessLevel",
"LastModifiedDate",
"LastModifiedById",
"SystemModstamp",
"DeveloperName",
"MSJ_Territory_Type__c",
"MSJ_Level__c"
],
"is_skip": false,
"is_update_last_fetch_datetime": true
}
]
}'''
json_parser = JsonParser(fetch_objects)
fetch_objects_dict = json_parser.parse()
# Act
FetchTargetObjects(fetch_objects_dict)
# Expects
pass
def test_raise_constructor_no_key(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータに対して必要なキーがない場合例外が発生すること
Arranges:
- オブジェクト情報文字列を準備する
Expects:
- 例外が発生し期待値と一致する
"""
# Arranges
fetch_objects_dict = {
"test_objects": [
{
"object_name": "AccountShare",
"columns": [
"Id",
"AccountId",
"UserOrGroupId",
"AccountAccessLevel",
"OpportunityAccessLevel",
"CaseAccessLevel",
"ContactAccessLevel",
"RowCause",
"LastModifiedDate",
"LastModifiedById",
"IsDeleted"
],
"is_skip": False,
"is_update_last_fetch_datetime": False,
"datetime_column": "LastModifiedDate"
},
{
"object_name": "Contact",
"columns": [
"Id",
"IsDeleted",
"MasterRecordId",
"AccountId",
"IsPersonAccount",
"LastName",
"FirstName",
"Salutation",
"Name",
"OtherStreet",
"OtherCity",
"OtherState",
"OtherPostalCode",
"OtherCountry",
"OtherLatitude",
"OtherLongitude",
"OtherGeocodeAccuracy",
"OtherAddress",
"MailingStreet",
"MailingCity",
"MailingState",
"MailingPostalCode",
"MailingCountry",
"MailingLatitude",
"MailingLongitude",
"MailingGeocodeAccuracy",
"MailingAddress",
"Phone",
"Fax",
"MobilePhone",
"HomePhone",
"OtherPhone",
"AssistantPhone",
"ReportsToId",
"Email",
"Title",
"Department",
"AssistantName",
"Birthdate",
"Description",
"OwnerId",
"HasOptedOutOfEmail",
"HasOptedOutOfFax",
"DoNotCall",
"CreatedDate",
"CreatedById",
"LastModifiedDate",
"LastModifiedById",
"SystemModstamp",
"LastActivityDate",
"LastCURequestDate",
"LastCUUpdateDate",
"MayEdit",
"IsLocked",
"LastViewedDate",
"LastReferencedDate",
"EmailBouncedReason",
"EmailBouncedDate",
"IsEmailBounced",
"PhotoUrl",
"Jigsaw",
"JigsawContactId",
"IndividualId",
"Mobile_ID_vod__c",
"H1Insights__H1_NPI_Value_for_Testing__c",
"H1Insights__H1_Person_ID__c",
"H1Insights__H1_Request_Status__c",
"H1Insights__H1_URL__c",
"H1Insights__NPI_Number__c",
"H1Insights__NPI_Number_for_H1_Insights__c",
"MSJ_Marketing_Cloud_Integration__c"
],
"is_skip": False,
"is_update_last_fetch_datetime": True
},
{
"object_name": "Territory2",
"columns": [
"Id",
"Name",
"Territory2TypeId",
"Territory2ModelId",
"ParentTerritory2Id",
"Description",
"ForecastUserId",
"AccountAccessLevel",
"OpportunityAccessLevel",
"CaseAccessLevel",
"ContactAccessLevel",
"LastModifiedDate",
"LastModifiedById",
"SystemModstamp",
"DeveloperName",
"MSJ_Territory_Type__c",
"MSJ_Level__c"
],
"is_skip": False,
"is_update_last_fetch_datetime": True
}
]
}
# Act
with pytest.raises(Exception) as e:
FetchTargetObjects(fetch_objects_dict)
# Expects
assert str(e.value) == '「objects」キーは必須です'
def test_raise_constructor_no_type(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータの対象のキーの値の型が想定と違う場合例外が発生すること
Arranges:
- オブジェクト情報文字列を準備する
Expects:
- 例外が発生し期待値と一致する
"""
# Arranges
fetch_objects_dict = {
"objects": "test_value"
}
# Act
with pytest.raises(Exception) as e:
FetchTargetObjects(fetch_objects_dict)
# Expects
assert str(e.value) == '「objects」キーの値は「<class \'list\'>」でなければなりません'
def test_constructor_iterator(self) -> None:
"""
Cases:
インスタンス生成テスト
登録されたオブジェクトリストをすべて取り出せること
Arranges:
- オブジェクト情報文字列を準備する
Expects:
- ループが最後まで回ること
"""
fetch_objects_dict = {
"objects": [
{
"object_name": "AccountShare",
"columns": [
"Id",
"AccountId",
"UserOrGroupId",
"AccountAccessLevel",
"OpportunityAccessLevel",
"CaseAccessLevel",
"ContactAccessLevel",
"RowCause",
"LastModifiedDate",
"LastModifiedById",
"IsDeleted"
],
"is_skip": False,
"is_update_last_fetch_datetime": False,
"datetime_column": "LastModifiedDate"
},
{
"object_name": "Contact",
"columns": [
"Id",
"IsDeleted",
"MasterRecordId",
"AccountId",
"IsPersonAccount",
"LastName",
"FirstName",
"Salutation",
"Name",
"OtherStreet",
"OtherCity",
"OtherState",
"OtherPostalCode",
"OtherCountry",
"OtherLatitude",
"OtherLongitude",
"OtherGeocodeAccuracy",
"OtherAddress",
"MailingStreet",
"MailingCity",
"MailingState",
"MailingPostalCode",
"MailingCountry",
"MailingLatitude",
"MailingLongitude",
"MailingGeocodeAccuracy",
"MailingAddress",
"Phone",
"Fax",
"MobilePhone",
"HomePhone",
"OtherPhone",
"AssistantPhone",
"ReportsToId",
"Email",
"Title",
"Department",
"AssistantName",
"Birthdate",
"Description",
"OwnerId",
"HasOptedOutOfEmail",
"HasOptedOutOfFax",
"DoNotCall",
"CreatedDate",
"CreatedById",
"LastModifiedDate",
"LastModifiedById",
"SystemModstamp",
"LastActivityDate",
"LastCURequestDate",
"LastCUUpdateDate",
"MayEdit",
"IsLocked",
"LastViewedDate",
"LastReferencedDate",
"EmailBouncedReason",
"EmailBouncedDate",
"IsEmailBounced",
"PhotoUrl",
"Jigsaw",
"JigsawContactId",
"IndividualId",
"Mobile_ID_vod__c",
"H1Insights__H1_NPI_Value_for_Testing__c",
"H1Insights__H1_Person_ID__c",
"H1Insights__H1_Request_Status__c",
"H1Insights__H1_URL__c",
"H1Insights__NPI_Number__c",
"H1Insights__NPI_Number_for_H1_Insights__c",
"MSJ_Marketing_Cloud_Integration__c"
],
"is_skip": False,
"is_update_last_fetch_datetime": True
},
{
"object_name": "Territory2",
"columns": [
"Id",
"Name",
"Territory2TypeId",
"Territory2ModelId",
"ParentTerritory2Id",
"Description",
"ForecastUserId",
"AccountAccessLevel",
"OpportunityAccessLevel",
"CaseAccessLevel",
"ContactAccessLevel",
"LastModifiedDate",
"LastModifiedById",
"SystemModstamp",
"DeveloperName",
"MSJ_Territory_Type__c",
"MSJ_Level__c"
],
"is_skip": False,
"is_update_last_fetch_datetime": True
}
]
}
# Act
sut = FetchTargetObjects(fetch_objects_dict)
for i, item in enumerate(sut, 1):
assert item is not None
# Expects
assert i == 3

View File

@ -0,0 +1,380 @@
import pytest
from src.config.objects import LastFetchDatetime
from src.util.execute_datetime import ExecuteDateTime
class TestLastFetchDatetime():
def test_constructor(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータに対してバリデーションチェックを行いチェックが通ることを確認する
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生しないこと
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": "2022-01-01T00:00:00.000Z"
}
execute_datetime = ExecuteDateTime()
# Act
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
pass
def test_raise_constructor_last_fetch_datetime_from_no_key(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータに対して必要なキー(last_fetch_datetime_from)がない場合例外が発生すること
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生し期待値と一致する
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_to": "2022-01-01T00:00:00.000Z"
}
execute_datetime = ExecuteDateTime()
# Act
with pytest.raises(Exception) as e:
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
assert str(e.value) == '「last_fetch_datetime_from」キーは必須です'
def test_raise_constructor_last_fetch_datetime_from_no_value(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータの対象のキー(last_fetch_datetime_from)の値が空文字の場合例外が発生すること
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生し期待値と一致する
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "",
"last_fetch_datetime_to": "2022-01-01T00:00:00.000Z"
}
execute_datetime = ExecuteDateTime()
# Act
with pytest.raises(Exception) as e:
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
assert str(e.value) == '「last_fetch_datetime_from」キーは必須です'
def test_raise_constructor_last_fetch_datetime_from_none_value(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータの対象のキー(last_fetch_datetime_from)の値がNoneの場合例外が発生すること
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生し期待値と一致する
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": None,
"last_fetch_datetime_to": "2022-01-01T00:00:00.000Z"
}
execute_datetime = ExecuteDateTime()
# Act
with pytest.raises(Exception) as e:
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
assert str(e.value) == '「last_fetch_datetime_from」キーは必須です'
def test_raise_constructor_last_fetch_datetime_from_other_type(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータの対象のキー(last_fetch_datetime_from)の値の型が想定と違う場合例外が発生すること
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生し期待値と一致する
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": 1,
"last_fetch_datetime_to": "2022-01-01T00:00:00.000Z"
}
execute_datetime = ExecuteDateTime()
# Act
with pytest.raises(Exception) as e:
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
assert str(e.value) == '「last_fetch_datetime_from」キーの値は「<class \'str\'>」でなければなりません'
def test_raise_constructor_last_fetch_datetime_from_other_string(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータの対象のキー(last_fetch_datetime_from)の値が正規表現と違う場合例外が発生すること
Arranges:
- 辞書型のオブジェクト情報を準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生し期待値と一致する
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "aaa",
"last_fetch_datetime_to": "2022-01-01T00:00:00.000Z"
}
execute_datetime = ExecuteDateTime()
# Act
with pytest.raises(Exception) as e:
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
assert str(
e.value) == '「last_fetch_datetime_from」キーの値の正規表現チェックに失敗しました 「YYYY-MM-DDTHH:MM:SS.000Z」形式である必要があります'
def test_raise_constructor_last_fetch_datetime_to_no_key(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータに対してキー(last_fetch_datetime_to)がない場合例外が発生しないこと
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生しないこと
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z"
}
execute_datetime = ExecuteDateTime()
# Act
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
pass
def test_constructor_last_fetch_datetime_to_no_value(self) -> None:
"""
Cases:
辞書型のデータの対象のキー(last_fetch_datetime_to)の値の型が空文字の場合例外が発生しないこと
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生しないこと
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}
execute_datetime = ExecuteDateTime()
# Act
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
None
def test_constructor_last_fetch_datetime_to_none__value(self) -> None:
"""
Cases:
辞書型のデータの対象のキー(last_fetch_datetime_to)の値の型がNoneの場合例外が発生しないこと
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生しないこと
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": None
}
execute_datetime = ExecuteDateTime()
# Act
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
pass
def test_raise_constructor_last_fetch_datetime_to_other_type(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータの対象のキー(last_fetch_datetime_to)の値の型が想定と違う場合例外が発生すること
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生し期待値と一致する
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": 1
}
execute_datetime = ExecuteDateTime()
# Act
with pytest.raises(Exception) as e:
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
assert str(e.value) == '「last_fetch_datetime_to」キーの値は「<class \'str\'>」でなければなりません'
def test_raise_constructor_last_fetch_datetime_to_other_string(self) -> None:
"""
Cases:
インスタンス生成テスト
辞書型のデータの対象のキー(last_fetch_datetime_to)の値が正規表現と違う場合例外が発生すること
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
Expects:
- 例外が発生し期待値と一致する
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": "aaa"
}
execute_datetime = ExecuteDateTime()
# Act
with pytest.raises(Exception) as e:
LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Expects
assert str(
e.value) == '「last_fetch_datetime_to」キーの値の正規表現チェックに失敗しました 「YYYY-MM-DDTHH:MM:SS.000Z」形式である必要があります'
def test_last_fetch_datetime_from(self) -> str:
"""
Cases:
オブジェクト情報から対象の値を返すこと
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
- 前回取得日時インスタンスを生成する
Expects:
- 戻り値が期待値と一致する
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": "2022-01-01T00:00:00.000Z"
}
execute_datetime = ExecuteDateTime()
sut = LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Act
actual = sut.last_fetch_datetime_from
# Expects
assert actual == '1900-01-01T00:00:00.000Z'
def test_last_fetch_datetime_to(self) -> str:
"""
Cases:
オブジェクト情報から対象の値を返すこと
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
- 前回取得日時インスタンスを生成する
Expects:
- 戻り値が期待値と一致する
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": "2022-01-01T00:00:00.000Z"
}
execute_datetime = ExecuteDateTime()
sut = LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Act
actual = sut.last_fetch_datetime_to
# Expects
assert actual == '2022-01-01T00:00:00.000Z'
def test_last_fetch_datetime_to_default(self) -> str:
"""
Cases:
オブジェクト情報から対象の値を返すこと
Arranges:
- 辞書型の前回取得日時データを準備する
- 実行日時インスタンスを生成する
- 前回取得日時インスタンスを生成する
Expects:
- 戻り値が期待値と一致する
"""
# Arranges
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}
execute_datetime = ExecuteDateTime()
sut = LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Act
actual = sut.last_fetch_datetime_to
# Expects
assert actual == str(execute_datetime)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,79 @@
"""pytestでフィクスチャやフック(テスト実行前後に差し込む処理)を管理するモジュール"""
import os
from datetime import datetime
import boto3
import pytest
from moto import mock_s3
from py.xml import html # type: ignore
from . import docstring_parser
@pytest.fixture
def aws_credentials():
"""Mocked AWS Credentials for moto."""
os.environ["AWS_ACCESS_KEY_ID"] = "testing"
os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
os.environ["AWS_SECURITY_TOKEN"] = "testing"
os.environ["AWS_SESSION_TOKEN"] = "testing"
@pytest.fixture
def s3_client(aws_credentials):
with mock_s3():
conn = boto3.client("s3", region_name="us-east-1")
yield conn
# 以下、レポート出力用の設定
def pytest_html_report_title(report):
# レポートタイトル
report.title = "CRMデータ連携 CRMデータ取得機能 単体機能テスト結果報告書"
def pytest_html_results_table_header(cells):
del cells[2:]
cells.insert(3, html.th("Cases"))
cells.insert(4, html.th("Arranges"))
cells.insert(5, html.th("Expects"))
cells.append(html.th("Time", class_="sortable time", col="time"))
def pytest_html_results_table_row(report, cells):
del cells[2:]
cells.insert(3, html.td(html.pre(report.cases))) # 「テスト内容」をレポートに出力
cells.insert(4, html.td(html.pre(report.arranges))) # 「期待結果」をレポートに出力
cells.insert(5, html.td(html.pre(report.expects))) # 「期待結果」をレポートに出力
cells.append(html.td(datetime.now(), class_="col-time")) # ついでに「時間」もレポートに出力
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
docstring = docstring_parser.parse(str(item.function.__doc__))
report.cases = docstring.get("Cases", '') # 「テスト内容」を`report`に追加
report.arranges = docstring.get("Arranges", '') # 「準備作業」を`report`に追加
report.expects = docstring.get("Expects", '') # 「期待結果」を`report`に追加
def pytest_addoption(parser):
parser.addoption(
"--walk-through", action="store_true", default=False, help="run walk through tests"
)
def pytest_configure(config):
config.addinivalue_line("markers", "slow: mark test as slow to run")
def pytest_collection_modifyitems(config, items):
skip_slow = pytest.mark.skip(reason="need --walk-through option to run")
if config.getoption("--walk-through"):
[item.add_marker(skip_slow) for item in items if "walk_through" not in item.keywords]
return
for item in items:
if "walk_through" in item.keywords:
item.add_marker(skip_slow)

View File

@ -0,0 +1,357 @@
from collections import OrderedDict
from src.converter.convert_strategy import (BooleanConvertStrategy,
ConvertStrategyFactory,
DatetimeConvertStrategy,
DictConvertStrategy,
IntConvertStrategy,
NoneValueConvertStrategy,
StringConvertStrategy)
class TestConvertStrategyFactory:
def test_create_none(self):
"""
Cases:
引数にnull(None)を指定した場合NoneValueConvertStrategyインスタンスが返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = ConvertStrategyFactory()
actual = sut.create(None)
# Expects
assert type(actual) == NoneValueConvertStrategy
def test_create_float(self):
"""
Cases:
引数に指数表記を指定した場合StringConvertStrategyインスタンスが返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = ConvertStrategyFactory()
actual = sut.create(1.2345678E7)
# Expects
# 変換しない
assert type(actual) == StringConvertStrategy
def test_create_float_scale(self):
"""
Cases:
引数に少数を指定した場合StringConvertStrategyインスタンスが返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = ConvertStrategyFactory()
actual = sut.create(1.2345678)
# Expects
# 変換しない
assert type(actual) == StringConvertStrategy
def test_create_bool_true(self):
"""
Cases:
引数に論理値(True)を指定した場合BooleanConvertStrategyインスタンスが返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = ConvertStrategyFactory()
actual = sut.create(True)
# Expects
assert type(actual) == BooleanConvertStrategy
def test_create_bool_false(self):
"""
Cases:
引数に論理値(False)を指定した場合BooleanConvertStrategyインスタンスが返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = ConvertStrategyFactory()
actual = sut.create(False)
# Expects
assert type(actual) == BooleanConvertStrategy
def test_create_datetime(self):
"""
Cases:
引数に文字列かつ日付文字列を指定した場合DatetimeConvertStrategyインスタンスが返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = ConvertStrategyFactory()
actual = sut.create('2022-06-13T10:15:32.000+0000')
# Expects
assert type(actual) == DatetimeConvertStrategy
def test_create_str(self):
"""
Cases:
引数にSalesforce日付型以外の文字列を指定した場合StringConvertStrategyインスタンスが返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = ConvertStrategyFactory()
actual = sut.create('test_string')
# Expects
assert type(actual) == StringConvertStrategy
def test_create_int(self):
"""
Cases:
引数に整数を指定した場合IntConvertStrategyインスタンスが返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = ConvertStrategyFactory()
actual = sut.create(100)
# Expects
assert type(actual) == IntConvertStrategy
def test_create_dict(self):
"""
Cases:
引数に辞書型の値を指定した場合IntConvertStrategyインスタンスが返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = ConvertStrategyFactory()
actual = sut.create({'key': 'value'})
# Expects
assert type(actual) == DictConvertStrategy
def test_create_ordered_dict_dict(self):
"""
Cases:
引数に辞書型の値を指定した場合IntConvertStrategyインスタンスが返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = ConvertStrategyFactory()
actual = sut.create(OrderedDict([('key', 'value')]))
# Expects
assert type(actual) == DictConvertStrategy
class TestNoneValueConvertStrategy:
def test_convert_value(self) -> str:
"""
Cases:
引数にnull(None)を指定した場合''(空文字)が返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = NoneValueConvertStrategy()
actual = sut.convert_value(None)
# Expects
assert actual == ""
class TestBooleanConvertStrategy:
def test_convert_value_true(self) -> bool:
"""
Cases:
引数に論理値(True)を指定した場合数値(1)が返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = BooleanConvertStrategy()
actual = sut.convert_value(True)
# Expects
assert actual == 1
def test_convert_value_false(self) -> bool:
"""
Cases:
引数に論理値(False)を指定した場合数値(0)が返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = BooleanConvertStrategy()
actual = sut.convert_value(False)
# Expects
assert actual == 0
class TestDatetimeConvertStrategy:
def test_convert_value(self) -> str:
"""
Cases:
引数に日付文字列を指定した場合YYYY-MM-DD HH:MM:SSがJSTで返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = DatetimeConvertStrategy()
actual = sut.convert_value('2022-06-13T20:15:32.000+0000')
# Expects
assert actual == "2022-06-14 05:15:32"
class TestIntConvertStrategy:
def test_convert_value(self):
"""
Cases:
引数に整数を指定した場合加工されず整数が返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = IntConvertStrategy()
actual = sut.convert_value(100)
# Expects
assert actual == 100
class TestStringConvertStrategy:
def test_convert_value(self):
"""
Cases:
引数に文字列を指定した場合加工されず文字列が返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = StringConvertStrategy()
actual = sut.convert_value('テストデータ')
# Expects
assert actual == 'テストデータ'
class TestDictConvertStrategy:
def test_convert_value_dict(self):
"""
Cases:
引数に辞書型のデータを指定した場合JSONの文字列が返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = DictConvertStrategy()
actual = sut.convert_value({'テストデータキー': 'テストデータバリュー'})
# Expects
assert actual == '{"テストデータキー": "テストデータバリュー"}'
def test_convert_value_dict_in_line_break(self):
"""
Cases:
引数に辞書型のデータを指定した場合JSONの文字列が返ってくること(バリューに改行を含む)
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = DictConvertStrategy()
actual = sut.convert_value({'テストデータキー': 'テスト\nデータ\nバリュー'})
# Expects
assert actual == '{"テストデータキー": "テスト\\nデータ\\nバリュー"}'
def test_convert_value_ordered_dict(self):
"""
Cases:
引数に整列された辞書型のデータを指定した場合JSONの文字列が返ってくること
Arranges:
- なし
Expects:
- 戻り値が期待値と一致する
"""
# Act
sut = DictConvertStrategy()
actual = sut.convert_value(OrderedDict(
[('テストデータキー', 'テストデータバリュー')]
))
# Expects
assert actual == '{"テストデータキー": "テストデータバリュー"}'

View File

@ -0,0 +1,393 @@
import textwrap
from collections import OrderedDict
import pytest
from src.config.objects import TargetObject
from src.converter.converter import CSVStringConverter
from src.util.execute_datetime import ExecuteDateTime
class TestCSVStringConverter:
def test_convert(self) -> str:
"""
Cases:
入力データがCSV形式の文字列で出力されること
Arranges:
- オブジェクト情報の作成
- データの作成
- 実行日時取得インスタンスの生成
- オブジェクト情報インスタンスの生成
Expects:
戻り値が期待値と一致すること
"""
# Arranges
object_info = {
"object_name": "AccountShare",
"columns": [
"Id",
"AccountId",
"UserOrGroupId",
"AccountAccessLevel",
"OpportunityAccessLevel",
"CaseAccessLevel",
"ContactAccessLevel",
"RowCause",
"LastModifiedDate",
"LastModifiedById",
"IsDeleted"
],
"is_skip": False,
"is_update_last_fetch_datetime": False,
"last_fetch_datetime_file_name": "AccountShare.json",
"upload_file_name": "CRM_AccountShare_{execute_datetime}",
"datetime_column": "LastModifiedDate"
}
data = [
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('Id', 'TEST001'),
('AccountId', 'test001'),
('UserOrGroupId', None),
('AccountAccessLevel', 1),
('OpportunityAccessLevel', 2),
('CaseAccessLevel', 3),
('ContactAccessLevel', 4),
('RowCause', 'テストのため1'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('IsDeleted', False)
]),
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('Id', 'TEST002'),
('AccountId', 'test002'),
('UserOrGroupId', None),
('AccountAccessLevel', 5),
('OpportunityAccessLevel', 6),
('CaseAccessLevel', 7),
('ContactAccessLevel', 8),
('RowCause', 'テストのため2'),
('LastModifiedDate', '2022-06-02T16:30:30.000+0000'),
('LastModifiedById', 2.23E+0),
('IsDeleted', True)
]),
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('Id', 'TEST003'),
('AccountId', 'test003'),
('UserOrGroupId', None),
('AccountAccessLevel', 9),
('OpportunityAccessLevel', 10),
('CaseAccessLevel', 11),
('ContactAccessLevel', 12),
('RowCause', 'テストのため3'),
('LastModifiedDate', '2022-06-03T23:50:50.000+0000'),
('LastModifiedById', 3.234567),
('IsDeleted', False)
])
]
execute_datetime = ExecuteDateTime()
target_object = TargetObject(object_info, execute_datetime)
# Act
csv_string_converter = CSVStringConverter(target_object, data)
actual = csv_string_converter.convert()
# Expects
expected_value = '''\
"Id","AccountId","UserOrGroupId","AccountAccessLevel","OpportunityAccessLevel","CaseAccessLevel","ContactAccessLevel","RowCause","LastModifiedDate","LastModifiedById","IsDeleted"\r\n\
"TEST001","test001","","1","2","3","4","テストのため1","2022-06-01 09:00:00","1234567.0","0"\r\n\
"TEST002","test002","","5","6","7","8","テストのため2","2022-06-03 01:30:30","2.23","1"\r\n\
"TEST003","test003","","9","10","11","12","テストのため3","2022-06-04 08:50:50","3.234567","0"\r\n\
'''
# expected_valueのインデントが半角スペースと認識されてしまうため、`textwrap.dedent`にて補正
assert actual == textwrap.dedent(expected_value)
def test_raise_convert_extract_jsons(self) -> str:
"""
Cases:
一部データのキーattributesの不足によりJSONの成形部分で例外が発生すること
Arranges:
- オブジェクト情報の作成
- データの作成
- 実行日時取得インスタンスの生成
- オブジェクト情報インスタンスの生成
Expects:
例外が期待値と一致すること
"""
# Arranges
object_info = {
"object_name": "AccountShare",
"columns": [
"Id",
"AccountId",
"UserOrGroupId",
"AccountAccessLevel",
"OpportunityAccessLevel",
"CaseAccessLevel",
"ContactAccessLevel",
"RowCause",
"LastModifiedDate",
"LastModifiedById",
"IsDeleted"
],
"is_skip": False,
"is_update_last_fetch_datetime": False,
"last_fetch_datetime_file_name": "AccountShare.json",
"upload_file_name": "CRM_AccountShare_{execute_datetime}",
"datetime_column": "LastModifiedDate"
}
data = [
OrderedDict([
('Id', 'TEST001'),
('AccountId', 'test001'),
('UserOrGroupId', None),
('AccountAccessLevel', 1),
('OpportunityAccessLevel', 2),
('CaseAccessLevel', 3),
('ContactAccessLevel', 4),
('RowCause', 'テストのため1'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('IsDeleted', False)
]),
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('Id', 'TEST002'),
('AccountId', 'test002'),
('UserOrGroupId', None),
('AccountAccessLevel', 5),
('OpportunityAccessLevel', 6),
('CaseAccessLevel', 7),
('ContactAccessLevel', 8),
('RowCause', 'テストのため2'),
('LastModifiedDate', '2022-06-02T16:30:30.000+0000'),
('LastModifiedById', 2.234567E+6),
('IsDeleted', True)
]),
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('Id', 'TEST003'),
('AccountId', 'test003'),
('UserOrGroupId', None),
('AccountAccessLevel', 9),
('OpportunityAccessLevel', 10),
('CaseAccessLevel', 11),
('ContactAccessLevel', 12),
('RowCause', 'テストのため3'),
('LastModifiedDate', '2022-06-03T23:50:50.000+0000'),
('LastModifiedById', 3.234567E+6),
('IsDeleted', False)
])
]
execute_datetime = ExecuteDateTime()
target_object = TargetObject(object_info, execute_datetime)
# Act
csv_string_converter = CSVStringConverter(target_object, data)
with pytest.raises(Exception) as e:
csv_string_converter.convert()
# Expects
assert '必要なjsonのデータ成形に失敗しました' in str(e.value)
def test_raise_convert_convert_to_csv(self) -> str:
"""
Cases:
一部データのカラムIdの不足によりCSV変換で例外が発生すること
Arranges:
- オブジェクト情報の作成
- データの作成
- 実行日時取得インスタンスの生成
- オブジェクト情報インスタンスの生成
Expects:
例外が期待値と一致すること
"""
# Arranges
object_info = {
"object_name": "AccountShare",
"columns": [
"Id",
"AccountId",
"UserOrGroupId",
"AccountAccessLevel",
"OpportunityAccessLevel",
"CaseAccessLevel",
"ContactAccessLevel",
"RowCause",
"LastModifiedDate",
"LastModifiedById",
"IsDeleted"
],
"is_skip": False,
"is_update_last_fetch_datetime": False,
"last_fetch_datetime_file_name": "AccountShare.json",
"upload_file_name": "CRM_AccountShare_{execute_datetime}",
"datetime_column": "LastModifiedDate"
}
data = [
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('AccountId', 'test001'),
('UserOrGroupId', None),
('AccountAccessLevel', 1),
('OpportunityAccessLevel', 2),
('CaseAccessLevel', 3),
('ContactAccessLevel', 4),
('RowCause', 'テストのため1'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('IsDeleted', False)
]),
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('Id', 'TEST002'),
('AccountId', 'test002'),
('UserOrGroupId', None),
('AccountAccessLevel', 5),
('OpportunityAccessLevel', 6),
('CaseAccessLevel', 7),
('ContactAccessLevel', 8),
('RowCause', 'テストのため2'),
('LastModifiedDate', '2022-06-02T16:30:30.000+0000'),
('LastModifiedById', 2.234567E+6),
('IsDeleted', True)
]),
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('Id', 'TEST003'),
('AccountId', 'test003'),
('UserOrGroupId', None),
('AccountAccessLevel', 9),
('OpportunityAccessLevel', 10),
('CaseAccessLevel', 11),
('ContactAccessLevel', 12),
('RowCause', 'テストのため3'),
('LastModifiedDate', '2022-06-03T23:50:50.000+0000'),
('LastModifiedById', 3.234567E+6),
('IsDeleted', False)
])
]
execute_datetime = ExecuteDateTime()
target_object = TargetObject(object_info, execute_datetime)
# Act
csv_string_converter = CSVStringConverter(target_object, data)
with pytest.raises(Exception) as e:
csv_string_converter.convert()
# Expects
assert 'CSV変換に失敗しました カラム名:[Id] 行番号: [1]' in str(e.value)
def test_raise_convert_write_csv_string(self, monkeypatch) -> str:
"""
Cases:
csvデータ出力のCSVデータ取得で例外が発生すること
Arranges:
- オブジェクト情報の作成
- データの作成
- 実行日時取得インスタンスの生成
- オブジェクト情報インスタンスの生成
- csvデータ出力のエラーを発生させるためのモックを準備
Expects:
例外が期待値と一致すること
"""
# Arranges
object_info = {
"object_name": "AccountShare",
"columns": [
"Id",
"AccountId",
"UserOrGroupId",
"AccountAccessLevel",
"OpportunityAccessLevel",
"CaseAccessLevel",
"ContactAccessLevel",
"RowCause",
"LastModifiedDate",
"LastModifiedById",
"IsDeleted"
],
"is_skip": False,
"is_update_last_fetch_datetime": False,
"last_fetch_datetime_file_name": "AccountShare.json",
"upload_file_name": "CRM_AccountShare_{execute_datetime}",
"datetime_column": "LastModifiedDate"
}
data = [
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('Id', 'TEST001'),
('AccountId', 'test001'),
('UserOrGroupId', None),
('AccountAccessLevel', 1),
('OpportunityAccessLevel', 2),
('CaseAccessLevel', 3),
('ContactAccessLevel', 4),
('RowCause', 'テストのため1'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('IsDeleted', False)
]),
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('Id', 'TEST002'),
('AccountId', 'test002'),
('UserOrGroupId', None),
('AccountAccessLevel', 5),
('OpportunityAccessLevel', 6),
('CaseAccessLevel', 7),
('ContactAccessLevel', 8),
('RowCause', 'テストのため2'),
('LastModifiedDate', '2022-06-02T16:30:30.000+0000'),
('LastModifiedById', 2.234567E+6),
('IsDeleted', True)
]),
OrderedDict([
('attributes', OrderedDict([('type', 'AccountShare'), ('url', '/services/data/v1.0/sobjects/AccountShare/test1')])),
('Id', 'TEST003'),
('AccountId', 'test003'),
('UserOrGroupId', None),
('AccountAccessLevel', 9),
('OpportunityAccessLevel', 10),
('CaseAccessLevel', 11),
('ContactAccessLevel', 12),
('RowCause', 'テストのため3'),
('LastModifiedDate', '2022-06-03T23:50:50.000+0000'),
('LastModifiedById', 3.234567E+6),
('IsDeleted', False)
])
]
execute_datetime = ExecuteDateTime()
target_object = TargetObject(object_info, execute_datetime)
def dummy_method(arg):
raise Exception(e)
# データ加工のみだと事前の処理によりエラーとなるため、csv出力モジュールをモック化する
monkeypatch.setattr("csv.writer", dummy_method)
# Act
csv_string_converter = CSVStringConverter(target_object, data)
with pytest.raises(Exception) as e:
csv_string_converter.convert()
# Expects
assert 'CSVデータの出力に失敗しました' in str(e.value)

View File

@ -0,0 +1,45 @@
"""pytest-htmlでレポート出力するため、各テスト関数のドキュメントコメントのセクションを抜き出して、辞書にする
Examples:
<セクション名>:となっている部分が対象になる
\"\"\"
Cases:
テストケース
Arranges:
準備作業
Expects:
期待値
\"\"\"
"""
import re
from itertools import takewhile
_section_rgx = re.compile(r"^\s*[a-zA-Z]+:\s*$")
_lspace_rgx = re.compile(r"^\s*")
def _parse_section(lines: list) -> list:
matches = map(lambda x: _section_rgx.match(x), lines)
indexes = [i for i, x in enumerate(matches) if x is not None]
return list(map(lambda x: (x, lines[x].strip()[: -1]), indexes))
def _count_lspace(s: str) -> int:
rgx = _lspace_rgx.match(s)
if rgx is not None:
return rgx.end()
return 0
def _parse_content(index: int, lines: list) -> str:
lspace = _count_lspace(lines[index])
i = index + 1
contents = takewhile(lambda x: _count_lspace(x) > lspace, lines[i:])
return "\n".join(map(lambda x: x.strip(), contents))
def parse(docstring: str) -> dict:
"""🚧sloppy docstring parser🚧"""
lines = docstring.splitlines()
sections = _parse_section(lines)
return dict(map(lambda x: (x[1], _parse_content(x[0], lines)), sections))

View File

@ -0,0 +1,77 @@
import pytest
from src.parser.json_parser import JsonParser
class TestJsonParser():
def test_parse(self) -> dict:
"""
Cases:
- コメントアウトが記載されているJSONからコメントを取り除き辞書型を返すこと
Arranges:
- JSON文字列を準備する
Expects:
- json.loadsされたファイルの内容が期待値と一致する
"""
# Arranges
json_string = """{
"aaaa": "aaaa",
# これはコメントです
"#これはコメントではありません": "#これはコメントではありません",
"bbb": false,
"hogehoge": [
"ccc",
/これはコメントです
"/これはコメントではありません"
]
}"""
# Act
sut = JsonParser(json_string)
actual = sut.parse()
# Expects
expected_value = {
"aaaa": "aaaa",
"#これはコメントではありません": "#これはコメントではありません",
"bbb": False,
"hogehoge": [
"ccc",
"/これはコメントではありません"
]
}
assert actual == expected_value
def test_raise_parse(self) -> dict:
"""
Cases:
- コメントアウト記号ではない文字をコメントアウトとしたときに例外が発生すること
Arranges:
- JSON文字列を準備する
Expects:
- 例外が発生し期待値と一致する
"""
# Arranges
json_string = """{
"aaaa": "aaaa",
$ これはコメントです
"#これはコメントではありません": "#これはコメントではありません",
"bbb": false,
"hogehoge": [
"ccc",
/これはコメントです
"/これはコメントではありません"
]
}"""
# Act
with pytest.raises(Exception) as e:
sut = JsonParser(json_string)
sut.parse()
# Expects
assert "Expecting property name enclosed in double quotes:" in str(e.value)

View File

@ -0,0 +1,662 @@
"""
注意
当テストコードはSalesforceのレコードに依存しています
Accountオブジェクトの下記SFIDのレコードはいじらないようにしてください
- 0015i00000LNywwAAD(テスト取引先1)
- 0015i00000LOClSAAX(テスト取引先2)
- 0015i00000LOCmGAAX(テスト取引先3)
変更してしまった場合は各SOQLの取得日付とデータを修正してください
"""
from typing import OrderedDict
import pytest
from requests.exceptions import ConnectTimeout, ReadTimeout
from src.config.objects import LastFetchDatetime, TargetObject
from src.salesforce.salesforce_api import SalesforceApiClient
from src.salesforce.soql_builder import SOQLBuilder
from src.util.execute_datetime import ExecuteDateTime
class TestSalesforceApiClient:
def test_fetch_sf_count(self):
"""
Cases:
Salesforceからオブジェクトの件数が取得できること
Arranges:
SalesforceのAccountオブジェクトにレコードを作成する(手作業コード上では行わない)
Expects:
取得件数が1件以上になる
"""
soql = """SELECT
COUNT(Id)
FROM
Account
WHERE
SystemModstamp > 2022-08-23T01:56:39.000Z AND
SystemModstamp <= 2022-08-24T00:00:00.000Z
"""
sut = SalesforceApiClient()
actual = sut.fetch_sf_count(soql)
assert actual >= 1
def test_fetch_sf_count_zero_record(self):
"""
Cases:
取得範囲外の場合Salesforceからオブジェクトの件数が取得できないこと
Arranges:
SalesforceのAccountオブジェクトにレコードを作成する(手作業コード上では行わない)
Expects:
取得件数が0件になる
"""
soql = """SELECT
COUNT(Id)
FROM
Account
WHERE
SystemModstamp > 1999-01-01T00:00:00.000Z AND
SystemModstamp <= 2000-01-01T00:00:00.000Z
"""
sut = SalesforceApiClient()
actual = sut.fetch_sf_count(soql)
assert actual >= 0
def test_fetch_sf_count_by_soql_builder_system_modstamp_to_ge(self):
"""
Cases:
- SOQLBuilderから生成したSOQLでSalesforceからオブジェクトの件数が取得できること
- SystemModStampがFrom指定日付未満のレコードは取得できないこと
- SystemModStampのToが指定日付以上のレコードは取得できること
Arranges:
- SalesforceのAccountオブジェクトにレコードを作成する
- LastFetchDatetimeのFromがSystemModstamp未満になるように指定するUTC指定
- LastFetchDatetimeのToがSystemModstampピッタリになるように指定するUTC指定
Expects:
取得件数が1になる
"""
execute_datetime = ExecuteDateTime()
last_fetch_datetime = LastFetchDatetime({
'last_fetch_datetime_from': '2022-08-23T02:38:59.000Z',
'last_fetch_datetime_to': '2022-08-23T02:39:00.000Z',
}, execute_datetime)
target_object = TargetObject({
'object_name': 'Account',
'columns': [
'Id',
'Name',
'SystemModstamp',
'LastModifiedDate',
'CustomItem1__c',
'CustomItem2__c',
'CustomItem3__c',
'CustomItem4__c',
'CustomItem5__c',
'CustomItem6__c',
'CustomItem7__c',
'CustomItem8__c'
]
}, execute_datetime)
soql_builder = SOQLBuilder(target_object, last_fetch_datetime)
soql = soql_builder.create_count_soql()
sut = SalesforceApiClient()
actual = sut.fetch_sf_count(soql)
assert actual == 1
def test_fetch_sf_count_by_soql_builder_system_modstamp_to_lt(self):
"""
Cases:
- SOQLBuilderから生成したSOQLでSalesforceからオブジェクトの件数が取得できること
- SystemModStampのFromが指定日付のより大きいレコードは取得できること
- SystemModStampのToが指定日付未満のレコードは取得できないこと
Arranges:
- SalesforceのAccountオブジェクトにレコードを作成する
- LastFetchDatetimeのFromがSystemModstampより大きくなるように指定するUTC指定
- LastFetchDatetimeのToがSystemModstamp未満になるように指定するUTC指定
Expects:
取得件数が1になる
"""
execute_datetime = ExecuteDateTime()
last_fetch_datetime = LastFetchDatetime({
'last_fetch_datetime_from': '2022-08-23T02:38:00.000Z',
'last_fetch_datetime_to': '2022-08-23T02:39:01.000Z',
}, execute_datetime)
target_object = TargetObject({
'object_name': 'Account',
'columns': [
'Id',
'Name',
'SystemModstamp',
'LastModifiedDate',
'CustomItem1__c',
'CustomItem2__c',
'CustomItem3__c',
'CustomItem4__c',
'CustomItem5__c',
'CustomItem6__c',
'CustomItem7__c',
'CustomItem8__c'
]
}, execute_datetime)
soql_builder = SOQLBuilder(target_object, last_fetch_datetime)
soql = soql_builder.create_count_soql()
sut = SalesforceApiClient()
actual = sut.fetch_sf_count(soql)
assert actual == 1
def test_fetch_sf_count_by_soql_builder_system_modstamp_all_range(self):
"""
Cases:
- SOQLBuilderから生成したSOQLでSalesforceからオブジェクトの件数が取得できること
- SystemModStampのFromが2000年1月1日以降のレコードが取得できること
- SystemModStampのToが2100年12月31日未満のレコードが取得できること
Arranges:
- SalesforceのAccountオブジェクトにレコードを作成する
- LastFetchDatetimeのFromに2000年1月1日を指定する
- LastFetchDatetimeのToに2100年12月31日を指定する
Expects:
取得件数が17になる
"""
execute_datetime = ExecuteDateTime()
last_fetch_datetime = LastFetchDatetime({
'last_fetch_datetime_from': '2000-01-01T00:00:00.000Z',
'last_fetch_datetime_to': '2100-12-31T23:59:59.000Z',
}, execute_datetime)
target_object = TargetObject({
'object_name': 'Account',
'columns': [
'Id',
'Name',
'SystemModstamp',
'LastModifiedDate',
'CustomItem1__c',
'CustomItem2__c',
'CustomItem3__c',
'CustomItem4__c',
'CustomItem5__c',
'CustomItem6__c',
'CustomItem7__c',
'CustomItem8__c'
]
}, execute_datetime)
soql_builder = SOQLBuilder(target_object, last_fetch_datetime)
soql = soql_builder.create_count_soql()
sut = SalesforceApiClient()
actual = sut.fetch_sf_count(soql)
assert actual == 17
def test_fetch_sf_data_one_record(self):
"""
Cases:
Salesforceからオブジェクトが取得できること
Arranges:
SalesforceのAccountオブジェクトにレコードを作成する(手作業コード上では行わない)
Expects:
オブジェクトが取得でき期待値と一致していること
"""
soql = """SELECT
Id,
Name,
SystemModstamp,
LastModifiedDate,
CustomItem1__c,
CustomItem2__c,
CustomItem3__c,
CustomItem4__c,
CustomItem5__c,
CustomItem6__c,
CustomItem7__c,
CustomItem8__c
FROM
Account
WHERE
SystemModstamp > 2022-08-23T01:56:39.000Z AND
SystemModstamp <= 2022-08-24T00:00:00.000Z
"""
sut = SalesforceApiClient()
expect = {
'Name': 'テスト取引先名1',
'CustomItem1__c': 'テスト',
'CustomItem2__c': 1.0,
'CustomItem3__c': True,
'CustomItem4__c': '01:15:00.000Z',
'CustomItem5__c': '1;2;3;4',
'CustomItem6__c': '改行ありの\r\nテスト\r\n項目です',
'CustomItem7__c': '2022-08-04',
'CustomItem8__c': '2022-08-04T03:00:00.000+0000',
}
actual = sut.fetch_sf_data(soql)
assert len(actual) >= 1
# 複数取れるが、アサーション対象は1つだけ
actual = [record for record in actual if record['Name'] == 'テスト取引先名1']
# Id, SystemModstamp, LastModifiedDateは自動生成なので、キーの有無だけ確認する
# Attributesも
assert 'Id' in actual[0].keys()
assert 'SystemModstamp' in actual[0].keys()
assert 'LastModifiedDate' in actual[0].keys()
assert 'attributes' in actual[0].keys()
del actual[0]['Id']
del actual[0]['SystemModstamp']
del actual[0]['LastModifiedDate']
del actual[0]['attributes']
assert dict(actual[0]) == expect
def test_fetch_sf_data_zero_record(self):
"""
Cases:
取得範囲外の場合Salesforceからオブジェクトが取得できないこと
Arranges:
SalesforceのAccountオブジェクトにレコードを作成する(手作業コード上では行わない)
Expects:
取得件数が0件になる
"""
soql = """SELECT
Id,
Name,
SystemModstamp,
LastModifiedDate,
CustomItem1__c,
CustomItem2__c,
CustomItem3__c,
CustomItem4__c,
CustomItem5__c,
CustomItem6__c,
CustomItem7__c,
CustomItem8__c
FROM
Account
WHERE
SystemModstamp > 1999-01-01T00:00:00.000Z AND
SystemModstamp <= 2000-01-01T00:00:00.000Z
"""
sut = SalesforceApiClient()
actual = sut.fetch_sf_data(soql)
assert len(actual) >= 0
def test_fetch_sf_data_by_soql_builder_system_modstamp_to_ge(self):
"""
Cases:
- SOQLBuilderから生成したSOQLでSalesforceからオブジェクトが取得できること
- SystemModStampがFrom指定日付未満のレコードは取得できないこと
- SystemModStampのToが指定日付以上のレコードは取得できること
Arranges:
- SalesforceのAccountオブジェクトにレコードを作成する
- LastFetchDatetimeのFromがSystemModstamp未満になるように指定するUTC指定
- LastFetchDatetimeのToがSystemModstampピッタリになるように指定するUTC指定
Expects:
取得できたオブジェクト1件が期待値どおりであること
"""
execute_datetime = ExecuteDateTime()
last_fetch_datetime = LastFetchDatetime({
'last_fetch_datetime_from': '2022-08-23T03:48:39.000Z',
'last_fetch_datetime_to': '2022-08-23T03:48:40.000Z',
}, execute_datetime)
target_object = TargetObject({
'object_name': 'Account',
'columns': [
'Id',
'Name',
'SystemModstamp',
'LastModifiedDate',
'CustomItem1__c',
'CustomItem2__c',
'CustomItem3__c',
'CustomItem4__c',
'CustomItem5__c',
'CustomItem6__c',
'CustomItem7__c',
'CustomItem8__c'
]
}, execute_datetime)
soql_builder = SOQLBuilder(target_object, last_fetch_datetime)
soql = soql_builder.create_fetch_soql()
sut = SalesforceApiClient()
actual = sut.fetch_sf_data(soql)
assert len(actual) == 1
# Id, SystemModstamp, LastModifiedDateは自動生成なので、キーの有無だけ確認する
# Attributesも
assert 'Id' in actual[0].keys()
assert 'SystemModstamp' in actual[0].keys()
assert 'LastModifiedDate' in actual[0].keys()
assert 'attributes' in actual[0].keys()
del actual[0]['Id']
del actual[0]['SystemModstamp']
del actual[0]['LastModifiedDate']
del actual[0]['attributes']
expect = {
'Name': 'テスト取引先名3',
'CustomItem1__c': 'テスト3',
'CustomItem2__c': 3.0,
'CustomItem3__c': True,
'CustomItem4__c': '00:45:00.000Z',
'CustomItem5__c': '2;3',
'CustomItem6__c': 'かいぎょ',
'CustomItem7__c': '2022-08-06',
'CustomItem8__c': '2022-08-06T00:00:00.000+0000',
}
assert dict(actual[0]) == expect
def test_fetch_sf_data_by_soql_builder_system_modstamp_to_gt(self):
"""
Cases:
- SOQLBuilderから生成したSOQLでSalesforceからオブジェクトが取得できること
- SystemModStampのFromが指定日付のより大きいレコードは取得できること
- SystemModStampのToが指定日付未満のレコードは取得できないこと
Arranges:
- SalesforceのAccountオブジェクトにレコードを作成する
- LastFetchDatetimeのFromがSystemModstampより小さくなるように指定するUTC指定
- LastFetchDatetimeのToがSystemModstampより大きくなるように指定するUTC指定
Expects:
取得できたオブジェクト1件が期待値どおりであること
"""
execute_datetime = ExecuteDateTime()
last_fetch_datetime = LastFetchDatetime({
'last_fetch_datetime_from': '2022-08-23T03:42:24.000Z',
'last_fetch_datetime_to': '2022-08-23T03:42:26.000Z',
}, execute_datetime)
target_object = TargetObject({
'object_name': 'Account',
'columns': [
'Id',
'Name',
'SystemModstamp',
'LastModifiedDate',
'CustomItem1__c',
'CustomItem2__c',
'CustomItem3__c',
'CustomItem4__c',
'CustomItem5__c',
'CustomItem6__c',
'CustomItem7__c',
'CustomItem8__c'
]
}, execute_datetime)
soql_builder = SOQLBuilder(target_object, last_fetch_datetime)
soql = soql_builder.create_fetch_soql()
sut = SalesforceApiClient()
actual = sut.fetch_sf_data(soql)
assert len(actual) == 1
# Id, SystemModstamp, LastModifiedDateは自動生成なので、キーの有無だけ確認する
# Attributesも
assert 'Id' in actual[0].keys()
assert 'SystemModstamp' in actual[0].keys()
assert 'LastModifiedDate' in actual[0].keys()
assert 'attributes' in actual[0].keys()
del actual[0]['Id']
del actual[0]['SystemModstamp']
del actual[0]['LastModifiedDate']
del actual[0]['attributes']
expect = {
'Name': 'テスト取引先名2',
'CustomItem1__c': 'テスト2',
'CustomItem2__c': 2.0,
'CustomItem3__c': False,
'CustomItem4__c': '00:30:00.000Z',
'CustomItem5__c': '1;4',
'CustomItem6__c': '改行あり項目',
'CustomItem7__c': '2022-08-05',
'CustomItem8__c': '2022-08-04T23:30:00.000+0000',
}
assert dict(actual[0]) == expect
def test_fetch_sf_data_by_soql_builder_address_item_check(self):
"""
Cases:
- SOQLBuilderから生成したSOQLでSalesforceからオブジェクトが取得できること
- できること
Arranges:
- SalesforceのAccountオブジェクトに住所項目を持つレコードを作成する
- 住所項目を持つレコードだけが取れるよう日付を設定する
Expects:
取得できたオブジェクト件数が1件になる
住所項目(BillingAddress)が想定通りの値になっていること
"""
execute_datetime = ExecuteDateTime()
last_fetch_datetime = LastFetchDatetime({
'last_fetch_datetime_from': '2022-08-23T02:38:00.000Z',
'last_fetch_datetime_to': '2022-08-23T02:39:00.000Z',
}, execute_datetime)
target_object = TargetObject({
'object_name': 'Account',
'columns': [
'Id',
'Name',
'SystemModstamp',
'LastModifiedDate',
'BillingStreet',
'BillingCity',
'BillingState',
'BillingPostalCode',
'BillingCountry',
'BillingLatitude',
'BillingLongitude',
'BillingGeocodeAccuracy',
'BillingAddress',
]
}, execute_datetime)
soql_builder = SOQLBuilder(target_object, last_fetch_datetime)
soql = soql_builder.create_fetch_soql()
sut = SalesforceApiClient()
actual = sut.fetch_sf_data(soql)
assert len(actual) == 1
expect_address = OrderedDict([
("city", '〇〇区'),
("country", "日本"),
("geocodeAccuracy", None),
("latitude", None),
("longitude", None),
("postalCode", '999-9999'),
("state", '東京都'),
("street", '△△-✗✗'),
])
assert actual[0]['BillingAddress'] == expect_address
assert actual[0]['BillingCity'] == '〇〇区'
assert actual[0]['BillingCountry'] == '日本'
assert actual[0]['BillingGeocodeAccuracy'] is None
assert actual[0]['BillingLatitude'] is None
assert actual[0]['BillingLongitude'] is None
assert actual[0]['BillingPostalCode'] == '999-9999'
assert actual[0]['BillingState'] == '東京都'
assert actual[0]['BillingStreet'] == '△△-✗✗'
def test_fetch_sf_data_by_soql_builder_system_modstamp_all_range(self):
"""
Cases:
- SOQLBuilderから生成したSOQLでSalesforceからオブジェクトが取得できること
- SystemModStampのFromが2000年1月1日以降のレコードが取得できること
- SystemModStampのToが2100年12月31日未満のレコードが取得できること
Arranges:
- SalesforceのAccountオブジェクトにレコードを作成する
- LastFetchDatetimeのFromに2000年1月1日を指定する
- LastFetchDatetimeのToに2100年12月31日を指定する
Expects:
取得できたオブジェクト件数が17件になる
"""
execute_datetime = ExecuteDateTime()
last_fetch_datetime = LastFetchDatetime({
'last_fetch_datetime_from': '2000-01-01T00:00:00.000Z',
'last_fetch_datetime_to': '2100-12-31T23:59:59.000Z',
}, execute_datetime)
target_object = TargetObject({
'object_name': 'Account',
'columns': [
'Id',
'Name',
'SystemModstamp',
'LastModifiedDate',
'BillingAddress',
'CustomItem1__c',
'CustomItem2__c',
'CustomItem3__c',
'CustomItem4__c',
'CustomItem5__c',
'CustomItem6__c',
'CustomItem7__c',
'CustomItem8__c'
]
}, execute_datetime)
soql_builder = SOQLBuilder(target_object, last_fetch_datetime)
soql = soql_builder.create_fetch_soql()
sut = SalesforceApiClient()
actual = sut.fetch_sf_data(soql)
assert len(actual) == 17
# 内容の確認は別のケースで行っているため省略
def test_raise_create_instance_cause_auth_failed(self, monkeypatch):
"""
Cases:
存在しないユーザを指定した場合エラーが発生すること
Arranges:
CRMのユーザ名を保持する環境変数に存在しないユーザー名を指定する
Expects:
ユーザ認証でエラーが発生すること
"""
monkeypatch.setattr('src.salesforce.salesforce_api.CRM_USER_NAME', 'invalid_username')
with pytest.raises(Exception):
SalesforceApiClient()
def test_raise_fetch_sf_count_auth_timeout(self, monkeypatch):
"""
Cases:
認証タイムアウトが発生した場合エラーが発生すること
Arranges:
認証タイムアウト秒数を保持する環境変数に0.0000000001を指定する
Expects:
コネクションタイムアウトエラーが発生すること
"""
monkeypatch.setattr('src.salesforce.salesforce_api.CRM_AUTH_TIMEOUT', 0.0000000001)
sf = SalesforceApiClient()
soql = """SELECT
COUNT(Id)
FROM
Account
WHERE
SystemModstamp > 2022-08-04T00:00:00.000Z AND
SystemModstamp <= 2022-08-06T00:00:00.000Z
"""
with pytest.raises(ConnectTimeout):
sf.fetch_sf_count(soql)
def test_raise_fetch_sf_count_read_timeout(self, monkeypatch):
"""
Cases:
読み取りタイムアウトが発生した場合エラーが発生すること
Arranges:
CRMの件数取得タイムアウト秒数を保持する環境変数に0.0000000001を指定する
Expects:
読み取りタイムアウトエラーが発生すること
"""
monkeypatch.setattr('src.salesforce.salesforce_api.CRM_GET_RECORD_COUNT_TIMEOUT', 0.0000000001)
sf = SalesforceApiClient()
soql = """SELECT
COUNT(Id)
FROM
Account
WHERE
SystemModstamp > 2022-08-04T00:00:00.000Z AND
SystemModstamp <= 2022-08-06T00:00:00.000Z
"""
with pytest.raises(ReadTimeout):
sf.fetch_sf_count(soql)
def test_raise_fetch_sf_count_invalid_soql(self, monkeypatch):
"""
Cases:
不正なSOQLを指定した場合にエラーが発生すること
Arranges:
不正なSOQLを作成する
Expects:
エラーが発生すること
"""
sf = SalesforceApiClient()
soql = "SELECT"
with pytest.raises(Exception):
sf.fetch_sf_count(soql)
def test_raise_fetch_sf_data_auth_timeout(self, monkeypatch):
"""
Cases:
認証タイムアウトが発生した場合エラーが発生すること
Arranges:
認証タイムアウト秒数を保持する環境変数に0.0000000001を指定する
Expects:
コネクションタイムアウトエラーが発生すること
"""
monkeypatch.setattr('src.salesforce.salesforce_api.CRM_AUTH_TIMEOUT', 0.0000000001)
sf = SalesforceApiClient()
soql = """SELECT
COUNT(Id)
FROM
Account
WHERE
SystemModstamp > 2022-08-04T00:00:00.000Z AND
SystemModstamp <= 2022-08-06T00:00:00.000Z
"""
with pytest.raises(ConnectTimeout):
sf.fetch_sf_data(soql)
def test_raise_fetch_sf_data_read_timeout(self, monkeypatch):
"""
Cases:
読み取りタイムアウトが発生した場合エラーが発生すること
Arranges:
CRMのデータ取得タイムアウト秒数を保持する環境変数に0.0000000001を指定する
Expects:
読み取りタイムアウトエラーが発生すること
"""
monkeypatch.setattr('src.salesforce.salesforce_api.CRM_FETCH_RECORD_TIMEOUT', 0.0000000001)
sf = SalesforceApiClient()
soql = """SELECT
COUNT(Id)
FROM
Account
WHERE
SystemModstamp > 2022-08-04T00:00:00.000Z AND
SystemModstamp <= 2022-08-06T00:00:00.000Z
"""
with pytest.raises(ReadTimeout):
sf.fetch_sf_data(soql)
def test_raise_fetch_sf_data_invalid_soql(self, monkeypatch):
"""
Cases:
不正なSOQLを指定した場合にエラーが発生すること
Arranges:
不正なSOQLを作成する
Expects:
エラーが発生すること
"""
sf = SalesforceApiClient()
soql = "SELECT"
with pytest.raises(Exception):
sf.fetch_sf_data(soql)

View File

@ -0,0 +1,162 @@
import pytest
from src.config.objects import ExecuteDateTime, LastFetchDatetime, TargetObject
from src.salesforce.soql_builder import SOQLBuilder
class TestSOQLBuilder:
def test_create_count_soql(self):
"""
Cases:
件数取得のSOQLが生成できること
Arranges:
SOQL生成用のパラメータを用意する
Expects:
期待値通りのSOQLが生成されること
"""
test_target_object_json = {
'object_name': 'Account',
'columns': [
'Id',
'Name',
'SystemModstamp',
'LastModifiedDate',
'CustomItem1__c',
'CustomItem2__c',
'CustomItem3__c',
'CustomItem4__c',
'CustomItem5__c',
'CustomItem6__c',
'CustomItem7__c',
'CustomItem8__c'
]
}
test_last_fetch_datetime_json = {
'last_fetch_datetime_from': '1999-01-01T00:00:00.000Z',
'last_fetch_datetime_to': '2000-01-01T00:00:00.000Z',
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(test_target_object_json, execute_datetime)
test_last_fetch_datetime = LastFetchDatetime(test_last_fetch_datetime_json, execute_datetime)
sut = SOQLBuilder(target_object, test_last_fetch_datetime)
actual = sut.create_count_soql()
expect = """SELECT
COUNT(Id)
FROM
Account
WHERE
SystemModstamp > 1999-01-01T00:00:00.000Z AND
SystemModstamp <= 2000-01-01T00:00:00.000Z
"""
assert actual.replace('\n', '').replace(' ', '') == expect.replace('\n', '').replace(' ', '')
def test_create_fetch_soql(self):
"""
Cases:
データ取得用のSOQLが生成できること
Arranges:
SOQL生成用のパラメータを用意する
Expects:
期待値通りのSOQLが生成されること
"""
test_target_object_json = {
'object_name': 'Account',
'columns': [
'Id',
'Name',
'SystemModstamp',
'LastModifiedDate',
'CustomItem1__c',
'CustomItem2__c',
'CustomItem3__c',
'CustomItem4__c',
'CustomItem5__c',
'CustomItem6__c',
'CustomItem7__c',
'CustomItem8__c'
]
}
test_last_fetch_datetime_json = {
'last_fetch_datetime_from': '1999-01-01T00:00:00.000Z',
'last_fetch_datetime_to': '2000-01-01T00:00:00.000Z',
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(test_target_object_json, execute_datetime)
test_last_fetch_datetime = LastFetchDatetime(test_last_fetch_datetime_json, execute_datetime)
sut = SOQLBuilder(target_object, test_last_fetch_datetime)
actual = sut.create_fetch_soql()
expect = """SELECT
Id,
Name,
SystemModstamp,
LastModifiedDate,
CustomItem1__c,
CustomItem2__c,
CustomItem3__c,
CustomItem4__c,
CustomItem5__c,
CustomItem6__c,
CustomItem7__c,
CustomItem8__c
FROM
Account
WHERE
SystemModstamp > 1999-01-01T00:00:00.000Z AND
SystemModstamp <= 2000-01-01T00:00:00.000Z
"""
print('actual', actual)
print('expect', expect)
assert actual.replace('\n', '').replace(' ', '') == expect.replace('\n', '').replace(' ', '')
@pytest.mark.skip('データ取得用のSOQLがカラムがない状態は、TargetObjectクラス側で制御されるため、テストを実施しない')
def test_raise_create_fetch_soql_cause_no_columns(self):
"""
Cases:
データ取得用のSOQLがカラムがない状態で生成されること
Arranges:
SOQL生成用のパラメータを用意する
Expects:
取得対象のカラムがないSOQLが生成されること
"""
test_target_object_json = {
'object_name': 'Account',
'columns': []
}
test_last_fetch_datetime_json = {
'last_fetch_datetime_from': '1999-01-01T00:00:00.000Z',
'last_fetch_datetime_to': '2000-01-01T00:00:00.000Z',
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(test_target_object_json, execute_datetime)
test_last_fetch_datetime = LastFetchDatetime(test_last_fetch_datetime_json, execute_datetime)
sut = SOQLBuilder(target_object, test_last_fetch_datetime)
actual = sut.create_fetch_soql()
# TargetObjectのバリデーションで、columnsが空の場合はエラーになるため、本来は発生しない
expect = """SELECT
FROM
Account
WHERE
SystemModstamp > 1999-01-01T00:00:00.000Z AND
SystemModstamp <= 2000-01-01T00:00:00.000Z
"""
print('actual', actual)
print('expect', expect)
assert actual.replace('\n', '').replace(' ', '') == expect.replace('\n', '').replace(' ', '')

View File

@ -0,0 +1,157 @@
import textwrap
from unittest.mock import patch
import pytest
from src.backup_crm_csv_data_process import backup_crm_csv_data_process
from src.config.objects import TargetObject
from src.error.exceptions import FileUploadException
from src.system_var.constants import CSVBK_JP_NAME
from src.util.execute_datetime import ExecuteDateTime
from .test_utils.log_message import generate_log_message_tuple
class TestBackupCrmCsvDataProcess:
@pytest.fixture
def bucket_name(self):
return 'test-backup-bucket'
@pytest.fixture
def prepare_bucket(self, s3_client, bucket_name):
s3_client.create_bucket(Bucket=bucket_name)
yield
def test_run_process_success(self, s3_client, prepare_bucket, bucket_name, monkeypatch, caplog):
"""
Cases:
CSVバックアップ処理が正常終了し期待通りの結果となること
Arranges:
- prepare_bucketフィクスチャでCRM電文データをバックアップするためのモックバケットを作る
- 作成したモックバケットを指すように環境変数を設定する
Expects:
- CSVバックアップファイルがバケットに配置される
- CSVバックアップ処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arrange
csv_string = textwrap.dedent("""\
"Id","AccountNumber","LastModifiedDate","LastModifiedById","SystemModstamp","IsDeleted"
"TEST001","test001","2022-06-01 09:00:00","1234567","2022-06-01 09:00:00","1"
"TEST002","test002","2022-06-01 09:00:00","1234567","2022-06-01 09:00:00","0"
"TEST003","test003","2022-06-01 09:00:00","1234567","2022-06-01 09:00:00","0"
""")
target_object_dict = {
'object_name': 'Account',
'columns': [
'Id',
'AccountNumber',
'LastModifiedDate',
'LastModifiedById',
'SystemModstamp',
'IsDeleted'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_object_dict, execute_datetime)
# 環境変数を編集
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.CRM_IMPORT_DATA_BACKUP_FOLDER', 'data_import')
# Act
backup_crm_csv_data_process(target_object, execute_datetime, csv_string)
# Assert
# ファイル確認
actual = s3_client.get_object(
Bucket=bucket_name,
Key=f'data_import/{execute_datetime.to_path()}/CRM_Account_{execute_datetime.format_date()}.csv')
assert actual['Body'].read().decode('utf-8') == csv_string
# ログの確認
assert generate_log_message_tuple(
log_message=f'I-CSVBK-01 [Account] のCSVデータのバックアップ処理を開始します ファイル名:[CRM_Account_{execute_datetime.format_date()}.csv]'
) in caplog.record_tuples
assert generate_log_message_tuple(
log_message='I-CSVBK-03 [Account] のCSVデータのバックアップ処理を終了します') in caplog.record_tuples
def test_call_depended_modules(self):
"""
Cases:
CSVバックアップ処理内で依存しているモジュールが正しく呼ばれていること
Arranges:
- CSVバックアップ処理の依存モジュールをモック化する
Expects:
- 依存しているモジュールが正しく呼ばれている
"""
# Arrange
target_object_dict = {
'object_name': 'Account',
'columns': [
'Id',
'AccountNumber',
'LastModifiedDate',
'LastModifiedById',
'SystemModstamp',
'IsDeleted'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_object_dict, execute_datetime)
with patch('src.backup_crm_csv_data_process.BackupBucket') as mock_backup_bucket:
mock_backup_bucket_inst = mock_backup_bucket.return_value
mock_backup_bucket_inst.put_csv.return_value = ''
# Act
backup_crm_csv_data_process(target_object, execute_datetime, '')
# Assert
assert mock_backup_bucket_inst.put_csv.called is True
def test_raise_put_csv(self, bucket_name, monkeypatch, caplog):
"""
Cases:
CSVデータをバックアップできない場合エラーが発生すること
Arranges:
- CSVデータをバックアップ処理で例外が発生するようにする
Expects:
- 例外が発生する
- ファイル登録できないエラーが返却される
"""
# Arrange
target_object_dict = {
'object_name': 'Account',
'columns': [
'Id',
'AccountNumber',
'LastModifiedDate',
'LastModifiedById',
'SystemModstamp',
'IsDeleted'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_object_dict, execute_datetime)
with patch('src.backup_crm_csv_data_process.BackupBucket') as mock_backup_bucket:
mock_backup_bucket_inst = mock_backup_bucket.return_value
mock_backup_bucket_inst.put_csv.side_effect = Exception('登録エラー')
# Act
with pytest.raises(FileUploadException) as e:
backup_crm_csv_data_process(target_object, execute_datetime, '')
# Assert
assert mock_backup_bucket_inst.put_csv.called is True
assert e.value.error_id == 'E-CSVBK-01'
assert e.value.func_name == CSVBK_JP_NAME
assert e.value.args[0] == \
f'[Account] CSVデータのバックアップに失敗しました ファイル名:[CRM_Account_{execute_datetime.format_date()}.csv] エラー内容:[登録エラー]'

View File

@ -0,0 +1,186 @@
import json
from collections import OrderedDict
from unittest.mock import patch
import pytest
from src.backup_crm_data_process import backup_crm_data_process
from src.config.objects import TargetObject
from src.error.exceptions import FileUploadException
from src.system_var.constants import RESBK_JP_NAME
from src.util.execute_datetime import ExecuteDateTime
from .test_utils.log_message import generate_log_message_tuple
class TestBackupCrmDataProcess:
@pytest.fixture
def bucket_name(self):
return 'test-backup-bucket'
@pytest.fixture
def prepare_bucket(self, s3_client, bucket_name):
s3_client.create_bucket(Bucket=bucket_name)
yield
def test_run_process_success(self, s3_client, prepare_bucket, bucket_name, monkeypatch, caplog):
"""
Cases:
CRM電文データバックアップ処理が正常終了し期待通りの結果となること
Arranges:
- prepare_bucketフィクスチャでCRM電文データをバックアップするためのモックバケットを作る
- 作成したモックバケットを指すように環境変数を設定する
Expects:
- CRM電文データバックアップファイルがバケットに配置される
- CRM電文データバックアップ処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arrange
response_json = [
OrderedDict([
('attributes', OrderedDict([('type', 'Account'),
('url', '/services/data/v1.0/sobjects/Account/TEST001')])),
('Id', 'TEST001'),
('AccountNumber', 'test001'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('SystemModstamp', '2022-06-01T00:00:00.000+0000'),
('IsDeleted', False),
('Name', 'テスト取引先1')
]),
OrderedDict([
('attributes', OrderedDict([('type', 'Account'),
('url', '/services/data/v1.0/sobjects/Account/TEST002')])),
('Id', 'TEST002'),
('AccountNumber', 'test002'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('SystemModstamp', '2022-06-01T00:00:00.000+0000'),
('IsDeleted', False),
('Name', 'テスト取引先2')
]),
OrderedDict([
('attributes', OrderedDict([('type', 'Account'),
('url', '/services/data/v1.0/sobjects/Account/TEST002')])),
('Id', 'TEST002'),
('AccountNumber', 'test002'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('SystemModstamp', '2022-06-01T00:00:00.000+0000'),
('IsDeleted', False),
('Name', 'テスト取引先3')
]),
]
target_object_dict = {
'object_name': 'Account',
'columns': [
'Id',
'AccountNumber',
'LastModifiedDate',
'LastModifiedById',
'SystemModstamp',
'IsDeleted'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_object_dict, execute_datetime)
# 環境変数を編集
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.RESPONSE_JSON_BACKUP_FOLDER', 'response_json')
# Act
backup_crm_data_process(target_object, response_json, execute_datetime)
# Assert
# ファイル確認
actual = s3_client.get_object(
Bucket=bucket_name,
Key=f'response_json/{execute_datetime.to_path()}/CRM_Account_{execute_datetime.format_date()}.json')
assert actual['Body'].read().decode('utf-8') == json.dumps(response_json, ensure_ascii=False)
# ログの確認
assert generate_log_message_tuple(
log_message='I-RESBK-01 [Account] のCRM電文データバックアップ処理を開始します') in caplog.record_tuples
assert generate_log_message_tuple(
log_message='I-RESBK-03 [Account] のCRM電文データバックアップ処理を終了します') in caplog.record_tuples
def test_call_depended_modules(self):
"""
Cases:
CRM電文データバックアップ処理内で依存しているモジュールが正しく呼ばれていること
Arranges:
- CRM電文データバックアップ処理の依存モジュールをモック化する
Expects:
- 依存しているモジュールが正しく呼ばれている
"""
# Arrange
target_object_dict = {
'object_name': 'Account',
'columns': [
'Id',
'AccountNumber',
'LastModifiedDate',
'LastModifiedById',
'SystemModstamp',
'IsDeleted'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_object_dict, execute_datetime)
with patch('src.backup_crm_data_process.BackupBucket') as mock_backup_bucket:
mock_backup_bucket_inst = mock_backup_bucket.return_value
mock_backup_bucket_inst.put_response_json.return_value = ''
# Act
backup_crm_data_process(target_object, {}, execute_datetime)
# Assert
assert mock_backup_bucket_inst.put_response_json.called is True
def test_raise_put_response_json(self, bucket_name, monkeypatch, caplog):
"""
Cases:
CRM電文データをS3に配置できない場合エラーが発生すること
Arranges:
- オブジェクト情報ファイル取得処理で例外が発生するようにする
Expects:
- 例外が発生する
- ファイル登録できないエラーが返却される
"""
# Arrange
target_object_dict = {
'object_name': 'Account',
'columns': [
'Id',
'AccountNumber',
'LastModifiedDate',
'LastModifiedById',
'SystemModstamp',
'IsDeleted'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_object_dict, execute_datetime)
with patch('src.backup_crm_data_process.BackupBucket') as mock_backup_bucket:
mock_backup_bucket_inst = mock_backup_bucket.return_value
mock_backup_bucket_inst.put_response_json.side_effect = Exception('登録エラー')
# Act
with pytest.raises(FileUploadException) as e:
backup_crm_data_process(target_object, {}, execute_datetime)
# Assert
assert mock_backup_bucket_inst.put_response_json.called is True
assert e.value.error_id == 'E-RESBK-01'
assert e.value.func_name == RESBK_JP_NAME
assert e.value.args[0] == \
f'[Account] 電文データのバックアップに失敗しました ファイル名:[CRM_Account_{execute_datetime.format_date()}.json] エラー内容:[登録エラー]'

View File

@ -0,0 +1,97 @@
from unittest.mock import MagicMock, patch
import pytest
from src.check_object_info_process import check_object_info_process
from src.config.objects import TargetObject
from src.error.exceptions import InvalidConfigException
from src.system_var.constants import CHK_JP_NAME
from src.util.execute_datetime import ExecuteDateTime
from .test_utils.log_message import generate_log_message_tuple
class TestCheckObjectInfoProcess:
def test_run_process_success(self, caplog):
"""
Cases:
オブジェクト情報形式チェック処理が正常終了し期待通りの結果が返ること
Arranges:
- チェック対象のdictオブジェクトを宣言する
Expects:
- チェック後のオブジェクト情報コレクションクラスのインスタンスが返却される
- オブジェクト情報形式チェック処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arrange
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
]
}
execute_datetime = ExecuteDateTime()
# Act
actual_fetch_target_objects = check_object_info_process(target_objects_dict, execute_datetime)
# Assert
# 返り値の期待値チェック
assert isinstance(actual_fetch_target_objects, TargetObject), 'CRM取得オブジェクトクラスのインスタンスが返却される'
assert actual_fetch_target_objects.object_name == 'Account'
assert actual_fetch_target_objects.columns == ['Id', 'Name']
assert actual_fetch_target_objects.is_skip is False
assert actual_fetch_target_objects.is_update_last_fetch_datetime is True
assert actual_fetch_target_objects.datetime_column == 'SystemModstamp'
assert actual_fetch_target_objects.upload_file_name == f'CRM_Account_{execute_datetime.format_date()}'
assert actual_fetch_target_objects.last_fetch_datetime_file_name == 'Account.json'
# ログの確認
assert generate_log_message_tuple(log_message='I-CHK-01 オブジェクト情報形式チェック処理を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-CHK-02 オブジェクト情報形式チェック処理を終了します') in caplog.record_tuples
def test_call_depended_modules(self):
"""
Cases:
オブジェクト情報形式チェック処理内で依存しているモジュールが正しく呼ばれていること
Arranges:
- オブジェクト情報形式チェック処理の依存モジュールをモック化する
Expects:
- 依存しているモジュールが正しく呼ばれている
"""
# Arrange
mock_target_object_init = MagicMock(return_value=None)
# Act
with patch('src.config.objects.TargetObject.__init__', mock_target_object_init):
check_object_info_process({}, {})
# Assert
assert mock_target_object_init.called is True
def test_raise_check_object_info(self):
"""
Cases:
オブジェクト形式チェック処理でエラーが発生した場合検査例外が発生すること
Arranges:
- オブジェクト形式チェック処理で例外が発生するようにする
Expects:
- 例外が発生する
- 形式チェックが失敗したエラーが返却される
"""
# Arrange
mock_target_object_init = MagicMock(side_effect=Exception('形式チェックエラー'))
# Act
with patch('src.config.objects.TargetObject.__init__', mock_target_object_init):
with pytest.raises(InvalidConfigException) as e:
check_object_info_process({}, {})
# Assert
assert mock_target_object_init.called is True
assert e.value.error_id == 'E-CHK-01'
assert e.value.func_name == CHK_JP_NAME
assert e.value.args[0] == f'オブジェクト情報形式チェック処理が失敗しました エラー内容:[形式チェックエラー]'

View File

@ -0,0 +1,905 @@
import logging
from copy import deepcopy
from unittest.mock import MagicMock, patch
import pytest
from src import controller
from src.config.objects import (FetchTargetObjects, LastFetchDatetime,
TargetObject)
from src.error.exceptions import MeDaCaCRMDataFetchException
from src.system_var.constants import (CHK_JP_NAME, CONV_JP_NAME, CSVBK_JP_NAME,
DATE_JP_NAME, END_JP_NAME, FETCH_JP_NAME,
PRE_JP_NAME, RESBK_JP_NAME, UPD_JP_NAME,
UPLD_JP_NAME)
from src.util.execute_datetime import ExecuteDateTime
from .test_utils.log_message import generate_log_message_tuple
COMMON_OBJECT_INFOS = {
'objects': [
{
'object_name': 'Account',
'columns': ['Id'],
'upload_file_name': 'Account_YYYYMMDDHHMMSS'
},
{
'object_name': 'Contact',
'columns': ['Id'],
'upload_file_name': 'Contact_YYYYMMDDHHMMSS'
},
{
'object_name': 'Call2_vod__c',
'columns': ['Id'],
'upload_file_name': 'Call2_vod__c_YYYYMMDDHHMMSS'
}
]
}
COMMON_EXECUTE_DATETIME = ExecuteDateTime()
COMMON_FETCH_TARGET_OBJECTS = FetchTargetObjects(COMMON_OBJECT_INFOS)
COMMON_TARGET_OBJECTS_1 = TargetObject(COMMON_OBJECT_INFOS['objects'][0], COMMON_EXECUTE_DATETIME)
COMMON_TARGET_OBJECTS_2 = TargetObject(COMMON_OBJECT_INFOS['objects'][1], COMMON_EXECUTE_DATETIME)
COMMON_TARGET_OBJECTS_3 = TargetObject(COMMON_OBJECT_INFOS['objects'][2], COMMON_EXECUTE_DATETIME)
COMMON_LAST_FETCH_DATETIME = LastFetchDatetime({
'last_fetch_datetime_from': '1900-01-01T00:00:00.000Z',
'last_fetch_datetime_to': ''
}, COMMON_EXECUTE_DATETIME)
class ForTestMeDaCaCRMDataFetchException(MeDaCaCRMDataFetchException):
def __init__(self, error_id: str, func_name: str, message: str) -> None:
super().__init__(error_id, func_name, message)
class ForTestException(Exception):
# カスタム例外とインタフェースを合わせるための引数
def __init__(self, error_id: str, func_name: str, message: str) -> None:
super().__init__(message)
class TestController:
@pytest.fixture(autouse=True)
def setup_teardown(self):
# setup
self.mock_prepare_data_fetch_process = MagicMock()
self.mock_check_object_info_process = MagicMock()
self.mock_set_datetime_period_process = MagicMock()
self.mock_fetch_crm_data_process = MagicMock()
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
# run test
yield
# teardown
self.mock_prepare_data_fetch_process.reset_mock()
self.mock_check_object_info_process.reset_mock()
self.mock_set_datetime_period_process.reset_mock()
self.mock_fetch_crm_data_process.reset_mock()
self.mock_backup_crm_data_process.reset_mock()
self.mock_convert_crm_csv_data_process.reset_mock()
self.mock_backup_crm_csv_data_process.reset_mock()
self.mock_copy_crm_csv_data_process.reset_mock()
self.mock_upload_last_fetch_datetime_process.reset_mock()
self.mock_upload_result_data_process.reset_mock()
@pytest.fixture()
def run_control_process(self):
def _func():
with patch('src.controller.prepare_data_fetch_process', self.mock_prepare_data_fetch_process),\
patch('src.controller.check_object_info_process', self.mock_check_object_info_process),\
patch('src.controller.set_datetime_period_process', self.mock_set_datetime_period_process),\
patch('src.controller.fetch_crm_data_process', self.mock_fetch_crm_data_process),\
patch('src.controller.backup_crm_data_process', self.mock_backup_crm_data_process),\
patch('src.controller.convert_crm_csv_data_process', self.mock_convert_crm_csv_data_process),\
patch('src.controller.backup_crm_csv_data_process', self.mock_backup_crm_csv_data_process),\
patch('src.controller.copy_crm_csv_data_process', self.mock_copy_crm_csv_data_process),\
patch('src.controller.upload_last_fetch_datetime_process', self.mock_upload_last_fetch_datetime_process),\
patch('src.controller.upload_result_data_process', self.mock_upload_result_data_process):
controller.controller()
yield _func
@pytest.fixture()
def call_all_processes(self, caplog, run_control_process):
"""
コントロール処理内ですべてのプロセス関数が呼ばれることのテストで使用するフィクスチャ
各種プロセス関数をモック化し正常終了させるように動く
Yields:
dict: プロセス関数呼び出し後のモックの辞書
"""
def _func():
mock_return_values = [COMMON_TARGET_OBJECTS_1, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_3]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock(return_value=COMMON_LAST_FETCH_DATETIME)
self.mock_fetch_crm_data_process = MagicMock(return_value=[{'Name': 'Test'}])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
yield _func
def test_call_all_processes(self, call_all_processes):
"""
Cases:
コントロール処理からすべてのプロセスが呼ばれること
Arranges:
- call_all_processesフィクスチャでコントロール処理を実行しておく
Expects:
- データ取得準備処理が1回のみ実行される
- オブジェクト情報形式チェック処理が複数回実行される
- データ取得期間設定処理が複数回実行される
- CRMデータ取得処理が複数回実行される
- CRM電文データバックアップ処理が複数回実行される
- CSV変換処理が複数回実行される
- CSVバックアップ処理が複数回実行される
- CSVアップロード処理が複数回実行される
- 前回取得日時ファイル更新処理が複数回実行される
- 取得処理実施結果アップロード処理が1回のみ実行される
"""
call_all_processes()
assert self.mock_prepare_data_fetch_process.called is True, 'データ取得準備処理が1回のみ実行されること'
assert self.mock_prepare_data_fetch_process.call_count == 1, 'データ取得準備処理が1回のみ実行されること'
assert self.mock_check_object_info_process.call_count == 3, 'オブジェクト情報形式チェック処理が複数回実行されること'
assert self.mock_set_datetime_period_process.call_count == 3, 'データ取得期間設定処理が複数回実行されること'
assert self.mock_fetch_crm_data_process.call_count == 3, 'CRMデータ取得処理が複数回実行されること'
assert self.mock_backup_crm_data_process.call_count == 3, 'CRM電文データバックアップ処理が複数回実行されること'
assert self.mock_convert_crm_csv_data_process.call_count == 3, 'CSV変換処理が複数回実行されること'
assert self.mock_backup_crm_csv_data_process.call_count == 3, 'CSVバックアップ処理が複数回実行されること'
assert self.mock_copy_crm_csv_data_process.call_count == 3, 'CSVアップロード処理が複数回実行されること'
assert self.mock_upload_last_fetch_datetime_process.call_count == 3, '前回取得日時ファイル更新処理が複数回実行されること'
assert self.mock_upload_result_data_process.called is True, '取得処理実施結果アップロード処理が1回のみ実行されること'
assert self.mock_upload_result_data_process.call_count == 1, '取得処理実施結果アップロード処理が1回のみ実行されること'
def test_print_normal_logs(self, call_all_processes, caplog):
"""
Cases:
コントロール処理の正常系ログがすべての出力されていること
Arranges:
- call_all_processesフィクスチャでコントロール処理を実行しておく
Expects:
- コントロール処理の正常系ログがすべて出力されている
"""
call_all_processes()
assert generate_log_message_tuple(log_message='I-CTRL-01 CRMデータ取得処理を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-CTRL-02 データ取得準備処理呼び出し') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-CTRL-03 取得対象オブジェクトのループ処理開始') in caplog.record_tuples
for name in ['Account', 'Contact', 'Call2_vod__c']:
object_name = name
upload_file_name = f'{name}_YYYYMMDDHHMMSS'
assert generate_log_message_tuple(log_message='I-CTRL-05 オブジェクト情報形式チェック処理呼び出し') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-06 [{object_name}]のデータ取得を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-08 [{object_name}]のデータ取得期間設定処理呼び出し') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-09 [{object_name}]のデータ取得処理呼び出し') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-10 [{object_name}] の出力ファイル名は [{upload_file_name}] となります') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-11 [{object_name}] CRM電文データバックアップ処理呼び出し') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-12 [{object_name}] CSV変換処理呼び出し') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-13 [{object_name}] CSVデータバックアップ処理呼び出し') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-14 [{object_name}] CSVデータアップロード処理呼び出し') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-15 [{object_name}] 前回取得日時ファイル更新処理呼び出し') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-16 [{object_name}] 処理正常終了') in caplog.record_tuples
expect_process_result = {
'Account': 'success',
'Contact': 'success',
'Call2_vod__c': 'success'
}
assert generate_log_message_tuple(log_message=f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{expect_process_result}]') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-18 CRM_取得処理実施結果ファイルアップロード処理開始') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-19 すべてのデータの取得に成功しました') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-20 CRMデータ取得処理を終了します') in caplog.record_tuples
def test_do_not_call_upload_process_result_process(self, caplog, run_control_process):
"""
Cases:
処理対象オブジェクトが0件の場合取得処理実施結果アップロード処理が実行されないこと
Arranges:
- データ取得準備処理で返される取得対象オブジェクト情報を0件にする
- 各種プロセスメソッドと内部で使用している値オブジェクトをモック化する
Expects:
- データ取得準備処理が1回のみ実行されること
- 取得処理実施結果アップロード処理が実行されないこと
- 処理対象が存在しない旨を示すログメッセージが出力されていること
"""
self.mock_prepare_data_fetch_process = MagicMock(return_value=([], COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock()
self.mock_set_datetime_period_process = MagicMock()
self.mock_fetch_crm_data_process = MagicMock()
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
# 実行回数の確認
assert self.mock_prepare_data_fetch_process.called, 'データ取得準備処理が1回のみ実行されること'
assert self.mock_prepare_data_fetch_process.call_count == 1, 'データ取得準備処理が1回のみ実行されること'
assert self.mock_upload_result_data_process.called is False, '取得処理実施結果アップロード処理が実行されないこと'
assert self.mock_upload_result_data_process.call_count == 0, '取得処理実施結果アップロード処理が実行されないこと'
# ログ出力の確認
assert generate_log_message_tuple(log_message='I-CTRL-21 処理対象のデータが存在しませんでした') in caplog.record_tuples, '処理対象が存在しない旨を示すログメッセージが出力されていること'
assert generate_log_message_tuple(log_message=f'I-CTRL-20 CRMデータ取得処理を終了します') in caplog.record_tuples
def test_do_not_call_upload_csv_process_cause_is_skip_true(self, caplog, run_control_process):
"""
Cases:
オブジェクト情報.is_skipがTrueの場合CSVアップロード処理が実行されないこと
Arranges:
- データ取得準備処理で返される取得対象オブジェクト情報のis_skipをTrueにする
- 各種プロセスメソッドと内部で使用している値オブジェクトをモック化する
Expects:
- オブジェクト情報形式チェック処理以降のプロセスが実行されないこと
- 処理をスキップする旨を示すログメッセージが出力されていること
"""
mock_check_object_info = {
'object_name': 'Account',
'columns': ['id'],
'is_skip': True
}
mock_return_values = [TargetObject(mock_check_object_info, COMMON_EXECUTE_DATETIME)]
self.mock_prepare_data_fetch_process = MagicMock(return_value=([mock_check_object_info], COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock()
self.mock_fetch_crm_data_process = MagicMock()
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
# 実行回数の確認
assert self.mock_check_object_info_process.called is True
assert self.mock_check_object_info_process.call_count == 1
# オブジェクト情報形式チェック処理以降のプロセスが実行されないこと
assert self.mock_set_datetime_period_process.call_count == 0
assert self.mock_fetch_crm_data_process.call_count == 0
assert self.mock_backup_crm_data_process.call_count == 0
assert self.mock_convert_crm_csv_data_process.call_count == 0
assert self.mock_backup_crm_csv_data_process.call_count == 0
assert self.mock_copy_crm_csv_data_process.call_count == 0
assert self.mock_upload_last_fetch_datetime_process.call_count == 0
# 結果ファイルの出力は行う
assert self.mock_upload_result_data_process.called is True
assert self.mock_upload_result_data_process.call_count == 1
# ログ出力の確認
assert generate_log_message_tuple(log_message='I-CTRL-07 [Account]のデータ取得処理をスキップします') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-20 CRMデータ取得処理を終了します') in caplog.record_tuples
def test_do_not_call_upload_csv_process_cause_is_skip_true_in_loop(self, caplog, run_control_process):
"""
Cases:
オブジェクト情報.is_skipがTrueのものが含まれる場合対象オブジェクトのCSVアップロード処理が実行されないこと
Arranges:
- データ取得準備処理で返される取得対象オブジェクト情報のうちつ目をis_skipをTrueにする
- 各種プロセスメソッドと内部で使用している値オブジェクトをモック化する
Expects:
- オブジェクト情報形式チェック処理が2回実行されること
- 処理をスキップする旨を示すログメッセージが出力されていること
- オブジェクト情報.is_skipがFalseのものはCSVアップロード処理のログメッセージが出力されていること
"""
mock_check_object_infos = {
'objects': [
{
'object_name': 'Account',
'columns': ['id'],
'is_skip': False
},
{
'object_name': 'Contact',
'columns': ['id'],
'is_skip': True
},
{
'object_name': 'Call2_vod__c',
'columns': ['id'],
'is_skip': False
}
]
}
FetchTargetObjects(mock_check_object_infos)
mock_return_values = [
TargetObject(mock_check_object_infos['objects'][0], COMMON_EXECUTE_DATETIME),
TargetObject(mock_check_object_infos['objects'][1], COMMON_EXECUTE_DATETIME),
TargetObject(mock_check_object_infos['objects'][2], COMMON_EXECUTE_DATETIME),
]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(FetchTargetObjects(mock_check_object_infos), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock()
self.mock_fetch_crm_data_process = MagicMock(side_effect=[[{"Name": "Test"}], [{"Name": "Test"}]])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
# 実行回数の確認
assert self.mock_prepare_data_fetch_process.called is True
assert self.mock_prepare_data_fetch_process.call_count == 1
assert self.mock_check_object_info_process.call_count == 3
# オブジェクト情報形式チェック処理以降のプロセスが実行されないこと
assert self.mock_set_datetime_period_process.call_count == 2
assert self.mock_fetch_crm_data_process.call_count == 2
assert self.mock_backup_crm_data_process.call_count == 2
assert self.mock_convert_crm_csv_data_process.call_count == 2
assert self.mock_backup_crm_csv_data_process.call_count == 2
assert self.mock_copy_crm_csv_data_process.call_count == 2
assert self.mock_upload_last_fetch_datetime_process.call_count == 2
assert self.mock_upload_result_data_process.called is True
assert self.mock_upload_result_data_process.call_count == 1
# ログ出力の確認
assert generate_log_message_tuple(
log_message='I-CTRL-14 [Account] CSVデータアップロード処理呼び出し') in caplog.record_tuples, 'オブジェクト情報.is_skipがFalseのものはCSVアップロード処理のログメッセージが出力されていること'
assert generate_log_message_tuple(
log_message='I-CTRL-07 [Contact]のデータ取得処理をスキップします') in caplog.record_tuples, '処理をスキップする旨を示すログメッセージが出力されていること'
assert generate_log_message_tuple(
log_message='I-CTRL-14 [Call2_vod__c] CSVデータアップロード処理呼び出し'
) in caplog.record_tuples, 'オブジェクト情報.is_skipがFalseのものはCSVアップロード処理のログメッセージが出力されていること'
assert generate_log_message_tuple(log_message=f'I-CTRL-20 CRMデータ取得処理を終了します') in caplog.record_tuples
def test_do_not_call_upload_csv_process_cause_crm_data_empty(self, caplog, run_control_process):
"""
Cases:
CRMデータ取得処理で取得できた件数が0件の場合CSVアップロード処理が実行されないこと
Arranges:
- CRMデータ取得処理で返されるオブジェクトを空のリストにする
- 各種プロセスメソッドと内部で使用している値オブジェクトをモック化する
Expects:
- CRM電文データバックアップ処理以降のプロセスが実行されないこと
- 処理をスキップする旨を示すログメッセージが出力されていること
"""
mock_check_object_infos = {
'objects': [
{
'object_name': 'Account',
'columns': ['id'],
'is_skip': False
}
]
}
FetchTargetObjects(mock_check_object_infos)
mock_return_values = [
TargetObject(mock_check_object_infos['objects'][0], COMMON_EXECUTE_DATETIME)
]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(FetchTargetObjects(mock_check_object_infos), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock()
self.mock_fetch_crm_data_process = MagicMock(return_value=[])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
# 実行回数の確認
assert self.mock_prepare_data_fetch_process.called is True
assert self.mock_prepare_data_fetch_process.call_count == 1
assert self.mock_check_object_info_process.call_count == 1
assert self.mock_set_datetime_period_process.call_count == 1
assert self.mock_fetch_crm_data_process.call_count == 1
# CRM電文データバックアップ処理以降のプロセスが実行されないこと
assert self.mock_backup_crm_data_process.call_count == 0
assert self.mock_convert_crm_csv_data_process.call_count == 0
assert self.mock_backup_crm_csv_data_process.call_count == 0
assert self.mock_copy_crm_csv_data_process.call_count == 0
assert self.mock_upload_last_fetch_datetime_process.call_count == 0
assert self.mock_upload_result_data_process.called is True
assert self.mock_upload_result_data_process.call_count == 1
# ログ出力の確認
assert generate_log_message_tuple(
log_message='I-CTRL-22 [Account]のレコード件数が0件のため、ファイルアップロードをスキップします') in caplog.record_tuples, '処理をスキップする旨を示すログメッセージが出力されていること'
def test_do_not_call_upload_csv_process_cause_crm_data_empty_in_loop(self, caplog, run_control_process):
"""
Cases:
CRMデータ取得処理で取得できた件数が0件のものが含まれる場合対象オブジェクトのCSVアップロード処理が実行されないこと
Arranges:
- CRMデータ取得処理で返されるオブジェクトのうちつ目を空のリストにする
- 各種プロセスメソッドと内部で使用している値オブジェクトをモック化する
Expects:
- CRMデータ取得処理が2回実行されること
- 処理をスキップする旨を示すログメッセージが出力されていること
- 取得オブジェクトが1件以上取れているものはCSVアップロード処理のログメッセージが出力されていること
"""
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=[COMMON_TARGET_OBJECTS_1, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_3])
self.mock_set_datetime_period_process = MagicMock(return_value=COMMON_LAST_FETCH_DATETIME)
self.mock_fetch_crm_data_process = MagicMock(side_effect=[[{"Name": "Test"}], [], [{"Name": "Test"}]])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
# 実行回数の確認
assert self.mock_check_object_info_process.call_count == 3
assert self.mock_set_datetime_period_process.call_count == 3
assert self.mock_fetch_crm_data_process.call_count == 3
# CRM電文データバックアップ処理以降のプロセスは件数があるもののみ実行されること
assert self.mock_backup_crm_data_process.call_count == 2
assert self.mock_convert_crm_csv_data_process.call_count == 2
assert self.mock_backup_crm_csv_data_process.call_count == 2
assert self.mock_copy_crm_csv_data_process.call_count == 2
assert self.mock_upload_last_fetch_datetime_process.call_count == 2
assert self.mock_upload_result_data_process.called is True
assert self.mock_upload_result_data_process.call_count == 1
# ログ出力の確認
assert generate_log_message_tuple(
log_message='I-CTRL-14 [Account] CSVデータアップロード処理呼び出し') in caplog.record_tuples, '取得オブジェクトが1件以上取れているものはCSVアップロード処理のログメッセージが出力されていること'
assert generate_log_message_tuple(
log_message='I-CTRL-22 [Contact]のレコード件数が0件のため、ファイルアップロードをスキップします') in caplog.record_tuples, '処理をスキップする旨を示すログメッセージが出力されていること'
assert generate_log_message_tuple(
log_message='I-CTRL-14 [Call2_vod__c] CSVデータアップロード処理呼び出し'
) in caplog.record_tuples, '取得オブジェクトが1件以上取れているものはCSVアップロード処理のログメッセージが出力されていること'
@pytest.mark.parametrize(
'exception, message',
[
(ForTestMeDaCaCRMDataFetchException, f'E-ERR-01 [{PRE_JP_NAME}]でエラーが発生したため、処理を終了します'),
(ForTestException, 'E-ERR-02 予期せぬエラーが発生したため、処理を終了します エラー内容: [例外発生]')
])
def test_raise_prepare_data_fetch_process(self, caplog, exception, message, run_control_process):
"""
Cases:
1. データ取得準備処理でシステム例外が発生した場合エラーで終了すること
2. データ取得準備処理で想定外の例外が発生した場合エラーで終了すること
Arranges:
- パラメータ1データ取得準備処理でシステム例外が発生するようにする
- パラメータ2データ取得準備処理で想定外の例外が発生するようにする
Expects:
- データ取得準備処理で例外が発生すること
- データ取得準備処理で発生した例外のログメッセージが出力されていること
"""
expect_exception = exception('E-PRE-01', PRE_JP_NAME, '例外発生')
self.mock_prepare_data_fetch_process = MagicMock(side_effect=expect_exception)
self.mock_check_object_info_process = MagicMock()
with pytest.raises(exception):
run_control_process()
assert self.mock_check_object_info_process.called is False
assert generate_log_message_tuple(
log_level=logging.ERROR,
log_message=message) in caplog.record_tuples, 'データ取得準備処理で発生した例外のログメッセージが出力されていること'
@pytest.mark.parametrize(
'exception, message',
[
(ForTestMeDaCaCRMDataFetchException, f'I-ERR-03 [Account] の[{CHK_JP_NAME}]でエラーが発生しました 次のオブジェクトの処理に移行します'),
(ForTestException, 'I-ERR-04 [Account] の処理中に予期せぬエラーが発生しました 次のオブジェクトの処理に移行します エラー内容: [例外発生]')
])
def test_raise_check_object_info_process(self, caplog, exception, message, run_control_process):
"""
Cases:
1. オブジェクト情報形式チェック処理でシステム例外が発生した場合エラーログが送出され後続の終了に移行すること
2. オブジェクト情報形式チェック処理で想定外の例外が発生した場合エラーログが送出され後続の終了に移行すること
Arranges:
- パラメータ1オブジェクト情報形式チェック処理でシステム例外が発生するようにする
- パラメータ2オブジェクト情報形式チェック処理で想定外の例外が発生するようにする
Expects:
- オブジェクト情報形式チェック処理で例外が発生する
- オブジェクト情報形式チェック処理で発生した例外のログメッセージが出力されている
- 例外が発生したオブジェクト以外は正常に終了する
"""
raise_object = exception('E-PRE-01', CHK_JP_NAME, '例外発生')
mock_return_values = [raise_object, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_2]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock(return_value=COMMON_LAST_FETCH_DATETIME)
self.mock_fetch_crm_data_process = MagicMock(return_value=[{'Name': 'Test'}])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
assert self.mock_check_object_info_process.call_count == 3
assert self.mock_set_datetime_period_process.call_count == 2
expect_process_result = {
'Account': 'fail',
'Contact': 'success',
'Call2_vod__c': 'success'
}
assert generate_log_message_tuple(log_message=message) in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{expect_process_result}]') in caplog.record_tuples
assert generate_log_message_tuple(
log_level=logging.ERROR, log_message=f'E-CTRL-01 一部のデータ取得に失敗しています 詳細はログをご確認ください') in caplog.record_tuples
@pytest.mark.parametrize(
'exception, message',
[
(ForTestMeDaCaCRMDataFetchException, f'I-ERR-03 [Account] の[{DATE_JP_NAME}]でエラーが発生しました 次のオブジェクトの処理に移行します'),
(ForTestException, 'I-ERR-04 [Account] の処理中に予期せぬエラーが発生しました 次のオブジェクトの処理に移行します エラー内容: [例外発生]')
])
def test_raise_set_datetime_period_process(self, caplog, exception, message, run_control_process):
"""
Cases:
1. データ取得期間設定処理でシステム例外が発生した場合エラーログが送出され後続の終了に移行すること
2. データ取得期間設定処理で想定外の例外が発生した場合エラーログが送出され後続の終了に移行すること
Arranges:
- パラメータ1データ取得期間設定処理でシステム例外が発生するようにする
- パラメータ2データ取得期間設定処理で想定外の例外が発生するようにする
Expects:
- データ取得期間設定チェック処理で例外が発生する
- データ取得期間設定チェック処理で発生した例外のログメッセージが出力されている
- 例外が発生したオブジェクト以外は正常に終了する
"""
mock_return_values = [COMMON_TARGET_OBJECTS_1, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_3]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock(side_effect=[exception(
'E-DATE-01', DATE_JP_NAME, '例外発生'), COMMON_LAST_FETCH_DATETIME, COMMON_LAST_FETCH_DATETIME])
self.mock_fetch_crm_data_process = MagicMock(return_value=[{'Name': 'Test'}])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
assert self.mock_set_datetime_period_process.call_count == 3
assert self.mock_fetch_crm_data_process.call_count == 2
expect_process_result = {
'Account': 'fail',
'Contact': 'success',
'Call2_vod__c': 'success'
}
assert generate_log_message_tuple(log_message=message) in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{expect_process_result}]') in caplog.record_tuples
assert generate_log_message_tuple(
log_level=logging.ERROR, log_message=f'E-CTRL-01 一部のデータ取得に失敗しています 詳細はログをご確認ください') in caplog.record_tuples
@pytest.mark.parametrize(
'exception, message',
[
(ForTestMeDaCaCRMDataFetchException, f'I-ERR-03 [Account] の[{FETCH_JP_NAME}]でエラーが発生しました 次のオブジェクトの処理に移行します'),
(ForTestException, 'I-ERR-04 [Account] の処理中に予期せぬエラーが発生しました 次のオブジェクトの処理に移行します エラー内容: [例外発生]')
])
def test_raise_fetch_crm_data_process(self, caplog, exception, message, run_control_process):
"""
Cases:
1. CRMデータ取得処理でシステム例外が発生した場合エラーログが送出され後続の終了に移行すること
2. CRMデータ取得処理で想定外の例外が発生した場合エラーログが送出され後続の終了に移行すること
Arranges:
- パラメータ1CRMデータ取得処理でシステム例外が発生するようにする
- パラメータ2CRMデータ取得処理で想定外の例外が発生するようにする
Expects:
- CRMデータ取得処理で例外が発生すること
- CRMデータ取得処理で発生した例外のログメッセージが出力されていること
"""
mock_return_values = [COMMON_TARGET_OBJECTS_1, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_3]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock(return_value=COMMON_LAST_FETCH_DATETIME)
self.mock_fetch_crm_data_process = MagicMock(side_effect=[exception(
'E-FETCH-01', FETCH_JP_NAME, '例外発生'), [{'Name': 'Test'}], [{'Name': 'Test'}]])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
assert self.mock_fetch_crm_data_process.call_count == 3
assert self.mock_backup_crm_data_process.call_count == 2
expect_process_result = {
'Account': 'fail',
'Contact': 'success',
'Call2_vod__c': 'success'
}
assert generate_log_message_tuple(log_message=message) in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{expect_process_result}]') in caplog.record_tuples
assert generate_log_message_tuple(
log_level=logging.ERROR, log_message=f'E-CTRL-01 一部のデータ取得に失敗しています 詳細はログをご確認ください') in caplog.record_tuples
@pytest.mark.parametrize(
'exception, message',
[
(ForTestMeDaCaCRMDataFetchException, f'I-ERR-03 [Account] の[{RESBK_JP_NAME}]でエラーが発生しました 次のオブジェクトの処理に移行します'),
(ForTestException, 'I-ERR-04 [Account] の処理中に予期せぬエラーが発生しました 次のオブジェクトの処理に移行します エラー内容: [例外発生]')
])
def test_raise_backup_crm_data_process(self, caplog, exception, message, run_control_process):
"""
Cases:
1. CRM電文データバックアップ処理でシステム例外が発生した場合エラーログが送出され後続の終了に移行すること
2. CRM電文データバックアップ処理で想定外の例外が発生した場合エラーログが送出され後続の終了に移行すること
Arranges:
- パラメータ1CRM電文データバックアップ処理でシステム例外が発生するようにする
- パラメータ2CRM電文データバックアップ処理で想定外の例外が発生するようにする
Expects:
- CRM電文データバックアップ処理で例外が発生すること
- CRM電文データバックアップ処理で発生した例外のログメッセージが出力されていること
"""
mock_return_values = [COMMON_TARGET_OBJECTS_1, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_3]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock(return_value=COMMON_LAST_FETCH_DATETIME)
self.mock_fetch_crm_data_process = MagicMock(side_effect=[[{'Name': 'Test'}], [{'Name': 'Test'}], [{'Name': 'Test'}]])
self.mock_backup_crm_data_process = MagicMock(side_effect=[exception(
'E-RESBK-01', RESBK_JP_NAME, '例外発生'), None, None])
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
assert self.mock_backup_crm_data_process.call_count == 3
assert self.mock_convert_crm_csv_data_process.call_count == 2
expect_process_result = {
'Account': 'fail',
'Contact': 'success',
'Call2_vod__c': 'success'
}
assert generate_log_message_tuple(log_message=message) in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{expect_process_result}]') in caplog.record_tuples
assert generate_log_message_tuple(
log_level=logging.ERROR, log_message=f'E-CTRL-01 一部のデータ取得に失敗しています 詳細はログをご確認ください') in caplog.record_tuples
@pytest.mark.parametrize(
'exception, message',
[
(ForTestMeDaCaCRMDataFetchException, f'I-ERR-03 [Account] の[{CONV_JP_NAME}]でエラーが発生しました 次のオブジェクトの処理に移行します'),
(ForTestException, 'I-ERR-04 [Account] の処理中に予期せぬエラーが発生しました 次のオブジェクトの処理に移行します エラー内容: [例外発生]')
])
def test_raise_convert_crm_csv_data_process(self, caplog, exception, message, run_control_process):
"""
Cases:
1. CSV変換処理でシステム例外が発生した場合エラーログが送出され後続の終了に移行すること
2. CSV変換処理で想定外の例外が発生した場合エラーログが送出され後続の終了に移行すること
Arranges:
- パラメータ1CSV変換処理でシステム例外が発生するようにする
- パラメータ2CSV変換処理で想定外の例外が発生するようにする
Expects:
- CSV変換処理で例外が発生すること
- CSV変換処理で発生した例外のログメッセージが出力されていること
"""
mock_return_values = [COMMON_TARGET_OBJECTS_1, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_3]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock(return_value=COMMON_LAST_FETCH_DATETIME)
self.mock_fetch_crm_data_process = MagicMock(side_effect=[[{'Name': 'Test'}], [{'Name': 'Test'}], [{'Name': 'Test'}]])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock(side_effect=[exception(
'E-CONV-01', CONV_JP_NAME, '例外発生'), None, None])
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
assert self.mock_convert_crm_csv_data_process.call_count == 3
assert self.mock_backup_crm_csv_data_process.call_count == 2
expect_process_result = {
'Account': 'fail',
'Contact': 'success',
'Call2_vod__c': 'success'
}
assert generate_log_message_tuple(log_message=message) in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{expect_process_result}]') in caplog.record_tuples
assert generate_log_message_tuple(
log_level=logging.ERROR, log_message=f'E-CTRL-01 一部のデータ取得に失敗しています 詳細はログをご確認ください') in caplog.record_tuples
@pytest.mark.parametrize(
'exception, message',
[
(ForTestMeDaCaCRMDataFetchException, f'I-ERR-03 [Account] の[{CSVBK_JP_NAME}]でエラーが発生しました 次のオブジェクトの処理に移行します'),
(ForTestException, 'I-ERR-04 [Account] の処理中に予期せぬエラーが発生しました 次のオブジェクトの処理に移行します エラー内容: [例外発生]')
])
def test_raise_backup_crm_csv_data_process(self, caplog, exception, message, run_control_process):
"""
Cases:
1. CSVバックアップ処理でシステム例外が発生した場合エラーログが送出され後続の終了に移行すること
2. CSVバックアップ処理で想定外の例外が発生した場合エラーログが送出され後続の終了に移行すること
Arranges:
- パラメータ1CSVバックアップ処理でシステム例外が発生するようにする
- パラメータ2CSVバックアップ処理で想定外の例外が発生するようにする
Expects:
- CSVバックアップ処理で例外が発生すること
- CSVバックアップ処理で発生した例外のログメッセージが出力されていること
"""
mock_return_values = [COMMON_TARGET_OBJECTS_1, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_3]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock(return_value=COMMON_LAST_FETCH_DATETIME)
self.mock_fetch_crm_data_process = MagicMock(side_effect=[[{'Name': 'Test'}], [{'Name': 'Test'}], [{'Name': 'Test'}]])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock(side_effect=[exception(
'E-CSVBK-01', CSVBK_JP_NAME, '例外発生'), None, None])
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
assert self.mock_backup_crm_csv_data_process.call_count == 3
assert self.mock_copy_crm_csv_data_process.call_count == 2
expect_process_result = {
'Account': 'fail',
'Contact': 'success',
'Call2_vod__c': 'success'
}
assert generate_log_message_tuple(log_message=message) in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{expect_process_result}]') in caplog.record_tuples
assert generate_log_message_tuple(
log_level=logging.ERROR, log_message=f'E-CTRL-01 一部のデータ取得に失敗しています 詳細はログをご確認ください') in caplog.record_tuples
@pytest.mark.parametrize(
'exception, message',
[
(ForTestMeDaCaCRMDataFetchException, f'I-ERR-03 [Account] の[{UPLD_JP_NAME}]でエラーが発生しました 次のオブジェクトの処理に移行します'),
(ForTestException, 'I-ERR-04 [Account] の処理中に予期せぬエラーが発生しました 次のオブジェクトの処理に移行します エラー内容: [例外発生]')
])
def test_raise_copy_crm_csv_data_process(self, caplog, exception, message, run_control_process):
"""
Cases:
1. CSVアップロード処理でシステム例外が発生した場合エラーログが送出され後続の終了に移行すること
2. CSVアップロード処理で想定外の例外が発生した場合エラーログが送出され後続の終了に移行すること
Arranges:
- パラメータ1CSVアップロード処理でシステム例外が発生するようにする
- パラメータ2CSVアップロード処理で想定外の例外が発生するようにする
Expects:
- CSVアップロード処理で例外が発生すること
- CSVアップロード処理で発生した例外のログメッセージが出力されていること
"""
mock_return_values = [COMMON_TARGET_OBJECTS_1, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_3]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock(return_value=COMMON_LAST_FETCH_DATETIME)
self.mock_fetch_crm_data_process = MagicMock(side_effect=[[{'Name': 'Test'}], [{'Name': 'Test'}], [{'Name': 'Test'}]])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock(side_effect=[exception(
'E-UPLD-01', UPLD_JP_NAME, '例外発生'), None, None])
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock()
run_control_process()
assert self.mock_copy_crm_csv_data_process.call_count == 3
assert self.mock_upload_last_fetch_datetime_process.call_count == 2
expect_process_result = {
'Account': 'fail',
'Contact': 'success',
'Call2_vod__c': 'success'
}
assert generate_log_message_tuple(log_message=message) in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{expect_process_result}]') in caplog.record_tuples
assert generate_log_message_tuple(
log_level=logging.ERROR, log_message=f'E-CTRL-01 一部のデータ取得に失敗しています 詳細はログをご確認ください') in caplog.record_tuples
@pytest.mark.parametrize(
'exception, message',
[
(ForTestMeDaCaCRMDataFetchException, f'I-ERR-03 [Account] の[{UPD_JP_NAME}]でエラーが発生しました 次のオブジェクトの処理に移行します'),
(ForTestException, 'I-ERR-04 [Account] の処理中に予期せぬエラーが発生しました 次のオブジェクトの処理に移行します エラー内容: [例外発生]')
])
def test_raise_upload_last_fetch_datetime_process(self, caplog, exception, message, run_control_process):
"""
Cases:
1. 前回取得日時ファイル更新処理でシステム例外が発生した場合エラーログが送出され後続の終了に移行すること
2. 前回取得日時ファイル更新処理で想定外の例外が発生した場合エラーログが送出され後続の終了に移行すること
Arranges:
- パラメータ1前回取得日時ファイル更新処理でシステム例外が発生するようにする
- パラメータ2前回取得日時ファイル更新処理で想定外の例外が発生するようにする
Expects:
- 前回取得日時ファイル更新処理で例外が発生すること
- 前回取得日時ファイル更新処理で発生した例外のログメッセージが出力されていること
"""
mock_return_values = [COMMON_TARGET_OBJECTS_1, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_3]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock(return_value=COMMON_LAST_FETCH_DATETIME)
self.mock_fetch_crm_data_process = MagicMock(side_effect=[[{'Name': 'Test'}], [{'Name': 'Test'}], [{'Name': 'Test'}]])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock(side_effect=[exception(
'E-UPD-01', UPD_JP_NAME, '例外発生'), None, None])
self.mock_upload_result_data_process = MagicMock()
run_control_process()
assert self.mock_upload_last_fetch_datetime_process.call_count == 3
expect_process_result = {
'Account': 'fail',
'Contact': 'success',
'Call2_vod__c': 'success'
}
assert generate_log_message_tuple(log_message=message) in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{expect_process_result}]') in caplog.record_tuples
assert generate_log_message_tuple(
log_level=logging.ERROR, log_message=f'E-CTRL-01 一部のデータ取得に失敗しています 詳細はログをご確認ください') in caplog.record_tuples
@pytest.mark.parametrize(
'exception, message',
[
(ForTestMeDaCaCRMDataFetchException, f'E-ERR-01 [{END_JP_NAME}]でエラーが発生したため、処理を終了します'),
(ForTestException, 'E-ERR-02 予期せぬエラーが発生したため、処理を終了します エラー内容: [例外発生]')
])
def test_raise_upload_result_data_process(self, caplog, exception, message, run_control_process):
"""
Cases:
1. 取得処理実施結果アップロード処理でシステム例外が発生した場合エラーで終了すること
2. 取得処理実施結果アップロード処理で想定外の例外が発生した場合エラーで終了すること
Arranges:
- パラメータ1取得処理実施結果アップロード処理でシステム例外が発生するようにする
- パラメータ2取得処理実施結果アップロード処理で想定外の例外が発生するようにする
Expects:
- 取得処理実施結果アップロード処理で例外が発生すること
- 取得処理実施結果アップロード処理で発生した例外のログメッセージが出力されていること
"""
mock_return_values = [COMMON_TARGET_OBJECTS_1, COMMON_TARGET_OBJECTS_2, COMMON_TARGET_OBJECTS_3]
self.mock_prepare_data_fetch_process = MagicMock(return_value=(deepcopy(COMMON_FETCH_TARGET_OBJECTS), COMMON_EXECUTE_DATETIME, {}))
self.mock_check_object_info_process = MagicMock(side_effect=mock_return_values)
self.mock_set_datetime_period_process = MagicMock(return_value=COMMON_LAST_FETCH_DATETIME)
self.mock_fetch_crm_data_process = MagicMock(side_effect=[[{'Name': 'Test'}], [{'Name': 'Test'}], [{'Name': 'Test'}]])
self.mock_backup_crm_data_process = MagicMock()
self.mock_convert_crm_csv_data_process = MagicMock()
self.mock_backup_crm_csv_data_process = MagicMock()
self.mock_copy_crm_csv_data_process = MagicMock()
self.mock_upload_last_fetch_datetime_process = MagicMock()
self.mock_upload_result_data_process = MagicMock(side_effect=[exception(
'E-END-01', END_JP_NAME, '例外発生'), None, None])
with pytest.raises(exception):
run_control_process()
assert self.mock_upload_result_data_process.called is True
assert self.mock_upload_result_data_process.call_count == 1
assert generate_log_message_tuple(
log_level=logging.ERROR,
log_message=message) in caplog.record_tuples, '取得処理実施結果アップロード処理で発生した例外のログメッセージが出力されていること'

View File

@ -0,0 +1,200 @@
import textwrap
from collections import OrderedDict
from unittest.mock import patch
import pytest
from src.config.objects import TargetObject
from src.convert_crm_csv_data_process import convert_crm_csv_data_process
from src.error.exceptions import DataConvertException
from src.system_var.constants import CONV_JP_NAME
from src.util.execute_datetime import ExecuteDateTime
from .test_utils.log_message import generate_log_message_tuple
class TestConvertCrmCsvDataProcess:
def test_run_process_success(self, caplog):
"""
Cases:
CSV変換処理が正常終了し期待通りの結果が返ること
Arranges:
- チェック対象のdictオブジェクトを宣言する
Expects:
- CSV変換処理の結果文字列が返却されること
- CSV変換処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arrange
response_json = [
OrderedDict([
('attributes', OrderedDict([('type', 'Account'),
('url', '/services/data/v1.0/sobjects/Account/TEST001')])),
('Id', 'TEST001'),
('AccountNumber', 'test001'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('SystemModstamp', '2022-06-01T00:00:00.000+0000'),
('IsDeleted', True),
('PersonMailingAddress', OrderedDict([
('PersonMailingStreet', 'Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'), # noqa: E501
('PersonMailingCity', 'New york city'),
('PersonMailingState', 'Ohaio'),
('PersonMailingPostalCode', '999-9999'),
('PersonMailingCountry', 'US'),
('PersonMailingLatitude', 50.1234567),
('PersonMailingLongitude', 103.1234567),
('PersonMailingGeocodeAccuracy', 'Address'),
])),
]),
OrderedDict([
('attributes', OrderedDict([('type', 'Account'),
('url', '/services/data/v1.0/sobjects/Account/TEST002')])),
('Id', 'TEST002'),
('AccountNumber', 'test002'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.23E+0),
('SystemModstamp', '2022-06-01T00:00:00.000+0000'),
('IsDeleted', False),
('PersonMailingAddress', OrderedDict([
('PersonMailingStreet', 'Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'), # noqa: E501
('PersonMailingCity', 'New york city'),
('PersonMailingState', 'Ohaio'),
('PersonMailingPostalCode', '999-9999'),
('PersonMailingCountry', 'US'),
('PersonMailingLatitude', 50.1234567),
('PersonMailingLongitude', 103.1234567),
('PersonMailingGeocodeAccuracy', 'Address'),
])),
]),
OrderedDict([
('attributes', OrderedDict([('type', 'Account'),
('url', '/services/data/v1.0/sobjects/Account/TEST003')])),
('Id', 'TEST003'),
('AccountNumber', 'test003'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567),
('SystemModstamp', '2022-06-01T00:00:00.000+0000'),
('IsDeleted', False),
('PersonMailingAddress', OrderedDict([
('PersonMailingStreet', 'Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit, \nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'), # noqa: E501
('PersonMailingCity', 'New york city'),
('PersonMailingState', 'Ohaio'),
('PersonMailingPostalCode', '999-9999'),
('PersonMailingCountry', 'US'),
('PersonMailingLatitude', 50.1234567),
('PersonMailingLongitude', 103.1234567),
('PersonMailingGeocodeAccuracy', 'Address'),
])),
]),
]
target_object_dict = {
'object_name': 'Account',
'columns': [
'Id',
'AccountNumber',
'LastModifiedDate',
'LastModifiedById',
'SystemModstamp',
'IsDeleted',
'PersonMailingAddress'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_object_dict, execute_datetime)
# Act
actual_csv_string = convert_crm_csv_data_process(target_object, response_json)
# Assert
expect_csv_string = """\
"Id","AccountNumber","LastModifiedDate","LastModifiedById","SystemModstamp","IsDeleted","PersonMailingAddress"\r\n\
"TEST001","test001","2022-06-01 09:00:00","1234567.0","2022-06-01 09:00:00","1","{""PersonMailingStreet"": ""Lorem ipsum dolor sit amet, \\nconsectetur adipiscing elit, \\nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua."", ""PersonMailingCity"": ""New york city"", ""PersonMailingState"": ""Ohaio"", ""PersonMailingPostalCode"": ""999-9999"", ""PersonMailingCountry"": ""US"", ""PersonMailingLatitude"": 50.1234567, ""PersonMailingLongitude"": 103.1234567, ""PersonMailingGeocodeAccuracy"": ""Address""}"\r\n\
"TEST002","test002","2022-06-01 09:00:00","1.23","2022-06-01 09:00:00","0","{""PersonMailingStreet"": ""Lorem ipsum dolor sit amet, \\nconsectetur adipiscing elit, \\nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua."", ""PersonMailingCity"": ""New york city"", ""PersonMailingState"": ""Ohaio"", ""PersonMailingPostalCode"": ""999-9999"", ""PersonMailingCountry"": ""US"", ""PersonMailingLatitude"": 50.1234567, ""PersonMailingLongitude"": 103.1234567, ""PersonMailingGeocodeAccuracy"": ""Address""}"\r\n\
"TEST003","test003","2022-06-01 09:00:00","1.234567","2022-06-01 09:00:00","0","{""PersonMailingStreet"": ""Lorem ipsum dolor sit amet, \\nconsectetur adipiscing elit, \\nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua."", ""PersonMailingCity"": ""New york city"", ""PersonMailingState"": ""Ohaio"", ""PersonMailingPostalCode"": ""999-9999"", ""PersonMailingCountry"": ""US"", ""PersonMailingLatitude"": 50.1234567, ""PersonMailingLongitude"": 103.1234567, ""PersonMailingGeocodeAccuracy"": ""Address""}"\r\n\
"""
# 返り値の期待値チェック
assert isinstance(actual_csv_string, str), 'CSV文字列が返却される'
assert actual_csv_string == textwrap.dedent(expect_csv_string)
# ログの確認
assert generate_log_message_tuple(log_message='I-CONV-01 [Account] のCSV変換処理を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-CONV-03 [Account] のCSV変換処理を終了します') in caplog.record_tuples
def test_call_depended_modules(self):
"""
Cases:
CSV変換処理内で依存しているモジュールが正しく呼ばれていること
Arranges:
- CSV変換処理の依存モジュールをモック化する
Expects:
- 依存しているモジュールが正しく呼ばれている
"""
# Arrange
target_object_dict = {
'object_name': 'Account',
'columns': [
'Id',
'AccountNumber',
'LastModifiedDate',
'LastModifiedById',
'SystemModstamp',
'IsDeleted'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_object_dict, execute_datetime)
# Act
with patch('src.convert_crm_csv_data_process.CSVStringConverter') as mock_converter:
inst = mock_converter.return_value
inst.convert.return_value = ''
convert_crm_csv_data_process(target_object, {})
# Assert
assert mock_converter.called is True
assert inst.convert.called is True
def test_raise_convert(self):
"""
Cases:
CSV変換処理でエラーが発生した場合検査例外が発生すること
Arranges:
- CSV変換処理で例外が発生するようにする
Expects:
- 例外が発生する
- 形式チェックが失敗したエラーが返却される
"""
# Arrange
target_object_dict = {
'object_name': 'Account',
'columns': [
'Id',
'AccountNumber',
'LastModifiedDate',
'LastModifiedById',
'SystemModstamp',
'IsDeleted'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_object_dict, execute_datetime)
# Act
with patch('src.convert_crm_csv_data_process.CSVStringConverter') as mock_converter:
inst = mock_converter.return_value
inst.convert.side_effect = Exception('変換エラー')
with pytest.raises(DataConvertException) as e:
convert_crm_csv_data_process(target_object, {})
# Assert
assert mock_converter.called is True
assert inst.convert.called is True
assert e.value.error_id == 'E-CONV-01'
assert e.value.func_name == CONV_JP_NAME
assert e.value.args[0] == f'[Account] CSV変換に失敗しました エラー内容:[変換エラー]'

View File

@ -0,0 +1,160 @@
from unittest.mock import MagicMock, patch
import pytest
from src.config.objects import TargetObject
from src.copy_crm_csv_data_process import copy_crm_csv_data_process
from src.error.exceptions import FileUploadException
from src.system_var.constants import UPLD_JP_NAME
from src.util.execute_datetime import ExecuteDateTime
from .test_utils.log_message import generate_log_message_tuple
class TestCopyCrmCsvDataProcess:
@pytest.fixture
def bucket_name(self):
return 'test-bucket'
@pytest.fixture
def prepare_bucket(self, s3_client, bucket_name):
s3_client.create_bucket(Bucket=bucket_name)
yield
def test_run_process_success(self, s3_client, prepare_bucket, bucket_name, monkeypatch, caplog):
"""
Cases:
CSVアップロード処理が正常終了し期待通りの結果が返ること
Arranges:
- オブジェクト情報の準備
- コピー元のバケットのモックを生成する
- コピー先のバケットの環境変数を設定する
- コピー元にファイルを配置する
Expects:
- コピーしたしたCSVファイルが存在し内容が想定と一致する
- CSVアップロード処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arranges
# オブジェクト情報の準備
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
# コピー元のバケットをモック化
from_copy_bucket = 'for-copy-bucket'
s3_client.create_bucket(Bucket=from_copy_bucket)
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', from_copy_bucket)
monkeypatch.setattr('src.copy_crm_csv_data_process.CRM_IMPORT_DATA_BACKUP_FOLDER', 'data_import')
# コピー先のバケットをモック化
monkeypatch.setattr('src.aws.s3.IMPORT_DATA_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.CRM_IMPORT_DATA_FOLDER', 'crm/target')
# コピー元のモックバケットにデータを配置
s3_client.put_object(Bucket=from_copy_bucket,
Key=f'data_import/{execute_datetime.to_path()}/CRM_Account_{execute_datetime.format_date()}.csv', Body=b'test,test,test')
# Act
copy_crm_csv_data_process(target_object, execute_datetime)
# Assert
# ファイル確認
actual = s3_client.get_object(
Bucket=bucket_name, Key=f'crm/target/CRM_Account_{execute_datetime.format_date()}.csv')
assert actual['Body'].read().decode('utf-8') == 'test,test,test'
# ログの確認
assert generate_log_message_tuple(
log_message=f'I-UPLD-01 [Account] のCSVデータアップロード処理を開始します ファイル名:[CRM_Account_{execute_datetime.format_date()}.csv]') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-UPLD-03 [Account] のCSVデータのアップロード処理を終了します') in caplog.record_tuples
def test_call_depended_modules(self):
"""
Cases:
CSVアップロード処理内で依存しているモジュールが正しく呼ばれていること
Arranges:
- CSVアップロード処理の依存モジュールをモック化する
- オブジェクト情報の準備
Expects:
- 依存しているモジュールが正しく呼ばれている
"""
# Arrange
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
mock_data_bucket = MagicMock(return_value=None)
mock_backup_bucket = MagicMock(return_value=None)
mock_copy_bucket = MagicMock(return_value=None)
# Act
with patch('src.aws.s3.DataBucket.__init__', mock_data_bucket), \
patch('src.aws.s3.BackupBucket.__init__', mock_backup_bucket), \
patch('src.aws.s3.DataBucket.put_csv_from', mock_copy_bucket):
copy_crm_csv_data_process(target_object, execute_datetime)
# Assert
assert mock_data_bucket.called is True
assert mock_backup_bucket.called is True
assert mock_copy_bucket.called is True
def test_raise_run_process_success(self, s3_client, prepare_bucket, bucket_name, monkeypatch, caplog):
"""
Cases:
CSVデータをコピーできない場合エラーが発生すること
Arranges:
- オブジェクト情報の準備
- コピー元のバケットのモックを生成する
- コピー先のバケットの環境変数を設定する
- コピー元にファイルを配置する
Expects:
- 例外が発生する
- ファイルがアップロードできないエラーが返却される
"""
# Arranges
# オブジェクト情報の準備
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
mock_copy_bucket = MagicMock(side_effect=Exception('ファイルコピーエラー'))
# Act
with patch('src.aws.s3.DataBucket.put_csv_from', mock_copy_bucket):
with pytest.raises(FileUploadException) as e:
copy_crm_csv_data_process(target_object, execute_datetime)
# Assert
assert e.value.error_id == 'E-UPLD-01'
assert e.value.func_name == UPLD_JP_NAME
assert e.value.args[0] == f'[Account] CSVデータのアップロードに失敗しました ファイル名:[CRM_Account_{execute_datetime.format_date()}.csv] エラー内容:[ファイルコピーエラー]'

View File

@ -0,0 +1,438 @@
import logging
from collections import OrderedDict
from unittest.mock import patch
import pytest
from requests.exceptions import ConnectTimeout, ReadTimeout
from src.config.objects import LastFetchDatetime, TargetObject
from src.error.exceptions import SalesforceAPIException
from src.fetch_crm_data_process import fetch_crm_data_process
from src.system_var.constants import FETCH_JP_NAME
from src.util.execute_datetime import ExecuteDateTime
from .test_utils.log_message import generate_log_message_tuple
common_target_object_dict = {
'object_name': 'Account',
'columns': [
'Id',
'AccountNumber',
'LastModifiedDate',
'LastModifiedById',
'SystemModstamp',
'IsDeleted'
]
}
common_last_fetch_datetime_dict = {
'last_fetch_datetime_from': '2000-01-01T00:00:00.000Z',
'last_fetch_datetime_to': '2010-01-01T00:00:00.000Z',
}
common_execute_datetime = ExecuteDateTime()
common_target_object = TargetObject(common_target_object_dict, common_execute_datetime)
common_last_fetch_datetime = LastFetchDatetime(common_last_fetch_datetime_dict, common_execute_datetime)
common_expect = [
OrderedDict([
('attributes', OrderedDict([('type', 'Account'), ('url', '/services/data/v1.0/sobjects/Account/TEST001')])),
('Id', 'TEST001'),
('AccountNumber', 'test001'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('SystemModstamp', '2022-06-01T00:00:00.000+0000'),
('IsDeleted', False)
]),
OrderedDict([
('attributes', OrderedDict([('type', 'Account'), ('url', '/services/data/v1.0/sobjects/Account/TEST002')])),
('Id', 'TEST002'),
('AccountNumber', 'test002'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('SystemModstamp', '2022-06-01T00:00:00.000+0000'),
('IsDeleted', False)
]),
OrderedDict([
('attributes', OrderedDict([('type', 'Account'), ('url', '/services/data/v1.0/sobjects/Account/TEST002')])),
('Id', 'TEST002'),
('AccountNumber', 'test002'),
('LastModifiedDate', '2022-06-01T00:00:00.000+0000'),
('LastModifiedById', 1.234567E+6),
('SystemModstamp', '2022-06-01T00:00:00.000+0000'),
('IsDeleted', False)
]),
]
class TestFetchCrmDataProcess:
def test_run_process_success(self, monkeypatch, caplog):
"""
Cases:
CRMデータ取得処理が正常終了し期待通りの結果が返ること
Arranges:
- SalesforceApiClientクラスをモック化し固定の値を返すようにする
Expects:
- CRMから取得した取得オブジェクトのリストが返却される
- CRMデータ取得処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arrange
# モック化
with patch('src.fetch_crm_data_process.SalesforceApiClient') as mock:
instance = mock.return_value
instance.fetch_sf_count.return_value = len(common_expect)
instance.fetch_sf_data.return_value = common_expect
# Act
actual_crm_data_response = fetch_crm_data_process(common_target_object, common_last_fetch_datetime)
# Assert
# 返り値の期待値チェック
assert isinstance(actual_crm_data_response, list)
assert isinstance(actual_crm_data_response[0], OrderedDict)
assert actual_crm_data_response == common_expect
# ログの確認
assert generate_log_message_tuple(log_message='I-FETCH-01 [Account] のCRMからのデータ取得処理を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-FETCH-02 [Account] の件数取得を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-FETCH-03 [Account] の件数:[3]') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-FETCH-04 [Account] のレコード取得を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-FETCH-05 [Account] のレコード取得が成功しました') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-FETCH-06 [Account] のCRMからのデータ取得処理を終了します') in caplog.record_tuples
def test_call_depended_modules(self):
"""
Cases:
CRMデータ取得処理内で依存しているモジュールが正しく呼ばれていること
Arranges:
- CRMデータ取得処理の依存モジュールをモック化する
Expects:
- 依存しているモジュールが正しく呼ばれている
"""
# Arrange
with patch('src.fetch_crm_data_process.CounterObject', ) as mock_counter, \
patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \
patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client:
# モック化
mock_counter_inst = mock_counter.return_value
mock_counter_inst.describe.return_value = 1
mock_counter_inst.increment.return_value = 1
mock_builder_inst = mock_soql_builder.return_value
mock_builder_inst.create_count_soql.return_value = ''
mock_builder_inst.create_fetch_soql.return_value = ''
mock_client_inst = mock_api_client.return_value
mock_client_inst.fetch_sf_count.return_value = 1
mock_client_inst.fetch_sf_data.return_value = common_expect
# Act
fetch_crm_data_process(common_target_object, common_last_fetch_datetime)
# Assert
assert mock_counter.call_count == 2, 'リトライカウント用クラスのインスタンスが生成されていること'
assert mock_counter_inst.describe.called is False, 'リトライ用のカウントが取得されていないこと'
assert mock_counter_inst.increment.called is False, 'リトライ用のカウントが足されていないこと'
assert mock_soql_builder.call_count == 1, 'SOQL生成クラスのインスタンスが生成されていること'
assert mock_builder_inst.create_count_soql.called is True, '件数取得SOQLが生成されていること'
assert mock_builder_inst.create_fetch_soql.called is True, 'データ取得SOQLが生成されていること'
assert mock_api_client.call_count == 2, 'Salesforce APIクライアントのインスタンスが生成されていること'
assert mock_client_inst.fetch_sf_count.called is True, '件数の取得が呼ばれていること'
assert mock_client_inst.fetch_sf_data.called is True, 'レコードの取得が呼ばれていること'
def test_raise_create_count_soql(self, monkeypatch, caplog):
"""
Cases:
件数取得用SOQLが生成できない場合エラーが発生すること
Arranges:
- 件数取得用SOQL生成処理で例外が発生するようにする
Expects:
- 例外が発生する
- データ件数取得に失敗した旨のエラーが出力される
"""
# Arrange
with patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \
patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client:
# モック化
mock_builder_inst = mock_soql_builder.return_value
mock_builder_inst.create_count_soql.side_effect = Exception('生成エラー')
mock_builder_inst.create_fetch_soql.return_value = ''
mock_client_inst = mock_api_client.return_value
mock_client_inst.fetch_sf_count.return_value = 1
mock_client_inst.fetch_sf_data.return_value = common_expect
# Act
with pytest.raises(SalesforceAPIException) as e:
fetch_crm_data_process(common_target_object, common_last_fetch_datetime)
# Assert
assert e.value.error_id == 'E-FETCH-01'
assert e.value.func_name == FETCH_JP_NAME
assert e.value.args[0] == f'[Account] の件数取得に失敗しました エラー内容:[生成エラー]'
def test_raise_create_fetch_soql(self, monkeypatch, caplog):
"""
Cases:
データ取得用SOQLが生成できない場合エラーが発生すること
Arranges:
- データ取得用SOQL生成処理で例外が発生するようにする
Expects:
- 例外が発生する
- データ取得に失敗した旨のエラーが出力される
"""
# Arrange
with patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \
patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client:
# モック化
mock_builder_inst = mock_soql_builder.return_value
mock_builder_inst.create_count_soql.return_value = ''
mock_builder_inst.create_fetch_soql.side_effect = Exception('生成エラー')
mock_client_inst = mock_api_client.return_value
mock_client_inst.fetch_sf_count.return_value = 1
mock_client_inst.fetch_sf_data.return_value = common_expect
# Act
with pytest.raises(SalesforceAPIException) as e:
fetch_crm_data_process(common_target_object, common_last_fetch_datetime)
# Assert
assert e.value.error_id == 'E-FETCH-02'
assert e.value.func_name == FETCH_JP_NAME
assert e.value.args[0] == f'[Account] のレコード取得に失敗しました エラー内容:[生成エラー]'
@pytest.mark.parametrize('timeout_env_name, exception, expect_message', [
('CRM_AUTH_TIMEOUT', ConnectTimeout('接続タイムアウト'), 'W-FETCH-01 CRMの接続処理がタイムアウトしため、リトライします[1] エラー内容:[接続タイムアウト]'),
('CRM_GET_RECORD_COUNT_TIMEOUT', ReadTimeout('読み取りタイムアウト'), 'W-FETCH-02 [Account] の件数取得処理がタイムアウトしたため、リトライします:[1] エラー内容:[読み取りタイムアウト]'),
('CRM_AUTH_TIMEOUT', Exception('予期せぬ例外'), 'W-FETCH-03 [Account] の件数取得に失敗したため、リトライします エラー内容:[予期せぬ例外]'),
], ids=['connection_timeout', 'read_timeout', 'unexpected_exception'])
def test_raise_fetch_sf_count_with_retry_success(self, monkeypatch, caplog, timeout_env_name, exception, expect_message):
"""
Cases:
1. データ件数取得処理で接続タイムアウト例外が発生した場合リトライした結果復旧し正常終了すること
2. データ件数取得処理で読み取りタイムアウト例外が発生した場合リトライした結果復旧し正常終了すること
3. データ件数取得処理で予期せぬ例外が発生した場合リトライした結果復旧し正常終了すること
Arranges:
- データ件数取得処理の最大リトライ試行回数を4に設定する
- timeout_env_nameに指定されたリトライタイムアウト時間の秒数を1に設定する
- データ件数取得処理の初回に接続タイムアウト例外が発生するようにする
Expects:
- 正常終了する
- データ件数取得に失敗した旨のエラーが出力されない
"""
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT', 4)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL', 1)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL', 1)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_INTERVAL', 1)
monkeypatch.setattr(f'src.fetch_crm_data_process.{timeout_env_name}', 1)
# Arrange
with patch('src.fetch_crm_data_process.CounterObject', ) as mock_counter, \
patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \
patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client:
# モック化
mock_counter_inst = mock_counter.return_value
mock_counter_inst.describe.side_effect = [1, 2, 3, 4]
mock_counter_inst.increment.side_effect = [2, 3, 4, 5]
mock_builder_inst = mock_soql_builder.return_value
mock_builder_inst.create_count_soql.return_value = ''
mock_builder_inst.create_fetch_soql.return_value = ''
mock_client_inst = mock_api_client.return_value
mock_client_inst.fetch_sf_count.side_effect = [exception, len(common_expect)]
mock_client_inst.fetch_sf_data.return_value = common_expect
# Act
fetch_crm_data_process(common_target_object, common_last_fetch_datetime)
# Assert
assert mock_counter_inst.describe.call_count == 1
assert mock_counter_inst.increment.call_count == 1
assert generate_log_message_tuple(
log_level=logging.WARNING,
log_message=expect_message) in caplog.record_tuples
called_log_counts = len([log for log in caplog.messages if log == expect_message])
assert called_log_counts == 1
assert generate_log_message_tuple(log_message='I-FETCH-06 [Account] のCRMからのデータ取得処理を終了します') in caplog.record_tuples
@pytest.mark.parametrize('timeout_env_name, exception, expect_message', [
('CRM_AUTH_TIMEOUT', ConnectTimeout('接続タイムアウト'), 'W-FETCH-01 CRMの接続処理がタイムアウトしため、リトライします[1] エラー内容:[接続タイムアウト]'),
('CRM_GET_RECORD_COUNT_TIMEOUT', ReadTimeout('読み取りタイムアウト'), 'W-FETCH-02 [Account] の件数取得処理がタイムアウトしたため、リトライします:[1] エラー内容:[読み取りタイムアウト]'),
('CRM_AUTH_TIMEOUT', Exception('予期せぬ例外'), 'W-FETCH-03 [Account] の件数取得に失敗したため、リトライします エラー内容:[予期せぬ例外]'),
], ids=['connection_timeout', 'read_timeout', 'unexpected_exception'])
def test_raise_fetch_sf_count_with_retry_fail(self, monkeypatch, caplog, timeout_env_name, exception, expect_message):
"""
Cases:
1. データ件数取得処理で接続タイムアウト例外が発生した場合リトライした結果復旧せず異常終了すること
2. データ件数取得処理で読み取りタイムアウト例外が発生した場合リトライした結果復旧せず異常終了すること
3. データ件数取得処理で予期せぬ例外が発生した場合リトライした結果復旧せず異常終了すること
Arranges:
- データ件数取得処理の最大リトライ試行回数を4に設定する
- timeout_env_nameに指定されたリトライタイムアウト時間の秒数を1に設定する
- データ件数取得処理の1回目2回目3回目4回目で接続タイムアウト例外が発生するようにする
Expects:
- 異常終了する
- データ件数取得に失敗した旨のエラーが出力される
"""
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT', 4)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL', 1)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL', 1)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_INTERVAL', 1)
monkeypatch.setattr(f'src.fetch_crm_data_process.{timeout_env_name}', 1)
# Arrange
with patch('src.fetch_crm_data_process.CounterObject', ) as mock_counter, \
patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \
patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client:
# モック化
mock_counter_inst = mock_counter.return_value
mock_counter_inst.describe.side_effect = [1, 2, 3, 4]
mock_counter_inst.increment.side_effect = [2, 3, 4, 5]
mock_builder_inst = mock_soql_builder.return_value
mock_builder_inst.create_count_soql.return_value = ''
mock_builder_inst.create_fetch_soql.return_value = ''
mock_client_inst = mock_api_client.return_value
mock_client_inst.fetch_sf_count.side_effect = [exception, exception, exception, exception]
mock_client_inst.fetch_sf_data.return_value = common_expect
# Act
with pytest.raises(SalesforceAPIException) as e:
fetch_crm_data_process(common_target_object, common_last_fetch_datetime)
# Assert
# 取得は4回行われる
assert mock_counter_inst.describe.call_count == 4
# 足し込みは3回のみ
assert mock_counter_inst.increment.call_count == 3
assert generate_log_message_tuple(
log_level=logging.WARNING,
log_message=expect_message) in caplog.record_tuples
called_log_counts = len([log for log in caplog.messages if log == expect_message])
assert called_log_counts == 3
assert generate_log_message_tuple(log_message='I-FETCH-06 [Account] のCRMからのデータ取得処理を終了します') not in caplog.record_tuples
assert e.value.error_id == 'E-FETCH-01'
assert e.value.func_name == FETCH_JP_NAME
# リトライ例外のオブジェクトIDが違うため、in句で比較
assert f'[Account] の件数取得に失敗しました エラー内容:[RetryError' in e.value.args[0]
@pytest.mark.parametrize('timeout_env_name, exception, expect_message', [
('CRM_AUTH_TIMEOUT', ConnectTimeout('接続タイムアウト'), 'W-FETCH-04 CRMの接続処理がタイムアウトしため、リトライします[1] エラー内容:[接続タイムアウト]'),
('CRM_FETCH_RECORD_TIMEOUT', ReadTimeout('読み取りタイムアウト'), 'W-FETCH-05 [Account] のレコード取得処理がタイムアウトしたため、リトライします:[1] エラー内容:[読み取りタイムアウト]'),
('CRM_AUTH_TIMEOUT', Exception('予期せぬ例外'), 'W-FETCH-06 [Account] のレコード取得に失敗したため、リトライします エラー内容:[予期せぬ例外]'),
], ids=['connection_timeout', 'read_timeout', 'unexpected_exception'])
def test_raise_fetch_sf_data_with_retry_success(self, monkeypatch, caplog, timeout_env_name, exception, expect_message):
"""
Cases:
1. レコード取得処理で接続タイムアウト例外が発生した場合リトライした結果復旧し正常終了すること
2. レコード取得処理で読み取りタイムアウト例外が発生した場合リトライした結果復旧し正常終了すること
3. レコード取得処理で予期せぬ例外が発生した場合リトライした結果復旧し正常終了すること
Arranges:
- レコード取得処理の最大リトライ試行回数を4に設定する
- timeout_env_nameに指定されたリトライタイムアウト時間の秒数を1に設定する
- レコード取得処理の初回に接続タイムアウト例外が発生するようにする
Expects:
- 正常終了する
- データレコード取得に失敗した旨のエラーが出力されない
"""
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_MAX_RETRY_ATTEMPT', 4)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MAX_INTERVAL', 1)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_MIN_INTERVAL', 1)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_GET_RECORD_COUNT_RETRY_INTERVAL', 1)
monkeypatch.setattr(f'src.fetch_crm_data_process.{timeout_env_name}', 1)
# Arrange
with patch('src.fetch_crm_data_process.CounterObject', ) as mock_counter, \
patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \
patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client:
# モック化
mock_counter_inst = mock_counter.return_value
mock_counter_inst.describe.side_effect = [1, 2, 3, 4]
mock_counter_inst.increment.side_effect = [2, 3, 4, 5]
mock_builder_inst = mock_soql_builder.return_value
mock_builder_inst.create_count_soql.return_value = ''
mock_builder_inst.create_fetch_soql.return_value = ''
mock_client_inst = mock_api_client.return_value
mock_client_inst.fetch_sf_count.return_value = 1
mock_client_inst.fetch_sf_data.side_effect = [exception, common_expect]
# Act
fetch_crm_data_process(common_target_object, common_last_fetch_datetime)
# Assert
assert mock_counter_inst.describe.call_count == 1
assert mock_counter_inst.increment.call_count == 1
assert generate_log_message_tuple(
log_level=logging.WARNING,
log_message=expect_message) in caplog.record_tuples
called_log_counts = len([log for log in caplog.messages if log == expect_message])
assert called_log_counts == 1
assert generate_log_message_tuple(log_message='I-FETCH-06 [Account] のCRMからのデータ取得処理を終了します') in caplog.record_tuples
@pytest.mark.parametrize('timeout_env_name, exception, expect_message', [
('CRM_AUTH_TIMEOUT', ConnectTimeout('接続タイムアウト'), 'W-FETCH-04 CRMの接続処理がタイムアウトしため、リトライします[1] エラー内容:[接続タイムアウト]'),
('CRM_FETCH_RECORD_TIMEOUT', ReadTimeout('読み取りタイムアウト'), 'W-FETCH-05 [Account] のレコード取得処理がタイムアウトしたため、リトライします:[1] エラー内容:[読み取りタイムアウト]'),
('CRM_AUTH_TIMEOUT', Exception('予期せぬ例外'), 'W-FETCH-06 [Account] のレコード取得に失敗したため、リトライします エラー内容:[予期せぬ例外]'),
], ids=['connection_timeout', 'read_timeout', 'unexpected_exception'])
def test_raise_fetch_sf_data_with_retry_fail(self, monkeypatch, caplog, timeout_env_name, exception, expect_message):
"""
Cases:
1. レコード取得処理で接続タイムアウト例外が発生した場合リトライした結果復旧せず異常終了すること
2. レコード取得処理で読み取りタイムアウト例外が発生した場合リトライした結果復旧せず異常終了すること
3. レコード取得処理で予期せぬ例外が発生した場合リトライした結果復旧せず異常終了すること
Arranges:
- レコード取得処理の最大リトライ試行回数を4に設定する
- timeout_env_nameに指定されたリトライタイムアウト時間の秒数を1に設定する
- レコード取得処理の1回目2回目3回目4回目で接続タイムアウト例外が発生するようにする
Expects:
- 異常終了する
- データレコード取得に失敗した旨のエラーが出力される
"""
monkeypatch.setattr('src.fetch_crm_data_process.CRM_FETCH_RECORD_MAX_RETRY_ATTEMPT', 4)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_FETCH_RECORD_RETRY_MAX_INTERVAL', 1)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_FETCH_RECORD_RETRY_MIN_INTERVAL', 1)
monkeypatch.setattr('src.fetch_crm_data_process.CRM_FETCH_RECORD_RETRY_INTERVAL', 1)
monkeypatch.setattr(f'src.fetch_crm_data_process.{timeout_env_name}', 1)
# Arrange
with patch('src.fetch_crm_data_process.CounterObject', ) as mock_counter, \
patch('src.fetch_crm_data_process.SOQLBuilder') as mock_soql_builder, \
patch('src.fetch_crm_data_process.SalesforceApiClient') as mock_api_client:
# モック化
mock_counter_inst = mock_counter.return_value
mock_counter_inst.describe.side_effect = [1, 2, 3, 4]
mock_counter_inst.increment.side_effect = [2, 3, 4, 5]
mock_builder_inst = mock_soql_builder.return_value
mock_builder_inst.create_count_soql.return_value = ''
mock_builder_inst.create_fetch_soql.return_value = ''
mock_client_inst = mock_api_client.return_value
mock_client_inst.fetch_sf_count.return_value = 1
mock_client_inst.fetch_sf_data.side_effect = [exception, exception, exception, exception]
# Act
with pytest.raises(SalesforceAPIException) as e:
fetch_crm_data_process(common_target_object, common_last_fetch_datetime)
# Assert
# 取得は4回行われる
assert mock_counter_inst.describe.call_count == 4
# 足し込みは3回のみ
assert mock_counter_inst.increment.call_count == 3
assert generate_log_message_tuple(
log_level=logging.WARNING,
log_message=expect_message) in caplog.record_tuples
called_log_counts = len([log for log in caplog.messages if log == expect_message])
assert called_log_counts == 3
assert generate_log_message_tuple(log_message='I-FETCH-06 [Account] のCRMからのデータ取得処理を終了します') not in caplog.record_tuples
assert e.value.error_id == 'E-FETCH-02'
assert e.value.func_name == FETCH_JP_NAME
# リトライ例外のオブジェクトIDが違うため、in句で比較
assert f'[Account] のレコード取得に失敗しました エラー内容:[RetryError' in e.value.args[0]

View File

@ -0,0 +1,209 @@
import json
from datetime import datetime
from unittest.mock import MagicMock, patch
import pytest
from src.config.objects import FetchTargetObjects
from src.error.exceptions import FileNotFoundException, InvalidConfigException
from src.prepare_data_fetch_process import prepare_data_fetch_process
from src.system_var.constants import PRE_JP_NAME, YYYYMMDDTHHMMSSTZ
from src.system_var.environments import (CRM_CONFIG_BUCKET,
OBJECT_INFO_FILENAME,
OBJECT_INFO_FOLDER)
from .test_utils.log_message import generate_log_message_tuple
class TestPrepareDataFetchProcess:
@pytest.fixture
def bucket_name(self):
return 'test-config-bucket'
@pytest.fixture
def prepare_bucket(self, s3_client, bucket_name):
s3_client.create_bucket(Bucket=bucket_name)
yield
def test_run_process_success(self, s3_client, prepare_bucket, bucket_name, monkeypatch, caplog):
"""
Cases:
データ取得準備処理が正常終了し期待通りの結果が返ること
Arranges:
- prepare_bucketフィクスチャでCRM_取得オブジェクト情報ファイルを置くためのモックバケットを作る
- 作成したモックバケットを指すように環境変数を設定する
- CRM_取得オブジェクト情報ファイルを置く
Expects:
- CRM取得オブジェクトクラスのインスタンスが返却される
- CRM取得オブジェクトクラスのインスタンスが期待値と一致
- 実行日時クラスのインスタンスが返却される
- 実行日時クラスがテスト実行後の日時よりも小さい
- 処理結果出力用の空のdictが返却される
- データ取得準備処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arrange
target_objects_dict = {
'objects': [
{
'object_name': 'Account',
'columns': [
'Id',
'Name'
]
},
{
'object_name': 'Contact',
'columns': [
'Id',
'Name'
]
}
]
}
s3_client.put_object(Bucket=bucket_name, Key='crm/object_info/crm_object_list_diff.json', Body=json.dumps(target_objects_dict))
# 環境変数を編集
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.OBJECT_INFO_FOLDER', 'crm/object_info')
monkeypatch.setattr('src.aws.s3.OBJECT_INFO_FILENAME', 'crm_object_list_diff.json')
# Act
actual_fetch_target_objects, actual_execute_datetime, actual_process_result = \
prepare_data_fetch_process()
# Assert
# 返り値の期待値チェック
expect_fetch_target_objects = FetchTargetObjects(target_objects_dict)
assert isinstance(actual_fetch_target_objects, FetchTargetObjects), 'CRM取得オブジェクトクラスのインスタンスが返却される'
assert all([a == e for a, e in zip(actual_fetch_target_objects, expect_fetch_target_objects)]), 'CRM取得オブジェクトクラスのインスタンスが期待値と一致'
assert datetime.strptime(str(actual_execute_datetime), YYYYMMDDTHHMMSSTZ) < datetime.now(), '実行日時がアサーション時点の日時よりも小さいこと'
assert actual_process_result == {}, '処理結果出力用の空のdictが返却される'
# ログの確認
assert generate_log_message_tuple(log_message='I-PRE-01 データ取得準備処理を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-PRE-02 データ取得処理開始日時:{str(actual_execute_datetime)}') in caplog.record_tuples
assert generate_log_message_tuple(
log_message=f'I-PRE-03 CRM_取得オブジェクト情報ファイルの取得開始します ファイルパス:[s3://{CRM_CONFIG_BUCKET}/{OBJECT_INFO_FOLDER}/{OBJECT_INFO_FILENAME}]') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-PRE-09 データ取得準備処理を終了します') in caplog.record_tuples
def test_call_depended_modules(self):
"""
Cases:
データ取得準備処理内で依存しているモジュールが正しく呼ばれていること
Arranges:
- データ取得準備処理の依存モジュールをモック化する
Expects:
- 依存しているモジュールが正しく呼ばれている
"""
# Arrange
mock_config_bucket = MagicMock(return_value='')
mock_json_parser = MagicMock(return_value={})
mock_fetch_target_objects = MagicMock(return_value=None)
# Act
with patch('src.aws.s3.ConfigBucket.get_object_info_file', mock_config_bucket), \
patch('src.parser.json_parser.JsonParser.parse', mock_json_parser), \
patch('src.config.objects.FetchTargetObjects.__init__', mock_fetch_target_objects):
prepare_data_fetch_process()
# Assert
assert mock_config_bucket.called is True
assert mock_json_parser.called is True
assert mock_fetch_target_objects.called is True
def test_raise_get_object_info(self, bucket_name, monkeypatch, caplog):
"""
Cases:
CRM_取得オブジェクト情報ファイルを取得できない場合エラーが発生すること
Arranges:
- オブジェクト情報ファイル取得処理で例外が発生するようにする
Expects:
- 例外が発生する
- ファイルが読み込めないエラーが返却される
"""
# Arrange
monkeypatch.setattr('src.prepare_data_fetch_process.OBJECT_INFO_FILENAME', 'crm_object_list_diff.json')
mock_config_bucket = MagicMock(side_effect=Exception('ファイル取得エラー'))
mock_json_parser = MagicMock(return_value={})
mock_fetch_target_objects = MagicMock(return_value=None)
# Act
with patch('src.aws.s3.ConfigBucket.get_object_info_file', mock_config_bucket), \
patch('src.parser.json_parser.JsonParser.parse', mock_json_parser), \
patch('src.config.objects.FetchTargetObjects.__init__', mock_fetch_target_objects):
with pytest.raises(FileNotFoundException) as e:
prepare_data_fetch_process()
# Assert
assert mock_config_bucket.called is True
assert mock_json_parser.called is False
assert mock_fetch_target_objects.called is False
assert e.value.error_id == 'E-PRE-01'
assert e.value.func_name == PRE_JP_NAME
assert e.value.args[0] == f'CRM_取得オブジェクト情報ファイルが存在しません ファイル名:[crm_object_list_diff.json] エラー内容:[ファイル取得エラー]'
def test_raise_parse_json_str(self, bucket_name, monkeypatch, caplog):
"""
Cases:
CRM_取得オブジェクト情報ファイルをパースできない場合エラーが発生すること
Arranges:
- オブジェクト情報ファイルパース処理で例外が発生するようにする
Expects:
- 例外が発生する
- パースが失敗した旨エラーメッセージが表示される
"""
# Arrange
mock_config_bucket = MagicMock(return_value='')
mock_json_parser = MagicMock(side_effect=Exception('JSONパースエラー'))
mock_fetch_target_objects = MagicMock(return_value=None)
# Act
with patch('src.aws.s3.ConfigBucket.get_object_info_file', mock_config_bucket), \
patch('src.parser.json_parser.JsonParser.parse', mock_json_parser), \
patch('src.config.objects.FetchTargetObjects.__init__', mock_fetch_target_objects):
with pytest.raises(InvalidConfigException) as e:
prepare_data_fetch_process()
# Assert
assert mock_config_bucket.called is True
assert mock_json_parser.called is True
assert mock_fetch_target_objects.called is False
assert e.value.error_id == 'E-PRE-02'
assert e.value.func_name == PRE_JP_NAME
assert e.value.args[0] == f'CRM_取得オブジェクト情報ファイルのパースに失敗しました エラー内容:[JSONパースエラー]'
def test_raise_check_objects_format(self, bucket_name, monkeypatch, caplog):
"""
Cases:
CRM_取得オブジェクト情報のキーobjectsの形式チェックが不正の場合エラーが発生すること
Arranges:
- CRM_取得オブジェクト情報のキーobjectsの形式チェックで例外が発生するようにする
Expects:
- 例外が発生する
- 形式チェックが失敗した旨エラーメッセージが表示される
"""
# Arrange
monkeypatch.setattr('src.prepare_data_fetch_process.OBJECT_INFO_FILENAME', 'crm_object_list_diff.json')
mock_config_bucket = MagicMock(return_value='')
mock_json_parser = MagicMock(return_value={})
mock_fetch_target_objects = MagicMock(side_effect=Exception('形式チェックエラー'))
# Act
with patch('src.aws.s3.ConfigBucket.get_object_info_file', mock_config_bucket), \
patch('src.parser.json_parser.JsonParser.parse', mock_json_parser), \
patch('src.config.objects.FetchTargetObjects.__init__', mock_fetch_target_objects):
with pytest.raises(InvalidConfigException) as e:
prepare_data_fetch_process()
# Assert
assert mock_config_bucket.called is True
assert mock_json_parser.called is True
assert mock_fetch_target_objects.called is True
assert e.value.error_id == 'E-PRE-03'
assert e.value.func_name == PRE_JP_NAME
assert e.value.args[0] == 'CRM_取得オブジェクト情報ファイルの形式チェックに失敗しました ファイル名:[crm_object_list_diff.json] エラー内容:[形式チェックエラー]'

View File

@ -0,0 +1,238 @@
import json
from unittest.mock import MagicMock, patch
import pytest
from src.config.objects import LastFetchDatetime, TargetObject
from src.error.exceptions import FileNotFoundException, InvalidConfigException
from src.set_datetime_period_process import set_datetime_period_process
from src.util.execute_datetime import ExecuteDateTime
from .test_utils.log_message import generate_log_message_tuple
class TestSetDatetimePeriodProcess:
@pytest.fixture
def bucket_name(self):
return 'test-config-bucket'
@pytest.fixture
def prepare_bucket(self, s3_client, bucket_name):
s3_client.create_bucket(Bucket=bucket_name)
yield
def test_run_process_success(self, s3_client, prepare_bucket, bucket_name, monkeypatch, caplog):
"""
Cases:
データ取得期間設定処理が正常終了し期待通りの結果が返ること
Arranges:
- チェック対象のdictオブジェクトを宣言する
Expects:
- チェック後のオブジェクト情報コレクションクラスのインスタンスが返却される
- データ取得期間設定処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arrange
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
]
}
last_fetch_datetime_dict = {
'last_fetch_datetime_from': '1999-01-01T00:00:00.000Z',
'last_fetch_datetime_to': '2100-12-31T23:59:59.000Z',
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
s3_client.put_object(Bucket=bucket_name, Key='crm/last_fetch_datetime/Account.json', Body=json.dumps(last_fetch_datetime_dict))
# 環境変数を編集
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.set_datetime_period_process.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.LAST_FETCH_DATE_FOLDER', 'crm/last_fetch_datetime')
# Act
actual_last_fetch_datetime = set_datetime_period_process(target_object, execute_datetime)
# Assert
# 返り値の期待値チェック
assert isinstance(actual_last_fetch_datetime, LastFetchDatetime), '最終取得日時オブジェクトクラスのインスタンスが返却される'
assert actual_last_fetch_datetime.last_fetch_datetime_from == '1999-01-01T00:00:00.000Z'
assert actual_last_fetch_datetime.last_fetch_datetime_to == '2100-12-31T23:59:59.000Z'
# ログの確認
assert generate_log_message_tuple(log_message='I-DATE-01 [Account] のデータ取得期間設定処理を開始します') in caplog.record_tuples
assert generate_log_message_tuple(
log_message='I-DATE-02 前回取得日時ファイルの取得開始します ファイルパス:[s3://test-config-bucket/crm/last_fetch_datetime/Account.json]') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-DATE-03 前回取得日時ファイルの取得成功しました') in caplog.record_tuples
assert generate_log_message_tuple(
log_message='I-DATE-06 取得範囲 From: [1999-01-01T00:00:00.000Z] To: [2100-12-31T23:59:59.000Z]') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-DATE-07 [Account] のデータ取得期間設定処理を終了します') in caplog.record_tuples
def test_call_depended_modules(self, s3_client, prepare_bucket, bucket_name, monkeypatch, caplog):
"""
Cases:
データ取得期間設定処理内で依存しているモジュールが正しく呼ばれていること
Arranges:
- データ取得期間設定処理内の依存モジュールをモック化する
Expects:
- 依存しているモジュールが正しく呼ばれている
"""
# Arrange
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
]
}
last_fetch_datetime_dict = {
'last_fetch_datetime_from': '1999-01-01T00:00:00.000Z',
'last_fetch_datetime_to': '2100-12-31T23:59:59.000Z',
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
mock_config_bucket = MagicMock(return_value=last_fetch_datetime_dict)
mock_json_parser = MagicMock(return_value=last_fetch_datetime_dict)
# Act
with patch('src.aws.s3.ConfigBucket.get_last_fetch_datetime_file', mock_config_bucket), \
patch('src.parser.json_parser.JsonParser.parse', mock_json_parser):
set_datetime_period_process(target_object, execute_datetime)
# Assert
assert mock_config_bucket.called is True
assert mock_json_parser.called is True
def test_raise_get_last_fetch_datetime_file(self):
"""
Cases:
最終取得日時ファイル取得処理でエラーが発生すること
Arranges:
- オブジェクト形式チェック処理で例外が発生するようにする
Expects:
- 例外が発生する
- 形式チェックが失敗したエラーが返却される
"""
# Arrange
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
]
}
last_fetch_datetime_dict = {
'last_fetch_datetime_from': '1999-01-01T00:00:00.000Z',
'last_fetch_datetime_to': '2100-12-31T23:59:59.000Z',
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
mock_config_bucket = MagicMock(side_effect=Exception('取得エラー'))
mock_json_parser = MagicMock(return_value=last_fetch_datetime_dict)
mock_last_fetch_datetime = MagicMock(return_value=None)
# Act
with patch('src.aws.s3.ConfigBucket.get_last_fetch_datetime_file', mock_config_bucket), \
patch('src.parser.json_parser.JsonParser.parse', mock_json_parser), \
patch('src.config.objects.LastFetchDatetime.__init__', mock_last_fetch_datetime):
with pytest.raises(FileNotFoundException) as e:
set_datetime_period_process(target_object, execute_datetime)
# Assert
assert mock_config_bucket.called is True
assert mock_json_parser.called is False
assert e.value.args[0] == f'前回取得日時ファイルが存在しません ファイル名:[Account.json] エラー内容:[取得エラー]'
def test_raise_json_parse(self):
"""
Cases:
Jsonパース処理でエラーが発生すること
Arranges:
- Jsonパース処理で例外が発生するようにする
Expects:
- 例外が発生する
- 形式チェックが失敗したエラーが返却される
"""
# Arrange
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
]
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
mock_config_bucket = MagicMock(return_value='')
mock_json_parser = MagicMock(side_effect=Exception('変換エラー'))
mock_last_fetch_datetime = MagicMock(return_value=None)
# Act
with patch('src.aws.s3.ConfigBucket.get_last_fetch_datetime_file', mock_config_bucket), \
patch('src.parser.json_parser.JsonParser.parse', mock_json_parser), \
patch('src.config.objects.LastFetchDatetime.__init__', mock_last_fetch_datetime):
with pytest.raises(InvalidConfigException) as e:
set_datetime_period_process(target_object, execute_datetime)
# Assert
assert mock_config_bucket.called is True
assert mock_json_parser.called is True
assert e.value.args[0] == f'前回取得日時ファイルの形式チェック処理が失敗しました エラー内容:[変換エラー]'
def test_raise_check_last_fetch_datetime(self):
"""
Cases:
Jsonパース処理でエラーが発生すること
Arranges:
- Jsonパース処理で例外が発生するようにする
Expects:
- 例外が発生する
- 形式チェックが失敗したエラーが返却される
"""
# Arrange
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
]
}
last_fetch_datetime_dict = {
'last_fetch_datetime_from': '1999-01-01T00:00:00.000Z',
'last_fetch_datetime_to': '2100-12-31T23:59:59.000Z',
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
mock_config_bucket = MagicMock(return_value='')
mock_json_parser = MagicMock(return_value=last_fetch_datetime_dict)
mock_last_fetch_datetime = MagicMock(side_effect=Exception('形式エラー'))
# Act
with patch('src.aws.s3.ConfigBucket.get_last_fetch_datetime_file', mock_config_bucket), \
patch('src.parser.json_parser.JsonParser.parse', mock_json_parser), \
patch('src.config.objects.LastFetchDatetime.__init__', mock_last_fetch_datetime):
with pytest.raises(InvalidConfigException) as e:
set_datetime_period_process(target_object, execute_datetime)
# Assert
assert mock_config_bucket.called is True
assert mock_json_parser.called is True
assert mock_last_fetch_datetime.called is True
assert e.value.args[0] == f'前回取得日時ファイルの形式チェック処理が失敗しました エラー内容:[形式エラー]'

View File

@ -0,0 +1,218 @@
from unittest.mock import MagicMock, patch
import pytest
from src.config.objects import LastFetchDatetime, TargetObject
from src.error.exceptions import FileUploadException
from src.system_var.constants import UPD_JP_NAME
from src.upload_last_fetch_datetime_process import \
upload_last_fetch_datetime_process
from src.util.execute_datetime import ExecuteDateTime
from .test_utils.log_message import generate_log_message_tuple
class TestUploadLastFetchDatetimeProcess:
@pytest.fixture
def bucket_name(self):
return 'test-config-bucket'
@pytest.fixture
def prepare_bucket(self, s3_client, bucket_name):
s3_client.create_bucket(Bucket=bucket_name)
yield
def test_run_process_success(self, s3_client, prepare_bucket, bucket_name, monkeypatch, caplog):
"""
Cases:
前回取得日時ファイル更新処理が正常終了し期待通りの結果が返ること
Arranges:
- prepare_bucketフィクスチャで前回取得日時ファイルを置くためのモックバケットを作る
- モックバケットを指すように環境変数を設定する
- オブジェクト情報を準備する
- 最終更新日時インスタンスを準備する
Expects:
- PUTした前回取得日時ファイルが存在し内容が想定と一致する
- 前回取得日時ファイル更新処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arranges
# 環境変数を編集
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.LAST_FETCH_DATE_FOLDER', 'crm/last_fetch_datetime')
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
],
'is_update_last_fetch_datetime': True
}
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1999-01-01T00:00:00.000Z",
"last_fetch_datetime_to": "2100-12-31T23:59:59.000Z",
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
last_fetch_datetime = LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Act
upload_last_fetch_datetime_process(target_object, last_fetch_datetime)
# Assert
# ファイル確認
actual = s3_client.get_object(Bucket=bucket_name, Key=f'crm/last_fetch_datetime/Account.json')
assert actual['Body'].read().decode(
'utf-8') == '{"last_fetch_datetime_from": "2100-12-31T23:59:59.000Z", "last_fetch_datetime_to": ""}'
# ログの確認
assert generate_log_message_tuple(log_message='I-UPD-01 [Account] の前回取得日時ファイルの更新処理を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-UPD-02 [Account] の前回取得日時ファイルの更新処理をスキップします') not in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-UPD-04 [Account] の前回取得日時ファイルの更新処理を終了します') in caplog.record_tuples
def test_run_process_success_skip(self, s3_client, prepare_bucket, bucket_name, monkeypatch, caplog):
"""
Cases:
前回取得日時ファイル更新処理が正常終了し期待通りの結果が返ること
Arranges:
- prepare_bucketフィクスチャで前回取得日時ファイルを置くためのモックバケットを作る
- モックバケットを指すように環境変数を設定する
- ファイルPUT処理のモックを準備する
- オブジェクト情報を準備する
- 最終更新日時インスタンスを準備する
Expects:
- ファイルPUT処理が実行されないこと
- 前回取得日時ファイル更新処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arranges
# 環境変数を編集
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.LAST_FETCH_DATE_FOLDER', 'crm/last_fetch_datetime')
mock_config_bucket = MagicMock(return_value=None)
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
],
'is_update_last_fetch_datetime': False
}
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1999-01-01T00:00:00.000Z",
"last_fetch_datetime_to": "2100-12-31T23:59:59.000Z",
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
last_fetch_datetime = LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Act
with patch('src.aws.s3.ConfigBucket.put_last_fetch_datetime_file', mock_config_bucket):
upload_last_fetch_datetime_process(target_object, last_fetch_datetime)
# Assert
# 処理実行確認
assert mock_config_bucket.called is False
# ログの確認
assert generate_log_message_tuple(log_message='I-UPD-01 [Account] の前回取得日時ファイルの更新処理を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message='I-UPD-02 [Account] の前回取得日時ファイルの更新処理をスキップします') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-UPD-04 [Account] の前回取得日時ファイルの更新処理を終了します') not in caplog.record_tuples
def test_call_depended_modules(self):
"""
Cases:
前回取得日時ファイル更新処理内で依存しているモジュールが正しく呼ばれていること
Arranges:
- 前回取得日時ファイル更新処理の依存モジュールをモック化する
- オブジェクト情報を準備する
- 最終更新日時インスタンスを準備する
Expects:
- 依存しているモジュールが正しく呼ばれている
"""
# Arrange
mock_config_bucket = MagicMock(return_value=None)
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
],
'is_update_last_fetch_datetime': True
}
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1999-01-01T00:00:00.000Z",
"last_fetch_datetime_to": "2100-12-31T23:59:59.000Z",
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
last_fetch_datetime = LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Act
with patch('src.aws.s3.ConfigBucket.put_last_fetch_datetime_file', mock_config_bucket):
upload_last_fetch_datetime_process(target_object, last_fetch_datetime)
# Assert
assert mock_config_bucket.called is True
def test_raise_put_last_fetch_datetime_file(self, monkeypatch):
"""
Cases:
前回取得日時ファイルをアップロードできない場合エラーが発生すること
Arranges:
- 前回取得日時ファイル更新処理で例外を発生させるモックを生成する
- 取得処理実施結果を準備する
- 実行日時取得インスタンスを生成する
Expects:
- 例外が発生する
- ファイルがアップロードできないエラーが返却される
"""
# Arrange
mock_config_bucket = MagicMock(side_effect=Exception('ファイルアップロードエラー'))
target_objects_dict = {
'object_name': 'Account',
'columns': [
'Id',
'Name'
],
'is_update_last_fetch_datetime': True
}
last_fetch_datetime_dict = {
"last_fetch_datetime_from": "1999-01-01T00:00:00.000Z",
"last_fetch_datetime_to": "2100-12-31T23:59:59.000Z",
}
execute_datetime = ExecuteDateTime()
target_object = TargetObject(target_objects_dict, execute_datetime)
last_fetch_datetime = LastFetchDatetime(last_fetch_datetime_dict, execute_datetime)
# Act
with patch('src.aws.s3.ConfigBucket.put_last_fetch_datetime_file', mock_config_bucket):
with pytest.raises(FileUploadException) as e:
upload_last_fetch_datetime_process(target_object, last_fetch_datetime)
# Assert
assert mock_config_bucket.called is True
assert e.value.error_id == 'E-UPD-01'
assert e.value.func_name == UPD_JP_NAME
assert e.value.args[0] == f'[Account] 前回処理日時ファイルのアップロードに失敗しました ファイル名:[Account.json] エラー内容:[ファイルアップロードエラー]'

View File

@ -0,0 +1,124 @@
from unittest.mock import MagicMock, patch
import pytest
from src.error.exceptions import FileUploadException
from src.system_var.constants import END_JP_NAME
from src.upload_result_data_process import upload_result_data_process
from src.util.execute_datetime import ExecuteDateTime
from .test_utils.log_message import generate_log_message_tuple
class TestUploadResultDataProcess:
@pytest.fixture
def bucket_name(self):
return 'test-config-bucket'
@pytest.fixture
def prepare_bucket(self, s3_client, bucket_name):
s3_client.create_bucket(Bucket=bucket_name)
yield
def test_run_process_success(self, s3_client, prepare_bucket, bucket_name, monkeypatch, caplog):
"""
Cases:
取得処理実施結果アップロード処理が正常終了し期待通りの結果が返ること
Arranges:
- prepare_bucketフィクスチャで取得処理実施結果ファイルを置くためのモックバケットを作る
- モックバケットを指すように環境変数を設定する
- 取得処理実施結果を準備する
- 実行日時取得インスタンスを生成する
Expects:
- PUTした取得処理実施結果ファイルが存在し内容が想定と一致する
- 取得処理実施結果アップロード処理の仕様に沿った正常系ログが出力されること(デバッグログは除く)
"""
# Arranges
# 環境変数を編集
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', bucket_name)
monkeypatch.setattr('src.aws.s3.PROCESS_RESULT_FOLDER', 'data_import')
process_result = {
"Account": "success",
"Contact": "fail"
}
execute_datetime = ExecuteDateTime()
# Act
upload_result_data_process(process_result, execute_datetime)
# Assert
# ファイル確認
actual = s3_client.get_object(Bucket=bucket_name, Key=f'data_import/{execute_datetime.to_path()}/process_result.json')
assert actual['Body'].read().decode('utf-8') == '{"Account": "success", "Contact": "fail"}'
# ログの確認
assert generate_log_message_tuple(log_message='I-END-01 取得処理実施結果アップロード処理を開始します') in caplog.record_tuples
assert generate_log_message_tuple(log_message=f'I-END-03 取得処理実施結果アップロード処理を終了します') in caplog.record_tuples
def test_call_depended_modules(self):
"""
Cases:
取得処理実施結果アップロード処理内で依存しているモジュールが正しく呼ばれていること
Arranges:
- 取得処理実施結果アップロード処理の依存モジュールをモック化する
- 取得処理実施結果を準備する
- 実行日時取得インスタンスを生成する
Expects:
- 依存しているモジュールが正しく呼ばれている
"""
# Arrange
mock_backup_bucket = MagicMock(return_value=None)
process_result = {
"Account": "success",
"Contact": "fail"
}
execute_datetime = ExecuteDateTime()
# Act
with patch('src.aws.s3.BackupBucket.put_result_json', mock_backup_bucket):
upload_result_data_process(process_result, execute_datetime)
# Assert
assert mock_backup_bucket.called is True
def test_raise_put_result_json(self, monkeypatch):
"""
Cases:
取得処理実施結果をアップロードできない場合エラーが発生すること
Arranges:
- 取得処理実施結果アップロード処理で例外を発生させるモックを生成する
- 取得処理実施結果を準備する
- 実行日時取得インスタンスを生成する
Expects:
- 例外が発生する
- ファイルがアップロードできないエラーが返却される
"""
# Arrange
mock_backup_bucket = MagicMock(side_effect=Exception('ファイルアップロードエラー'))
process_result = {
"Account": "success",
"Contact": "fail"
}
execute_datetime = ExecuteDateTime()
# Act
with patch('src.aws.s3.BackupBucket.put_result_json', mock_backup_bucket):
with pytest.raises(FileUploadException) as e:
upload_result_data_process(process_result, execute_datetime)
# Assert
assert mock_backup_bucket.called is True
assert e.value.error_id == 'E-END-01'
assert e.value.func_name == END_JP_NAME
assert e.value.args[0] == f'取得処理実施結果のアップロードに失敗しました ファイル名:[process_result.json] エラー内容:[ファイルアップロードエラー]'

View File

@ -0,0 +1,7 @@
"""ログメッセージに関連するテストヘルパー"""
import logging
def generate_log_message_tuple(logger_name='root', log_level=logging.INFO, log_message=''):
return (logger_name, log_level, log_message)

View File

@ -0,0 +1,312 @@
import json
import logging
import os
import os.path as path
from datetime import datetime, timezone
import boto3
import pytest
from src.controller import controller
from src.parser.json_parser import JsonParser
from src.system_var.constants import YYYYMMDDTHHMMSSTZ
from src.util.execute_datetime import ExecuteDateTime
ROOT_DIR = path.abspath(path.dirname(__name__))
# ↓↓↓モックテスト用(バケットをモック化する際、コメントアウトを外す。AWS利用テスト用はコメントアウトする)↓↓↓
# DATA_BUCKET = 'mbj-newdwh2021-staging-data'
# CONFIG_BUCKET = 'mbj-newdwh2021-staging-config'
# BACKUP_BUCKET = 'mbj-newdwh2021-staging-backup-crm'
#
# @pytest.fixture
# def s3_test(s3_client):
# s3_client.create_bucket(Bucket=DATA_BUCKET)
# s3_client.create_bucket(Bucket=CONFIG_BUCKET)
# s3_client.create_bucket(Bucket=BACKUP_BUCKET)
# yield
# ↑↑↑モックテスト用↑↑↑
# ↓↓↓AWS利用テスト用(AWS上のバケットを利用する際、コメントアウトを外す。モックテスト用はコメントアウトする)↓↓↓
DATA_BUCKET = 'test-mbj-newdwh2021-test-data'
CONFIG_BUCKET = 'test-mbj-newdwh2021-test-config'
BACKUP_BUCKET = 'test-mbj-newdwh2021-test-backup-crm'
@pytest.fixture
def s3_test(s3_client):
yield
@pytest.fixture
def s3_client():
s3_client = boto3.client("s3")
yield s3_client
# ↑↑↑AWS利用テスト用↑↑↑
TARGET_FOLDER = 'crm/target'
OBJECT_INFO_FOLDER = 'crm/object_info'
LAST_FETCH_DATETIME_INFO_FOLDER = 'crm/last_fetch_datetime'
BACKUP_DATA_IMPORT_FOLDER = 'data_import'
BACKUP_RESPONSE_JSON_FOLDER = 'response_json'
PROCESS_RESULT_JSON_FILE_NAME = 'process_result.json'
@pytest.mark.walk_through
def test_walk_through(s3_test, s3_client, monkeypatch, caplog):
"""
Cases:
コントロール処理の頭から最後まで処理が流れきることを確認する
Arranges:
- 実行に必要なモックバケットを用意する
- 実行に必要なファイルをアップロードする
- 実行に必要な環境変数をmonkeypatchで設定する
- CRMデータカウント取得処理をモック化するログ出力にのみ使用しているため
Expects:
- すべてのログが仕様どおり二出力されていることデバッグログを除く
- すべてのファイルが使用どおりの場所にアップロードされていること
"""
# Arrange
# バケットにファイルをアップロードしていく
object_info_files = []
object_info_list = get_object_config_list('object_info')
for object_info in object_info_list:
json_file = read_json(object_info)
upload_json(json_file, s3_client, CONFIG_BUCKET, f'{OBJECT_INFO_FOLDER}/{path.basename(object_info)}')
object_info_files.append(json_file)
last_fetch_datetime_list = get_object_config_list('last_fetch_datetime')
for last_fetch_datetime in last_fetch_datetime_list:
json_file = read_json(last_fetch_datetime)
upload_json(json_file, s3_client, CONFIG_BUCKET, f'{LAST_FETCH_DATETIME_INFO_FOLDER}/{path.basename(last_fetch_datetime)}')
# 環境変数を設定(CRMの認証情報は別途設定しておくこと)
set_environment(monkeypatch)
# 差分取得用に環境変数を設定
monkeypatch.setattr('src.aws.s3.OBJECT_INFO_FILENAME', 'crm_object_list_diff.json')
# 一気通貫テスト用に件数の制限をかける
monkeypatch.setattr('src.salesforce.soql_builder.FETCH_LIMIT_CLAUSE', ' LIMIT 10')
# 件数取得はログ出力用なので、0件が返るようにする
monkeypatch.setattr('src.fetch_crm_data_process.fetch_record_count_retry', lambda x, y, z: 10)
# ログレベルをDEBUGに
monkeypatch.setattr('src.util.logger.LOG_LEVEL', 'DEBUG')
# 実行日時を固定する
now = datetime.now(timezone.utc).strftime(YYYYMMDDTHHMMSSTZ)
format_now = datetime.strptime(now, YYYYMMDDTHHMMSSTZ).strftime('%Y%m%d%H%M%S')
path_now = datetime.strptime(now, YYYYMMDDTHHMMSSTZ).strftime('%Y/%m/%d/%H%M%S')
class MockExecuteDateTime(ExecuteDateTime):
def __init__(self):
super().__init__()
self._ExecuteDateTime__execute_datetime = now
monkeypatch.setattr('src.prepare_data_fetch_process.ExecuteDateTime', MockExecuteDateTime)
# Act
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logger.info(f'##########################')
logger.info(f'# 差分データ取得処理:実行開始 #')
logger.info(f'##########################')
controller()
logger.info(f'##########################')
logger.info(f'# 差分データ取得処理:実行終了 #')
logger.info(f'##########################')
# Assertion
log_messages = caplog.messages
# ループ前のログ確認
assert 'I-CTRL-01 CRMデータ取得処理を開始します' in log_messages
assert 'I-CTRL-02 データ取得準備処理呼び出し' in log_messages
assert_prepare_process_log(log_messages, now, path.basename(object_info_list[0]))
assert 'I-CTRL-03 取得対象オブジェクトのループ処理開始' in log_messages
# オブジェクト情報を取得する(diff)
object_info_list = object_info_files[0]
for object_info in object_info_list['objects']:
target_object_name = object_info['object_name']
upload_file_name = f'CRM_{target_object_name}_{format_now}'
assert 'I-CTRL-05 オブジェクト情報形式チェック処理呼び出し' in log_messages
assert_check_process_log(log_messages)
assert f'I-CTRL-06 [{target_object_name}]のデータ取得を開始します' in log_messages
assert f'I-CTRL-08 [{target_object_name}]のデータ取得期間設定処理呼び出し' in log_messages
assert_period_process_log(log_messages, target_object_name, target_object_name, now)
assert f'I-CTRL-09 [{target_object_name}]のデータ取得処理呼び出し' in log_messages
assert_fetch_process_log(log_messages, target_object_name)
assert f'I-CTRL-10 [{target_object_name}] の出力ファイル名は [{upload_file_name}] となります' in log_messages
assert f'I-CTRL-11 [{target_object_name}] CRM電文データバックアップ処理呼び出し' in log_messages
assert_backup_response_process_log(log_messages, target_object_name)
assert_file_exist(s3_client, BACKUP_BUCKET, f'{BACKUP_RESPONSE_JSON_FOLDER}/{path_now}/{upload_file_name}.json')
assert f'I-CTRL-12 [{target_object_name}] CSV変換処理呼び出し' in log_messages
assert_convert_process_log(log_messages, target_object_name)
assert f'I-CTRL-13 [{target_object_name}] CSVデータバックアップ処理呼び出し' in log_messages
assert_backup_csv_process_log(log_messages, target_object_name, upload_file_name)
assert_file_exist(s3_client, BACKUP_BUCKET, f'{BACKUP_DATA_IMPORT_FOLDER}/{path_now}/{upload_file_name}.csv')
assert f'I-CTRL-14 [{target_object_name}] CSVデータアップロード処理呼び出し' in log_messages
assert_upload_csv_process_log(log_messages, target_object_name, upload_file_name)
assert_file_exist(s3_client, DATA_BUCKET, f'{TARGET_FOLDER}/{upload_file_name}.csv')
assert f'I-CTRL-15 [{target_object_name}] 前回取得日時ファイル更新処理呼び出し' in log_messages
assert_upload_fetch_datetime_process_log(log_messages, target_object_name, False)
assert_file_exist(s3_client, CONFIG_BUCKET, f'{LAST_FETCH_DATETIME_INFO_FOLDER}/{target_object_name}.json')
assert f'I-CTRL-16 [{target_object_name}] 処理正常終了' in log_messages
# ループ終了後のログの確認
process_result_json = {obj['object_name']: 'success' for obj in object_info_list['objects']}
assert f'I-CTRL-17 すべてのオブジェクトの処理が終了しました 実行結果:[{process_result_json}]' in log_messages
assert f'I-CTRL-18 CRM_取得処理実施結果ファイルアップロード処理開始' in log_messages
assert_file_exist(s3_client, BACKUP_BUCKET, f'{BACKUP_DATA_IMPORT_FOLDER}/{path_now}/{PROCESS_RESULT_JSON_FILE_NAME}')
assert f'I-CTRL-19 すべてのデータの取得に成功しました' in log_messages
assert f'I-CTRL-20 CRMデータ取得処理を終了します' in log_messages
# 全件要取得処理を実行する
# 全件取得用に環境変数を設定
monkeypatch.setattr('src.aws.s3.OBJECT_INFO_FILENAME', 'crm_object_list_all.json')
logger.info(f'##########################')
logger.info(f'# 全件データ取得処理:実行開始 #')
logger.info(f'##########################')
controller()
logger.info(f'##########################')
logger.info(f'# 全件データ取得処理:実行終了 #')
logger.info(f'##########################')
# ログ再取得
log_messages_all = caplog.messages
object_info_list_all = object_info_files[1]
# 開始ログなどはテスト済みなのでチェックを省く
for object_info in object_info_list_all['objects']:
target_object_name = object_info['object_name']
upload_file_name = f'CRM_{target_object_name}_ALL_{format_now}'
datetime_filename = f'{target_object_name}_ALL'
assert 'I-CTRL-05 オブジェクト情報形式チェック処理呼び出し' in log_messages_all
assert_check_process_log(log_messages_all)
assert f'I-CTRL-06 [{target_object_name}]のデータ取得を開始します' in log_messages_all
assert f'I-CTRL-08 [{target_object_name}]のデータ取得期間設定処理呼び出し' in log_messages_all
assert_period_process_log(log_messages_all, target_object_name, datetime_filename, now)
assert f'I-CTRL-09 [{target_object_name}]のデータ取得処理呼び出し' in log_messages_all
assert_fetch_process_log(log_messages_all, target_object_name)
assert f'I-CTRL-10 [{target_object_name}] の出力ファイル名は [{upload_file_name}] となります' in log_messages_all
assert f'I-CTRL-11 [{target_object_name}] CRM電文データバックアップ処理呼び出し' in log_messages_all
assert_backup_response_process_log(log_messages_all, target_object_name)
assert_file_exist(s3_client, BACKUP_BUCKET, f'{BACKUP_RESPONSE_JSON_FOLDER}/{path_now}/{upload_file_name}.json')
assert f'I-CTRL-12 [{target_object_name}] CSV変換処理呼び出し' in log_messages_all
assert_convert_process_log(log_messages_all, target_object_name)
assert f'I-CTRL-13 [{target_object_name}] CSVデータバックアップ処理呼び出し' in log_messages_all
assert_backup_csv_process_log(log_messages_all, target_object_name, upload_file_name)
assert_file_exist(s3_client, BACKUP_BUCKET, f'{BACKUP_DATA_IMPORT_FOLDER}/{path_now}/{upload_file_name}.csv')
assert f'I-CTRL-14 [{target_object_name}] CSVデータアップロード処理呼び出し' in log_messages_all
assert_upload_csv_process_log(log_messages_all, target_object_name, upload_file_name)
assert_file_exist(s3_client, DATA_BUCKET, f'{TARGET_FOLDER}/{upload_file_name}.csv')
assert f'I-CTRL-15 [{target_object_name}] 前回取得日時ファイル更新処理呼び出し' in log_messages_all
assert_upload_fetch_datetime_process_log(log_messages_all, target_object_name, True)
assert_file_exist(s3_client, CONFIG_BUCKET, f'{LAST_FETCH_DATETIME_INFO_FOLDER}/{target_object_name}.json')
assert f'I-CTRL-16 [{target_object_name}] 処理正常終了' in log_messages_all
"""
以下アサーション関数
"""
def assert_prepare_process_log(log_messages, now, object_info_list_filename):
assert 'I-PRE-01 データ取得準備処理を開始します' in log_messages
assert f'I-PRE-02 データ取得処理開始日時:{now}' in log_messages
assert f'I-PRE-03 CRM_取得オブジェクト情報ファイルの取得開始します ファイルパス:[s3://{CONFIG_BUCKET}/{OBJECT_INFO_FOLDER}/{object_info_list_filename}]' in log_messages
assert 'I-PRE-09 データ取得準備処理を終了します' in log_messages
def assert_check_process_log(log_messages):
assert 'I-CHK-01 オブジェクト情報形式チェック処理を開始します' in log_messages
assert 'I-CHK-02 オブジェクト情報形式チェック処理を終了します' in log_messages
def assert_period_process_log(log_messages, target_object_name, datetime_file_name, now):
assert f'I-DATE-01 [{target_object_name}] のデータ取得期間設定処理を開始します' in log_messages
assert f'I-DATE-02 前回取得日時ファイルの取得開始します ファイルパス:[s3://{CONFIG_BUCKET}/{LAST_FETCH_DATETIME_INFO_FOLDER}/{datetime_file_name}.json]' in log_messages
assert 'I-DATE-03 前回取得日時ファイルの取得成功しました' in log_messages
assert f'I-DATE-06 取得範囲 From: [1900-01-01T00:00:00.000Z] To: [{now}]' in log_messages
assert f'I-DATE-07 [{target_object_name}] のデータ取得期間設定処理を終了します' in log_messages
def assert_fetch_process_log(log_messages, target_object_name):
assert f'I-FETCH-01 [{target_object_name}] のCRMからのデータ取得処理を開始します' in log_messages
assert f'I-FETCH-02 [{target_object_name}] の件数取得を開始します' in log_messages
assert f'I-FETCH-03 [{target_object_name}] の件数:[10]' in log_messages
assert f'I-FETCH-04 [{target_object_name}] のレコード取得を開始します' in log_messages
assert f'I-FETCH-05 [{target_object_name}] のレコード取得が成功しました' in log_messages
assert f'I-FETCH-06 [{target_object_name}] のCRMからのデータ取得処理を終了します' in log_messages
def assert_backup_response_process_log(log_messages, target_object_name):
assert f'I-RESBK-01 [{target_object_name}] のCRM電文データバックアップ処理を開始します' in log_messages
assert f'I-RESBK-03 [{target_object_name}] のCRM電文データバックアップ処理を終了します' in log_messages
def assert_convert_process_log(log_messages, target_object_name):
assert f'I-CONV-01 [{target_object_name}] のCSV変換処理を開始します' in log_messages
assert f'I-CONV-03 [{target_object_name}] のCSV変換処理を終了します' in log_messages
def assert_backup_csv_process_log(log_messages, target_object_name, upload_file_name):
assert f'I-CSVBK-01 [{target_object_name}] のCSVデータのバックアップ処理を開始します ファイル名:[{upload_file_name}.csv]' in log_messages
assert f'I-CSVBK-03 [{target_object_name}] のCSVデータのバックアップ処理を終了します' in log_messages
def assert_upload_csv_process_log(log_messages, target_object_name, upload_file_name):
assert f'I-UPLD-01 [{target_object_name}] のCSVデータアップロード処理を開始します ファイル名:[{upload_file_name}.csv]' in log_messages
assert f'I-UPLD-03 [{target_object_name}] のCSVデータのアップロード処理を終了します' in log_messages
def assert_upload_fetch_datetime_process_log(log_messages, target_object_name, is_all=False):
assert f'I-UPD-01 [{target_object_name}] の前回取得日時ファイルの更新処理を開始します' in log_messages
if is_all:
assert f'I-UPD-02 [{target_object_name}] の前回取得日時ファイルの更新処理をスキップします' in log_messages
return
assert f'I-UPD-04 [{target_object_name}] の前回取得日時ファイルの更新処理を終了します' in log_messages
def assert_file_exist(s3_client, bucket_name, file_key):
try:
assert s3_client.head_object(Bucket=bucket_name, Key=file_key) is not None
except Exception:
raise Exception(f'ファイルが存在しません バケット名:{bucket_name}, ファイルパス:{file_key}')
"""
以下取得準備関数
"""
def get_object_config_list(folder_name: str):
local_s3_path = path.join(ROOT_DIR, '..', '..', 's3', 'config', 'crm', folder_name)
config_list = [os.path.join(local_s3_path, config) for config in os.listdir(local_s3_path) if config.endswith('.json')]
return sorted(config_list, reverse=True)
def read_json(json_path):
with open(json_path, 'r', encoding='utf8') as f:
json_str = f.read()
JsonParser
json_file = JsonParser(json_str).parse()
return json_file
def upload_json(json_file, s3_client, bucket, folder):
json_str = json.dumps(json_file)
s3_client.put_object(Bucket=bucket, Key=folder, Body=json_str)
def set_environment(monkeypatch):
# 環境変数を設定(CRMの認証情報は別途設定しておくこと)
monkeypatch.setattr('src.aws.s3.IMPORT_DATA_BUCKET', DATA_BUCKET)
monkeypatch.setattr('src.aws.s3.CRM_IMPORT_DATA_FOLDER', TARGET_FOLDER)
monkeypatch.setattr('src.aws.s3.CRM_CONFIG_BUCKET', CONFIG_BUCKET)
monkeypatch.setattr('src.aws.s3.OBJECT_INFO_FOLDER', OBJECT_INFO_FOLDER)
monkeypatch.setattr('src.aws.s3.LAST_FETCH_DATE_FOLDER', LAST_FETCH_DATETIME_INFO_FOLDER)
monkeypatch.setattr('src.aws.s3.CRM_BACKUP_BUCKET', BACKUP_BUCKET)
monkeypatch.setattr('src.upload_result_data_process.PROCESS_RESULT_FILENAME', PROCESS_RESULT_JSON_FILE_NAME)
monkeypatch.setattr('src.copy_crm_csv_data_process.CRM_IMPORT_DATA_BACKUP_FOLDER', BACKUP_DATA_IMPORT_FOLDER)
monkeypatch.setattr('src.set_datetime_period_process.CRM_CONFIG_BUCKET', CONFIG_BUCKET)
monkeypatch.setattr('src.aws.s3.CRM_IMPORT_DATA_BACKUP_FOLDER', BACKUP_DATA_IMPORT_FOLDER)
monkeypatch.setattr('src.aws.s3.PROCESS_RESULT_FOLDER', BACKUP_DATA_IMPORT_FOLDER)
monkeypatch.setattr('src.aws.s3.RESPONSE_JSON_BACKUP_FOLDER', BACKUP_RESPONSE_JSON_FOLDER)
monkeypatch.setattr('src.prepare_data_fetch_process.CRM_CONFIG_BUCKET', CONFIG_BUCKET)

View File

@ -0,0 +1,213 @@
import pytest
from src.util.counter_object import CounterObject
class TestCounterObject:
def test_constructor(self) -> int:
"""
Cases:
カウンターオブジェクト生成時に数値を渡すと例外が発生しないこと
Arranges:
なし
Expects:
例外が発生しないこと
"""
# Act
CounterObject(1)
# Expects
pass
def test_constructor_string_number(self) -> int:
"""
Cases:
カウンターオブジェクト生成時に文字列型の数値を渡すと例外が発生しないこと
Arranges:
なし
Expects:
例外が発生しないこと
"""
# Act
CounterObject("1")
# Expects
pass
def test_raise_constructor_string(self) -> int:
"""
Cases:
カウンターオブジェクト生成時に文字列を渡すと例外が発生すること
Arranges:
なし
Expects:
発生した例外が期待値と一致すること
"""
# Act
with pytest.raises(Exception) as e:
CounterObject("test1")
# Expects
assert "invalid literal for int() with base 10:" in str(e.value)
def test_describe(self) -> int:
"""
Cases:
カウンターオブジェクトにて保持した値を返すこと(インスタンス生成時引数なし)
Arranges:
なし
Expects:
問い合わせた値が期待値と一致する
"""
# Act
sut = CounterObject()
actual = sut.describe()
# Expects
assert actual == 1
def test_describe_argument(self) -> int:
"""
Cases:
カウンターオブジェクトにて保持した値を返すこと(インスタンス生成時引数あり)
Arranges:
なし
Expects:
問い合わせた値が期待値と一致する
"""
# Act
sut = CounterObject(3)
actual = sut.describe()
# Expects
assert actual == 3
def test_raise_describe(self) -> int:
"""
Cases:
カウンターオブジェクトの保持した値を問い合わせる際引数を渡すと例外が発生すること
Arranges:
なし
Expects:
問い合わせた値が期待値と一致する
"""
# Act
with pytest.raises(Exception) as e:
sut = CounterObject()
sut.describe(1)
# Expects
assert str(e.value) == 'describe() takes 1 positional argument but 2 were given'
def test_increment(self) -> int:
"""
Cases:
カウンターオブジェクトにて保持した値がインクリメントされていること(引数なし)
Arranges:
なし
Expects:
戻り値が期待値と一致する
"""
# Act
sut = CounterObject()
sut.increment()
actual = sut.increment()
# Expects
assert actual == 3
def test_increment_argument(self) -> int:
"""
Cases:
カウンターオブジェクトにて保持した値がインクリメントされていること(引数あり)
Arranges:
なし
Expects:
戻り値が期待値と一致する
"""
# Act
sut = CounterObject(5)
sut.increment(2)
actual = sut.increment(2)
# Expects
assert actual == 9
def test_raise_increment(self) -> int:
"""
Cases:
文字列を引数で渡すことで例外が発生すること
Arranges:
なし
Expects:
発生した例外が期待値と一致する
"""
# Act
with pytest.raises(Exception) as e:
sut = CounterObject(5)
sut.increment('aaa')
sut.increment('aaa')
# Expects
assert str(e.value) == "unsupported operand type(s) for +=: 'int' and 'str'"
def test_decrement(self) -> int:
"""
Cases:
カウンターオブジェクトにて保持した値がデクリメントされていること(引数なし)
Arranges:
なし
Expects:
戻り値が期待値と一致する
"""
# Act
sut = CounterObject()
sut.decrement()
actual = sut.decrement()
# Expects
assert actual == -1
def test_decrement_argument(self) -> int:
"""
Cases:
カウンターオブジェクトにて保持した値がデクリメントされていること(引数あり)
Arranges:
なし
Expects:
戻り値が期待値と一致する
"""
# Act
sut = CounterObject(5)
sut.decrement(2)
actual = sut.decrement(2)
# Expects
assert actual == 1
def test_raise_decrement(self) -> int:
"""
Cases:
文字列を引数で渡すことで例外が発生すること
Arranges:
なし
Expects:
発生した例外が期待値と一致する
"""
# Act
with pytest.raises(Exception) as e:
sut = CounterObject(5)
sut.decrement('aaa')
sut.decrement('aaa')
# Expects
assert str(e.value) == "unsupported operand type(s) for -=: 'int' and 'str'"

View File

@ -0,0 +1,361 @@
import pytest
from src.util.dict_checker import DictChecker
class TestDictChecker:
@pytest.mark.parametrize('test_data,expect', [
('', True),
(None, True),
('test_data', False),
])
def test_is_empty(self, test_data, expect):
"""
Cases:
1. 辞書型データの対象のキーの値が''(空文字)の場合Trueが返る
2. 辞書型データの対象のキーの値がNoneの場合Trueが返る
3. 辞書型データの対象のキーの値が存在する場合Falseが返る
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
Expects:
- 戻り値が期待値と一致する
"""
# Arranges
object_dict = {
"test_key": test_data
}
sut = DictChecker(object_dict)
# Act
actual = sut.is_empty("test_key")
# Expects
assert actual is expect
@pytest.mark.parametrize('test_data,expect', [
([''], False),
([None], False),
(['test_value'], False),
([], True),
('test_value', False)
])
def test_is_list_empty(self, test_data, expect):
"""
Cases:
1. 辞書型のデータの対象のキーのリストの値が''(空文字)の場合Falseが返る
2. 辞書型データの対象のキーのリストの値がNoneの場合Falseが返る
3. 辞書型データの対象のキーのリストの値が存在する場合Falseが返る
4. 辞書型データの対象のキーのリストの値が[]()の場合Trueが返る
5. 辞書型データの対象のキーの値に文字列が存在する場合Falseが返る
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
Expects:
- 戻り値が期待値と一致する
"""
# Arranges
object_dict = {
"test_key": test_data
}
sut = DictChecker(object_dict)
# Act
actual = sut.is_list_empty("test_key")
# Expects
assert actual is expect
@pytest.mark.parametrize('test_data,expect', [
({"test_key": "test_value"}, True),
({}, False),
({"test_key": ""}, False),
])
def test_check_key_exist(self, test_data, expect) -> bool:
"""
Cases:
1. 辞書型データの対象のキーが存在するかつ値が存在する場合Trueが返る
2. 辞書型データの対象のキーが存在しない場合Falseが返る
3. 辞書型データの対象のキーの値が存在しない場合Falseが返る
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
Expects:
- 戻り値が期待値と一致する
"""
# Arranges
object_dict = test_data
sut = DictChecker(object_dict)
# Act
actual = sut.check_key_exist("test_key")
# Expects
assert actual is expect
@pytest.mark.parametrize('test_data,test_type,expect', [
({"test_key": "test_value"}, str, True),
({"test_key": 1}, str, False)
])
def test_check_data_type(self, test_data, test_type, expect) -> bool:
"""
Cases:
1. 辞書型データの対象のキーの値の型が指定した型と一致した場合Trueが返る
2. 辞書型データの対象のキーの値の型が指定した型と一致しない場合Falseが返る
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
Expects:
- 戻り値が期待値と一致する
"""
# Arranges
object_dict = test_data
sut = DictChecker(object_dict)
# Act
actual = sut.check_data_type("test_key", test_type)
# Expects
assert actual is expect
@pytest.mark.parametrize('test_data,expect', [
({"test_key": "2022-08-01T10:00:00.000Z"}, True),
({"test_key": "test_value"}, False)
])
def test_check_match_pattern(self, test_data, expect) -> bool:
"""
Cases:
1. 辞書型データの対象のキーの値の型が指定した正規表現と一致した場合Trueが返る
2. 辞書型データの対象のキーの値の型が指定した正規表現と一致しない場合Falseが返る
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
- 正規表現パターンの生成
Expects:
- 戻り値が期待値と一致する
"""
# Arranges
object_dict = test_data
sut = DictChecker(object_dict)
pattern = r'[12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.000Z'
# Act
actual = sut.check_match_pattern(pattern, "test_key")
# Expects
assert actual is expect
def test_assert_key_exist(self) -> None:
"""
Cases:
辞書型データの対象のキーの値が存在する場合例外が発生しないこと
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
Expects:
- 例外が発生しないこと
"""
# Arranges
object_dict = {
"test_key": "test_value"
}
sut = DictChecker(object_dict)
# Act
sut.assert_key_exist("test_key")
# Expects
pass
def test_raise_assert_key_exist(self) -> None:
"""
Cases:
辞書型データの対象のキーの値が存在しない場合例外が発生すること
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
Expects:
- 発生した例外が期待値と一致すること
"""
# Arranges
object_dict = {
"test_key": ""
}
sut = DictChecker(object_dict)
# Act
with pytest.raises(Exception) as e:
sut.assert_key_exist("test_key")
# Expects
assert str(e.value) == '「test_key」キーは必須です'
def test_assert_data_type(self) -> None:
"""
Cases:
辞書型データの対象のキーの値の型が指定した型と一致した場合例外が発生しないこと
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
Expects:
- 例外が発生しないこと
"""
# Arranges
object_dict = {
"test_key": "test_value"
}
sut = DictChecker(object_dict)
# Act
sut.assert_data_type("test_key", str)
# Expects
pass
def test_raise_assert_data_type(self) -> None:
"""
Cases:
辞書型データの対象のキーの値の型が指定した型と一致しない場合例外が発生すること
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
Expects:
- 発生した例外が期待値と一致すること
"""
# Arranges
object_dict = {
"test_key": 1
}
sut = DictChecker(object_dict)
# Act
with pytest.raises(Exception) as e:
sut.assert_data_type("test_key", str)
# Expects
assert str(e.value) == "「test_key」キーの値は「<class 'str'>」でなければなりません"
def test_assert_match_pattern(self):
"""
Cases:
辞書型データの対象のキーの値の型が指定した正規表現と一致した場合例外が発生しないこと
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
- 正規表現パターンの生成
Expects:
- 例外が発生しないこと
"""
# Arranges
object_dict = {
"test_key": "2022-08-01T10:00:00.000Z"
}
sut = DictChecker(object_dict)
pattern = r'[12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.000Z'
# Act
sut.assert_match_pattern("test_key", pattern, 'YYYY-MM-DDTHH:MM:SS.000Z')
# Expects
pass
def test_raise_assert_match_pattern(self):
"""
Cases:
辞書型データの対象のキーの値の型が指定した正規表現と一致しない場合例外が発生すること
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
- 正規表現パターンの生成
Expects:
- 発生した例外が期待値と一致すること
"""
# Arranges
object_dict = {
"test_key": "test_value"
}
sut = DictChecker(object_dict)
pattern = r'[12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.000Z'
# Act
with pytest.raises(Exception) as e:
sut.assert_match_pattern("test_key", pattern, 'YYYY-MM-DDTHH:MM:SS.000Z')
assert str(e.value) == '「test_key」キーの値の正規表現チェックに失敗しました 「YYYY-MM-DDTHH:MM:SS.000Z」形式である必要があります'
def test_assert_list_empty(self):
"""
Cases:
辞書型データの対象のキーのリストの値が存在する場合例外が発生しないこと
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
Expects:
- 例外が発生しないこと
"""
# Arranges
object_dict = {
"test_key": [
"test_value"
]
}
sut = DictChecker(object_dict)
# Act
sut.assert_list_empty("test_key")
# Expects
pass
def test_raise_assert_list_empty(self):
"""
Cases:
辞書型データの対象のキーのリストの値が存在しない場合例外が発生すること
Arranges:
- 辞書型のオブジェクト情報を準備する
- 辞書型チェックインスタンスを生成する
Expects:
- 発生した例外が期待値と一致すること
"""
# Arranges
object_dict = {
"test_key": [
]
}
sut = DictChecker(object_dict)
# Act
with pytest.raises(Exception) as e:
sut.assert_list_empty("test_key")
# Expects
assert str(e.value) == '「test_key」キーのリストの値は必須です'

View File

@ -0,0 +1,81 @@
import re
from datetime import datetime, timezone
from src.util.execute_datetime import ExecuteDateTime
class TestExecuteDateTime:
def test_constructor(self):
"""
Cases:
インスタンス生成テスト
UTC時刻が文字列で設定されるかのチェック
Arranges:
- 現在時刻の保持
- 期待される値の正規表現作成
Expects:
- 作成されたクラスが保持している時刻が期待値と一致すること
- 作成されたクラスが保持している時刻が正規表現と一致すること
"""
# Arranges
datetime_now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z')
pattern = r'[12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\.000Z'
# Expects
sut = ExecuteDateTime()
assert str(sut) == datetime_now
assert re.fullmatch(pattern, str(sut))
def test_to_path(self) -> str:
"""
Cases:
生成されたパスがYYYYMMDD/HHMMSSの形式で出力されること
Arranges:
- 現在時刻をパスに変換する
- 期待される値の正規表現作成
Expects:
- 戻り値が期待値と一致する
- 戻り値が正規表現と一致すること
"""
# Arranges
datetime_now_path = datetime.now(timezone.utc).strftime(
'%Y-%m-%dT%H:%M:%S.000Z').rstrip('000Z').translate(str.maketrans({'-': '/', 'T': '/', ':': None, '.': None}))
pattern = r'[12]\d{3}\/(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/([01][0-9]|2[0-3])[0-5][0-9][0-5][0-9]'
# Expects
sut = ExecuteDateTime()
actual = sut.to_path()
assert actual == datetime_now_path
assert re.fullmatch(pattern, actual)
def test_format_date(self) -> str:
"""
Cases:
生成されたパスがYYYYMMDDHHMMSSの形式で出力されること
Arranges:
- 現在時刻をパスに変換する
- 期待される値の正規表現作成
Expects:
- 戻り値が期待値と一致する
- 戻り値が正規表現と一致すること
"""
# Arranges
datetime_now_path = datetime.now(timezone.utc).strftime(
'%Y-%m-%dT%H:%M:%S.000Z').rstrip('000Z').translate(str.maketrans({'-': None, 'T': None, ':': None, '.': None}))
pattern = r'[12]\d{3}(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])([01][0-9]|2[0-3])[0-5][0-9][0-5][0-9]'
# Expects
sut = ExecuteDateTime()
actual = sut.format_date()
assert actual == datetime_now_path
assert re.fullmatch(pattern, actual)

View File

@ -0,0 +1,78 @@
import logging
from logging import handlers
from unittest.mock import MagicMock, patch
from src.util.logger import Logger
class TestLogger:
def test_constructor_loglevel_test(self, caplog, monkeypatch):
"""
Cases:
ログレベルに対象外の文字列を指定した場合デフォルトであるInfoが指定されること
Arranges:
- LOG_LEVELの変数を想定外の文字列に置き換える
- loggerインスタンスの生成
- ログ出力
Expects:
- 期待値通りのログが出力されること
- ログレベルがInfoで設定されていること
"""
# Arranges
monkeypatch.setattr('src.util.logger.LOG_LEVEL', "test")
sut = Logger()
logger = sut.get_logger()
logger.info('infoログ出力')
# Expects
assert ("root", logging.INFO, "infoログ出力") in caplog.record_tuples
assert logger.getEffectiveLevel() == logging.INFO
def test_constructor_hashandlers(self, monkeypatch):
"""
Cases:
生成したloggerインスタンスを利用し期待値通りのログが出力されること
Arranges:
- `if not self.__logger.hasHandlers():`の分岐に入るようにモックを準備
- loggerインスタンスの生成
- ログ出力
Expects:
- loggerインスタンスに1件以上のハンドラーが存在すること
- loggerインスタンスにStreamHandlerが含まれていること
"""
# Arranges
def dummy_method(arg):
return False
monkeypatch.setattr("logging.Logger.hasHandlers", dummy_method)
sut = Logger()
logger = sut.get_logger()
logger.warning('warningログ出力')
# Expects
assert len(logger.handlers) >= 1
assert "StreamHandler" in str(logger.handlers)
def test_get_logger(self, caplog):
"""
Cases:
生成したloggerインスタンスを利用し期待値通りのログが出力されること
Arranges:
- loggerインスタンスの生成
- ログ出力
Expects:
- 期待値通りのログが出力されること
"""
# Arranges
sut = Logger()
logger = sut.get_logger()
logger.warning('warningログ出力')
# Expects
assert ("root", logging.WARNING, "warningログ出力") in caplog.record_tuples

View File

@ -0,0 +1,193 @@
import logging
from src.util.logger import logger_instance as logger
class TestLogger:
def test_logging_debug(self, caplog):
"""
Cases:
Debugレベルを設定したloggerについてそれ以下のログが出力されないこと
Arranges:
- 各レベルでのログを出力する
- ログレベルの設定
Expects:
- 期待値通りのログが出力されること
"""
# Arranges
caplog.set_level(logging.DEBUG)
logger.debug('debugログ出力')
logger.info('infoログ出力')
logger.warning("warningログ出力")
logger.error("errorログ出力")
logger.critical("criticalログ出力")
# Expects
assert ("root", logging.DEBUG, "debugログ出力") in caplog.record_tuples
assert ("root", logging.INFO, "infoログ出力") in caplog.record_tuples
assert ("root", logging.WARNING, "warningログ出力") in caplog.record_tuples
assert ("root", logging.ERROR, "errorログ出力") in caplog.record_tuples
assert ("root", logging.CRITICAL, "criticalログ出力") in caplog.record_tuples
def test_logging_info(self, caplog):
"""
Cases:
Infoレベルを設定したloggerについてそれ以下のログが出力されないこと
Arranges:
- 各レベルでのログを出力する
- ログレベルの設定
Expects:
- 期待値通りのログが出力されること
"""
# Arranges
caplog.set_level(logging.INFO)
logger.debug('debugログ出力')
logger.info('infoログ出力')
logger.warning("warningログ出力")
logger.error("errorログ出力")
logger.critical("criticalログ出力")
# Expects
assert ("root", logging.DEBUG, "debugログ出力") not in caplog.record_tuples
assert ("root", logging.INFO, "infoログ出力") in caplog.record_tuples
assert ("root", logging.WARNING, "warningログ出力") in caplog.record_tuples
assert ("root", logging.ERROR, "errorログ出力") in caplog.record_tuples
assert ("root", logging.CRITICAL, "criticalログ出力") in caplog.record_tuples
def test_logging_warning(self, caplog):
"""
Cases:
Warningレベルを設定したloggerについてそれ以下のログが出力されないこと
Arranges:
- 各レベルでのログを出力する
- ログレベルの設定
Expects:
- 期待値通りのログが出力されること
"""
# Arranges
caplog.set_level(logging.WARNING)
logger.debug('debugログ出力')
logger.info('infoログ出力')
logger.warning("warningログ出力")
logger.error("errorログ出力")
logger.critical("criticalログ出力")
# Expects
assert ("root", logging.DEBUG, "debugログ出力") not in caplog.record_tuples
assert ("root", logging.INFO, "infoログ出力") not in caplog.record_tuples
assert ("root", logging.WARNING, "warningログ出力") in caplog.record_tuples
assert ("root", logging.ERROR, "errorログ出力") in caplog.record_tuples
assert ("root", logging.CRITICAL, "criticalログ出力") in caplog.record_tuples
def test_logging_error(self, caplog):
"""
Cases:
Errorレベルを設定したloggerについてそれ以下のログが出力されないこと
Arranges:
- 各レベルでのログを出力する
- ログレベルの設定
Expects:
- 期待値通りのログが出力されること
"""
# Arranges
caplog.set_level(logging.ERROR)
logger.debug('debugログ出力')
logger.info('infoログ出力')
logger.warning("warningログ出力")
logger.error("errorログ出力")
logger.critical("criticalログ出力")
# Expects
assert ("root", logging.DEBUG, "debugログ出力") not in caplog.record_tuples
assert ("root", logging.INFO, "infoログ出力") not in caplog.record_tuples
assert ("root", logging.WARNING, "warningログ出力") not in caplog.record_tuples
assert ("root", logging.ERROR, "errorログ出力") in caplog.record_tuples
assert ("root", logging.CRITICAL, "criticalログ出力") in caplog.record_tuples
def test_logging_critical(self, caplog):
"""
Cases:
Criticalレベルを設定したloggerについてそれ以下のログが出力されないこと
Arranges:
- 各レベルでのログを出力する
- ログレベルの設定
Expects:
- 期待値通りのログが出力されること
"""
# Arranges
caplog.set_level(logging.CRITICAL)
logger.debug('debugログ出力')
logger.info('infoログ出力')
logger.warning("warningログ出力")
logger.error("errorログ出力")
logger.critical("criticalログ出力")
# Expects
assert ("root", logging.DEBUG, "debugログ出力") not in caplog.record_tuples
assert ("root", logging.INFO, "infoログ出力") not in caplog.record_tuples
assert ("root", logging.WARNING, "warningログ出力") not in caplog.record_tuples
assert ("root", logging.ERROR, "errorログ出力") not in caplog.record_tuples
assert ("root", logging.CRITICAL, "criticalログ出力") in caplog.record_tuples
def test_logging_getlogger_boto3(self):
"""
Cases:
個別設定したboto3モジュールのログレベル確認
Arranges:
なし
Expects:
設定されているログレベルが期待値と一致すること
"""
# Expects
assert logging.getLogger("boto3").getEffectiveLevel() == logging.WARNING
def test_logging_getlogger_botocore(self):
"""
Cases:
個別設定したbotocoreモジュールのログレベル確認
Arranges:
なし
Expects:
設定されているログレベルが期待値と一致すること
"""
# Expects
assert logging.getLogger("botocore").getEffectiveLevel() == logging.WARNING
def test_logging_getlogger_s3transfer(self):
"""
Cases:
個別設定したs3transferモジュールのログレベル確認
Arranges:
なし
Expects:
設定されているログレベルが期待値と一致すること
"""
# Expects
assert logging.getLogger("s3transfer").getEffectiveLevel() == logging.WARNING
def test_logging_getlogger_urllib3(self):
"""
Cases:
個別設定したurllib3モジュールのログレベル確認
Arranges:
なし
Expects:
設定されているログレベルが期待値と一致すること
"""
# Expects
assert logging.getLogger("s3transfer").getEffectiveLevel() == logging.WARNING

View File

@ -0,0 +1,46 @@
-- A5M2で実行時に[SQL] - [スラッシュ(/)のみの行でSQLを区切る]に変えてから実行する
-- $$から始まる文字は後からREPLACEする文字を示す独自ルール
-- crm_data_syncストアドプロシージャは、同一セッション内での並列処理を実行することができない
CREATE PROCEDURE src02.crm_data_sync(target_table VARCHAR(255), target_table_all VARCHAR(255), target_column VARCHAR(255))
BEGIN
-- 例外処理
-- エラーが発生した場合に一時テーブルの削除を実施
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
@error_state = RETURNED_SQLSTATE, @error_msg = MESSAGE_TEXT;
ROLLBACK;
SIGNAL SQLSTATE '45000'
SET MYSQL_ERRNO = @error_state, MESSAGE_TEXT = @error_msg;
END;
SET @error_state = NULL, @error_msg = NULL;
START TRANSACTION;
-- ①-1 Salesforce側で物理削除されたデータを検出し更新する
SET @update_end_datetime = '
UPDATE $$target_table$$ tt
SET
tt.end_datetime = CURRENT_TIMESTAMP ()
, tt.upd_user = CURRENT_USER ()
, tt.upd_date = CURRENT_TIMESTAMP ()
WHERE
tt.end_datetime = "9999-12-31 00:00:00"
AND NOT EXISTS (
SELECT
tta.id
FROM
$$target_table_all$$ tta
WHERE
tt.id = tta.id
AND tt.$$target_column$$ = tta.$$target_column$$
)
';
SET @update_end_datetime = REPLACE(@update_end_datetime, "$$target_table$$", target_table);
SET @update_end_datetime = REPLACE(@update_end_datetime, "$$target_table_all$$", target_table_all);
SET @update_end_datetime = REPLACE(@update_end_datetime, "$$target_column$$", target_column);
PREPARE update_end_datetime_stmt from @update_end_datetime;
EXECUTE update_end_datetime_stmt;
COMMIT;
END

View File

@ -0,0 +1,93 @@
-- A5M2で実行時に[SQL] - [スラッシュ(/)のみの行でSQLを区切る]に変えてから実行する
-- $$から始まり$$で終わる文字は後からREPLACEする文字を示す独自ルール
-- crm_historyストアドプロシージャは、同一セッション内での並列処理を実行することができない
CREATE PROCEDURE src02.crm_history(target_table VARCHAR(255), target_column VARCHAR(255))
BEGIN
-- 例外処理
-- エラーが発生した場合に一時テーブルの削除を実施
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
@error_state = RETURNED_SQLSTATE, @error_msg = MESSAGE_TEXT;
EXECUTE drop_tmp_table_stmt;
DEALLOCATE PREPARE drop_tmp_table_stmt;
ROLLBACK;
SIGNAL SQLSTATE '45000'
SET MYSQL_ERRNO = @error_state, MESSAGE_TEXT = @error_msg;
END;
SET @error_state = NULL, @error_msg = NULL;
START TRANSACTION;
-- ②-3で利用する一時テーブル削除のSQLを準備
-- 例外処理でも利用するため先に記述
SET @drop_tmp_table = '
DROP TEMPORARY TABLE IF EXISTS $$target_table$$_make_history_tmp
';
SET @drop_tmp_table = REPLACE(@drop_tmp_table, "$$target_table$$", target_table);
PREPARE drop_tmp_table_stmt from @drop_tmp_table;
-- ①-1 Salesforce側で更新されたデータの適用開始日時と適用終了日時を設定する
SET @new_history_save = '
UPDATE $$target_table$$
SET
start_datetime = $$target_column$$
, end_datetime = "9999-12-31 00:00:00"
, upd_user = CURRENT_USER()
, upd_date = CURRENT_TIMESTAMP()
WHERE
start_datetime IS NULL
AND end_datetime IS NULL
';
SET @new_history_save = REPLACE(@new_history_save, "$$target_table$$", target_table);
SET @new_history_save = REPLACE(@new_history_save, "$$target_column$$", target_column);
PREPARE new_history_save_stmt from @new_history_save;
EXECUTE new_history_save_stmt;
DEALLOCATE PREPARE new_history_save_stmt;
-- ②-1 Salesforce側で更新されたデータを検出用一時テーブルの作成
SET @make_history_tmp_create = '
CREATE TEMPORARY TABLE $$target_table$$_make_history_tmp
SELECT
id
, MIN($$target_column$$) AS min_start_datetime
, MAX(start_datetime) AS max_start_datetime
FROM
$$target_table$$
WHERE
end_datetime = "9999-12-31 00:00:00"
GROUP BY
id
HAVING
count(id) = 2
';
SET @make_history_tmp_create = REPLACE(@make_history_tmp_create, "$$target_table$$", target_table);
SET @make_history_tmp_create = REPLACE(@make_history_tmp_create, "$$target_column$$", target_column);
PREPARE make_history_tmp_create_stmt from @make_history_tmp_create;
EXECUTE make_history_tmp_create_stmt;
DEALLOCATE PREPARE make_history_tmp_create_stmt;
-- ②-2 「②-1」で取得した全件に更新処理を行う
SET @update_end_datetime = '
UPDATE $$target_table$$ tt
INNER JOIN $$target_table$$_make_history_tmp mht
ON tt.id = mht.id
AND tt.start_datetime = mht.min_start_datetime
SET
end_datetime = mht.max_start_datetime - INTERVAL 1 SECOND
, upd_user = CURRENT_USER()
, upd_date = CURRENT_TIMESTAMP()
';
SET @update_end_datetime = REPLACE(@update_end_datetime, "$$target_table$$", target_table);
SET @update_end_datetime = REPLACE(@update_end_datetime, "$$target_column$$", target_column);
PREPARE update_end_datetime_stmt from @update_end_datetime;
EXECUTE update_end_datetime_stmt;
DEALLOCATE PREPARE update_end_datetime_stmt;
-- ②-3 「②-1」で作成した一時テーブルを削除する
EXECUTE drop_tmp_table_stmt;
DEALLOCATE PREPARE drop_tmp_table_stmt;
COMMIT;
END

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

View File

@ -0,0 +1,4 @@
{
"last_fetch_datetime_from": "1900-01-01T00:00:00.000Z",
"last_fetch_datetime_to": ""
}

Some files were not shown because too many files have changed in this diff Show More