Compare commits
628 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5979e307b5 | |||
| c6bb0d3645 | |||
| 39ff0f9b35 | |||
| 0df769f86b | |||
| 274a2c5261 | |||
| c2a64000b4 | |||
| aca9045648 | |||
| b53098d3eb | |||
| 49c4137975 | |||
| 2bc0c83328 | |||
| f86c434d1f | |||
| b98ca6d496 | |||
| 2ec2fee225 | |||
| 9b280ce58c | |||
| 6f9f336bab | |||
| 9f29129168 | |||
| e862226d7f | |||
| 2a43809fac | |||
| 6e71b86fd0 | |||
| 056387a79e | |||
| 5488f23461 | |||
| 26aa66a52e | |||
| 6df69c750b | |||
| edeaf1f91a | |||
| d428f84985 | |||
| c8f503268e | |||
| 93133dc17e | |||
| 43b81ebf3b | |||
| 76640eb4da | |||
| b5e870050f | |||
| af5516f201 | |||
| 09aa04ff79 | |||
| a92549347c | |||
| 80887fe6c0 | |||
| c9fd8f012a | |||
| 5a3b3f7f8e | |||
| 636ab46f5b | |||
| d5a2b69f20 | |||
| ac9a088829 | |||
| 9c1c2ad2aa | |||
| b2fbc0b83f | |||
| 7518417578 | |||
| 741861c8f3 | |||
| 90b936cd47 | |||
| 9a4e02d2f3 | |||
| 64a2313ba9 | |||
| bc313d7c1a | |||
| 60e7c139bc | |||
| 877c4e2673 | |||
| 894f5d5dcc | |||
| eac2be21fd | |||
| e795ec521c | |||
| ba8d09ee52 | |||
| 86cd216417 | |||
| a68977db6f | |||
| afccab2bd9 | |||
| 6930978c91 | |||
| 6e512a3ff2 | |||
| 788b48f217 | |||
| dfe5b13c68 | |||
| 8f402d4e15 | |||
| dc81f97030 | |||
| b06d9bc5a1 | |||
| 9b34d78319 | |||
| 9eb99f52ac | |||
| 15f3f89189 | |||
| ca423995df | |||
| 5f66477a81 | |||
| 913e5c79a9 | |||
| 9d39001efd | |||
| 0f6afd874a | |||
| df2a5d73e5 | |||
| 42904783c3 | |||
| 7f4129ac72 | |||
| 74efc886cc | |||
| a3cbaf6441 | |||
| 119d53a3f0 | |||
| b6b4ebc1a8 | |||
| f685698b87 | |||
| e8597d65da | |||
| 03903f906e | |||
| f4cd49636b | |||
| 7f85320305 | |||
| 3f66b4bcf2 | |||
| 0dfda056f7 | |||
| 515d69b821 | |||
| bf6f5a761c | |||
| 5e5d9c3faf | |||
| ca84952391 | |||
| 42ac87962e | |||
| e980c64fcd | |||
| bfa6e3ac26 | |||
| 3f8a9ccf54 | |||
| edcc3274da | |||
| 81ad8361d8 | |||
| 6ef84a8afb | |||
| 3f584acbd0 | |||
| b9d5305ef6 | |||
| 418f4f373c | |||
| 6e7509e3cf | |||
| dfea0fd2bb | |||
| 2ffc46c0ec | |||
| b48079d295 | |||
| cc0fdf6fe9 | |||
| ab5d2e3bbc | |||
| 21d35c1f11 | |||
| 8df85ecec4 | |||
| 0015980718 | |||
| 13bed53acf | |||
| 258de53fd3 | |||
| ed78569d7a | |||
| f8751ba13c | |||
| dfa5757bff | |||
| cbf1f05258 | |||
| f16f4bb8a5 | |||
| 37ccc8883c | |||
| 3b034daa7c | |||
| 6f341a403e | |||
| 4670cd12c4 | |||
| 4bd0e932f2 | |||
| 0e76a70430 | |||
| 9bbd8b102e | |||
| 645ce066e4 | |||
| 8b28dadf18 | |||
| 74551cebde | |||
| 23aac206bf | |||
| 9a8161b864 | |||
| c24adc4ece | |||
| c5720accc8 | |||
| 4c62dcad48 | |||
| c681978462 | |||
| 72c4f0b4c6 | |||
| b8633b3f54 | |||
| 6a9bc68faa | |||
| 2e1158f790 | |||
| 77f79a91f0 | |||
| 8df87cd766 | |||
| 25c30715c6 | |||
| be52cc354f | |||
| d7a88aa5f1 | |||
| 1583762844 | |||
| 43155fe846 | |||
| 8be55c7ede | |||
| bd0a776129 | |||
| 3aa7409ac2 | |||
| 56e24b4a78 | |||
| 43762fafa6 | |||
| 73cf64e420 | |||
| b93191b335 | |||
| 2d1ce6b8fd | |||
| a48e179ebc | |||
| 86952acdaa | |||
| c436bc254f | |||
| 3136bee08f | |||
| 63597b81f3 | |||
| 4e6246e269 | |||
| 85e63aae6f | |||
| 497e9c4b72 | |||
| 962585298d | |||
| 08c01dedd3 | |||
| b455e5a154 | |||
| cb7efbe3b3 | |||
| 42ea52bcf3 | |||
| 3b117a010b | |||
| 0b33ba74de | |||
| ccd40b275c | |||
| 6cf8826e0d | |||
| d358c85c08 | |||
| 520e9ae9d3 | |||
| 27755cce87 | |||
| 675ff396a5 | |||
| 0b5853e069 | |||
| 221ec59015 | |||
| a4373d5ec5 | |||
| 75430ad4fd | |||
| cbf9fdd3c5 | |||
| 2ea9701a50 | |||
| 6cd670a9c2 | |||
| 1efc74a3f9 | |||
| 7c09b215da | |||
| 0e251a0a7d | |||
| 5a3c10db48 | |||
| e8b2b4dca3 | |||
| ba913ade2a | |||
| 23d37d4a88 | |||
| 328b941581 | |||
| 60dbf3c641 | |||
| e0318d5999 | |||
| 0846342d14 | |||
| eed4af91ef | |||
| e5c18ed350 | |||
| 4d22d97b36 | |||
| ce2de4f36c | |||
| 7c0b98c3ce | |||
| 0532086cb7 | |||
| 3521bb43b1 | |||
| e9a43c9595 | |||
| 9b9c1d9527 | |||
| fa176eaf9d | |||
| 0e11479be6 | |||
| 723abd03c6 | |||
| a34e0f32d1 | |||
| bcd58591ec | |||
| 98fa0aa835 | |||
| 029044058d | |||
| 9ecda74db9 | |||
| a985c7f524 | |||
| 5204f69be5 | |||
| ef66ee2473 | |||
| 95e9d937da | |||
| f2a9d3ab97 | |||
| f118c3db53 | |||
| 5a31865355 | |||
| b8d24912a0 | |||
| 80fbb82695 | |||
| fb55b3b5ba | |||
| 296bacdf9a | |||
| b77b486ab9 | |||
| ccb332c7b6 | |||
| 87d399b6cf | |||
| 711d3f474a | |||
| 437e21b841 | |||
| 822f39d103 | |||
| 8715aa5e0e | |||
| 00677ab22c | |||
| fdc037ad03 | |||
| 9db17e992c | |||
| 57f58be5aa | |||
| 18852a91a1 | |||
| 2ac95d779e | |||
| 36fbf8e9f2 | |||
| a88ff0b35a | |||
| 27076b15cd | |||
| e8646781b2 | |||
| e104b84db5 | |||
| 5f4d969618 | |||
| fe11ccf7d1 | |||
| 036f185194 | |||
| be2c6c1961 | |||
| 9abb34a4b4 | |||
| 2f47853a72 | |||
| 5bce821cf0 | |||
| 3885d5099c | |||
| c317b22a93 | |||
| 2b6277d3c6 | |||
| b502d2acd2 | |||
| 55ab759b6d | |||
| ada74ee74d | |||
| e00d8feb4c | |||
| 3480df6e5e | |||
| daf706eb36 | |||
| aae94ddc49 | |||
| 967ec6f9d5 | |||
| 2a2bef0946 | |||
| 3c082b5a97 | |||
| 6a9a488503 | |||
| c35b01614c | |||
| b61c219095 | |||
| dbc7c4c0f7 | |||
| 9f26d225c3 | |||
| f3bf66d565 | |||
| 1eff29e3b5 | |||
| 351e1f7997 | |||
| 671eb28e4f | |||
| ab8702c265 | |||
| 5fca5e7bab | |||
| 6b2444bfb4 | |||
| 5201889c76 | |||
| 2c4c5e858b | |||
| f474652e47 | |||
| 0fbc8833a4 | |||
| e56bab499f | |||
| fe531fb6c5 | |||
| d9cc0d43c6 | |||
| cf3872690b | |||
| b02caf33f2 | |||
| eb10cd8481 | |||
| 2d3c32709f | |||
| f4bd507f08 | |||
| 3cde524869 | |||
| c16d3bdd26 | |||
| e25df20907 | |||
| e27c464216 | |||
| a675ecfff2 | |||
| 70b04a31d8 | |||
| 0620258f4f | |||
| fc465325e6 | |||
| da3bcecc94 | |||
| 2a6b269316 | |||
| 8f2458b71c | |||
| 2fac50e903 | |||
| 2d7181942f | |||
| 539484bf59 | |||
| 0b8685b9f9 | |||
| 99efa4cbe5 | |||
| 3cc383b101 | |||
| 37dc41ed18 | |||
| 99701611f5 | |||
| 4d49b7235b | |||
| d922c68639 | |||
| 1031e2b344 | |||
| 6d06f8d928 | |||
| 14fb9a7fd5 | |||
| 5d5804357e | |||
| e925d8045a | |||
| 4dc3315f4e | |||
| 26718f1619 | |||
| 6588a26a27 | |||
| 3bde096265 | |||
| 8d2c261e3b | |||
| 34b695ed2d | |||
| 812456d2a9 | |||
| 541b97179e | |||
| bd5c0f3add | |||
| 38cd5a9e19 | |||
| 10c65f0bc6 | |||
| 5070a64f40 | |||
| 6b7737895a | |||
| ec04220aa0 | |||
| 5d10b7b1f3 | |||
| bdf31e10f2 | |||
| 5345b2884b | |||
| 28703c85c2 | |||
| f417c23ffb | |||
| 00f8b69799 | |||
| be6b3cd276 | |||
| 1d2532b138 | |||
| ad18dcb1a1 | |||
| 58de513bc1 | |||
| 0e30d0da79 | |||
| 307e486e1f | |||
| b35b352abe | |||
| ca504cb08b | |||
| e4a7f8aa0c | |||
| d782addf9f | |||
| b03c63bd9f | |||
| 85052c55f1 | |||
| 464f1cc182 | |||
| cd9be1dbec | |||
| 2f4c2edbd3 | |||
| c932f29f2a | |||
| 5cf6f2190e | |||
| d4fa122f5d | |||
| cc428dcfcc | |||
| e3a6643eb0 | |||
| 1b7a254106 | |||
| 5a43fb1b79 | |||
| 963cf15149 | |||
| 5743654658 | |||
| 8a072e714d | |||
| a132647daa | |||
| dc50893c99 | |||
| 0ee67de6aa | |||
| 57a1d29dfc | |||
| 6b536d86da | |||
| 4c50c2e30e | |||
| b393626df9 | |||
| cdf7aa27f6 | |||
| 411eb9510f | |||
| 55a36f48b7 | |||
| 00a8cfe1e5 | |||
| 7fea607a40 | |||
| 001c85357e | |||
| 2db21f4632 | |||
| 2c94a109c9 | |||
| dadb0ab265 | |||
| 3ef732f1f4 | |||
| 4e22b4dcbf | |||
| dc64504e60 | |||
| 98ea0ff49c | |||
| bfcafdee43 | |||
| e4e899d4d6 | |||
| 4b1062cc49 | |||
| a98e25177d | |||
| b14f23c52a | |||
| c20ad486c6 | |||
| 76a73b1841 | |||
| 56f64b2226 | |||
| 333211f4d6 | |||
| 41a2ca49d8 | |||
| 432d814d88 | |||
| 6628cf639f | |||
| f47a81fa89 | |||
| aeefedeafd | |||
| 6211ffd317 | |||
| 76c283263e | |||
| 399c9225a8 | |||
| d9e970e0b8 | |||
| 42d672c84b | |||
| fae1e8241c | |||
| 85c1285ea4 | |||
| f6706142a9 | |||
| 2d19546787 | |||
| 23b9e05807 | |||
| ac605de375 | |||
| e5b03b38de | |||
| be92d693e0 | |||
| 253a974f4a | |||
| 34864ebb7b | |||
| 6fd7bf4b13 | |||
| e6b096f6e6 | |||
| 0fcd756add | |||
| ebd339af25 | |||
| a3b5fed569 | |||
| 973065a9ee | |||
| ca0a0cb494 | |||
| 6d5ce05b98 | |||
| ee989e4aaa | |||
| 7c1e70da34 | |||
| 6b87a8fb91 | |||
| e03b2bfb73 | |||
| 12659f02ba | |||
| 8af4141ec4 | |||
| 0ef27447e9 | |||
| 47a3c5aac0 | |||
| 5468e093be | |||
| 83feaac990 | |||
| ea7b5f5fbd | |||
| 7d96161bd7 | |||
| 09944289f3 | |||
| 5129fad5ae | |||
| e1f10f4044 | |||
| 29b5f0ad80 | |||
| 6ec3a8ae1d | |||
| bf9d0ec9bc | |||
| 8f1314c87a | |||
| df450b50b4 | |||
| 18de060a8f | |||
| 7939ab13cc | |||
| 5ef3124fa3 | |||
| f0bdcbd69c | |||
| 5a5477b852 | |||
| 885dbe3067 | |||
| b73e135aa2 | |||
| 66aa113dd4 | |||
| 9dfa80bfb3 | |||
| fe6656c3ed | |||
| c8b1372f6c | |||
| c748281e5e | |||
| 13063262cb | |||
| 47e79bc85c | |||
| a721bf8416 | |||
| 4feaba4305 | |||
| 21a73e8da3 | |||
| e90cf92749 | |||
| a6dbebf835 | |||
| c830ffcf57 | |||
| e8764b76e2 | |||
| bb91cdd763 | |||
| b272f32c55 | |||
| 7c7cd6d0ee | |||
| 63f317dd70 | |||
| ec51ecaab5 | |||
| 38a6d36c91 | |||
| 4a25a43792 | |||
| d03bd5353a | |||
| 34f67d4c0f | |||
| 414c5bcb15 | |||
| e083211591 | |||
| 0aea752053 | |||
| 58b40ee976 | |||
| 0af7900e7b | |||
| 8f05b7ca62 | |||
| 13a5d74cae | |||
| cd3e0fa724 | |||
| aecf4c2547 | |||
| 89950e3024 | |||
| 0ac42cd017 | |||
| 9181a8dfcb | |||
| 70540cd7a2 | |||
| b2a41d5472 | |||
| 1672372e19 | |||
| 98598b6af1 | |||
| 8ee24471de | |||
| 52a0ee9d01 | |||
| 2305b9cab2 | |||
| 5b0260f004 | |||
| 83033ed052 | |||
| 9b54501b19 | |||
| c2afd848bb | |||
| 23bd0102a0 | |||
| 940f64eaec | |||
| c2783aff4c | |||
| 08321a2e3d | |||
| b34585a487 | |||
| 3a3db75331 | |||
| 32d667d55a | |||
| bba1824a08 | |||
| 1b8fefdf5a | |||
| 85176b43f7 | |||
| 6c45ec02f8 | |||
| 01c2aacc13 | |||
| 0676bcf49c | |||
| 8f0568d2c6 | |||
| aa3d10bd6b | |||
| 73eb93c14a | |||
| 2b01f7f731 | |||
| 0a74026258 | |||
| a030c4d871 | |||
| 19b064ae08 | |||
| f62e58dc66 | |||
| 3a69245723 | |||
| 911bfe19e9 | |||
| 05e545d8f4 | |||
| ff4677166d | |||
| d97d742997 | |||
| 361b1cc036 | |||
| a7be38c96e | |||
| 507f7fdf77 | |||
| 01b82ef107 | |||
| 6177c88ff7 | |||
| a722dfb167 | |||
| 71927e48f5 | |||
| cdb5128eaf | |||
| afddc4d85c | |||
| 6cdad7b683 | |||
| 02bd64b558 | |||
| 47beb2abcc | |||
| fe941babc8 | |||
| 031fe5bf12 | |||
| 638c3a072a | |||
| 4cb2ed9ac1 | |||
| db419691eb | |||
| 8310a9ff01 | |||
| 5846017b26 | |||
| 5f96a45770 | |||
| d39caa511d | |||
| e1a1fa6307 | |||
| 19b95bc1d9 | |||
| d85d7ae03d | |||
| 4ddd0b47f3 | |||
| c0e6946740 | |||
| b9bf6bcb8c | |||
| dcf25e6106 | |||
| edfb491f3d | |||
| 5c09962dbd | |||
| e2ef60e5fa | |||
| 8feec718b1 | |||
| 3435a05910 | |||
| 13af51a1c1 | |||
| e31978d529 | |||
| da7af84c8f | |||
| ebc519cbaf | |||
| 0aa9cfae03 | |||
| 623a9df19d | |||
| 82348e8f06 | |||
| bf7b2997b4 | |||
| 023793926c | |||
| ef96484dbb | |||
| 914c7a2c1f | |||
| 6e5f0aacd1 | |||
| bb588d8e71 | |||
| ca2a6888f7 | |||
| 624abcf187 | |||
| 14e5797425 | |||
| d07e38b571 | |||
| e064f72e53 | |||
| 2e358c49cc | |||
| 4d62bd64f8 | |||
| 4c1df2c6ab | |||
| 518fe9194c | |||
| 7db887675e | |||
| 6da443dbdc | |||
| a4c3f85e9d | |||
| fde17328ff | |||
| 7eec4ed44b | |||
| de2f4d8e8d | |||
| 5b88e2c763 | |||
| feb95933f0 | |||
| c4632cd65b | |||
| 05bcdfadfc | |||
| 9586acb576 | |||
| 8a8cece7bc | |||
| 8b2cb5ea53 | |||
| 7f6df3c484 | |||
| 85da277713 | |||
| 68f466c6c0 | |||
| d071af0536 | |||
| c81e365ab6 | |||
| 83f59087cc | |||
| 713409aa54 | |||
| edfa8e16b0 | |||
| 753cb4ac60 | |||
| e14baf9589 | |||
| a08b9e8eec | |||
| b6758613b0 | |||
| 97c68b96bf | |||
| b504642824 | |||
| 5d7ebab22c | |||
| a51048aaa6 | |||
| 1cc76eb17b | |||
| 89e7eb651f | |||
| a78c3a0734 | |||
| 84a3388d7c | |||
| 40e0d840d2 | |||
| a1fc40f5dd | |||
| 9aebbdc611 | |||
| cac0c6bccd | |||
| 4383d39d60 | |||
| 86a75f9b31 | |||
| 2c90971314 | |||
| 52fcd915bc | |||
| 327d0393ad | |||
| c713149e06 | |||
| a96e49181e | |||
| eed8f9c719 | |||
| cb24ac2b29 | |||
| 3dcee588b1 | |||
| 15ca62f7eb | |||
| 7da567d728 | |||
| 94f657c1e0 | |||
| ba6952712a | |||
| 235fadcc4c | |||
| 98f4b316fb | |||
| 75007f8c29 | |||
| 43c1d95a6b | |||
| 523ceff41e | |||
| 29086ab27a | |||
| 436f284a8d | |||
| 4f9328cb9e | |||
| fc9e3f18f2 | |||
| d239e1dbf0 | |||
| 4bf39173be | |||
| 87f68e0ce5 | |||
| 73e1fad281 | |||
| 0a69e0cbc9 | |||
| 4ea440f084 | |||
| 3f5c20dc18 |
@@ -93,4 +93,9 @@ iOSInjectionProject/
|
||||
Info.plist
|
||||
/PayCash.entitlements
|
||||
/PayCash.xcodeproj
|
||||
/ResultIPA/
|
||||
*.pbxproj
|
||||
*.xcscheme
|
||||
*.xcodeproj
|
||||
*.entitlements
|
||||
/iOS/Assets/Settings.bundle
|
||||
|
||||
+34
-18
@@ -1,5 +1,6 @@
|
||||
# Execute always
|
||||
before_script:
|
||||
- echo $CI_PIPELINE_IID
|
||||
- echo $GITLAB_USER_ID
|
||||
- echo ${CI_COMMIT_REF_SLUG}
|
||||
- id
|
||||
@@ -9,13 +10,12 @@ variables:
|
||||
|
||||
stages:
|
||||
- lint
|
||||
# - test
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
- notification
|
||||
|
||||
# Linting stage
|
||||
|
||||
.linting: &linting
|
||||
tags:
|
||||
- malinka
|
||||
@@ -34,6 +34,25 @@ Lint:
|
||||
- /^feature/
|
||||
- merge_requests
|
||||
|
||||
# Testing stage
|
||||
.testing: &testing
|
||||
tags:
|
||||
- malinka
|
||||
stage: test
|
||||
when: always
|
||||
allow_failure: false
|
||||
script:
|
||||
- chmod +x ./toolchain/testing.sh
|
||||
- ./toolchain/testing.sh
|
||||
|
||||
Test:
|
||||
<<: *testing
|
||||
only:
|
||||
- develop
|
||||
- /^bugfix/
|
||||
- /^feature/
|
||||
- merge_requests
|
||||
|
||||
# Build IPA
|
||||
.BuildIPA: &BuildIPA
|
||||
tags:
|
||||
@@ -44,23 +63,20 @@ Lint:
|
||||
script:
|
||||
- rm -rf ./ResultIPA
|
||||
- chmod +x ./toolchain/build_iOS.sh
|
||||
# - ./toolchain/build_iOS.sh PRODUCTION
|
||||
- ./toolchain/build_iOS.sh BETA
|
||||
- ./toolchain/build_iOS.sh ${APPLICATION_DEPLOY_TYPE} ${APPLICATION_NAME} ${APPLICATION_SCHEME_NAME}
|
||||
|
||||
# Build stage
|
||||
|
||||
build:
|
||||
stage: build
|
||||
<<: *BuildIPA
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
only:
|
||||
- tags
|
||||
- merge_requests
|
||||
- develop
|
||||
artifacts:
|
||||
paths:
|
||||
- ./ResultIPA
|
||||
expire_in: 3 days
|
||||
expire_in: 2 days
|
||||
|
||||
buildLeaf:
|
||||
stage: build
|
||||
@@ -85,7 +101,7 @@ buildLeaf:
|
||||
IPA_PATH: ./ResultIPA
|
||||
script:
|
||||
- chmod +x ./toolchain/deploy_iOS.sh
|
||||
- ./toolchain/deploy_iOS.sh ${IPA_DEPLOY_TARGET} ${IPA_PATH}
|
||||
- ./toolchain/deploy_iOS.sh ${IPA_DEPLOY_TARGET} ${IPA_PATH} ${APPLICATION_SCHEME_NAME}
|
||||
needs:
|
||||
- job: build
|
||||
artifacts: true
|
||||
@@ -95,9 +111,8 @@ deploy:
|
||||
<<: *DeployIPA
|
||||
when: on_success
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- tags
|
||||
- develop
|
||||
needs:
|
||||
- job: build
|
||||
artifacts: true
|
||||
@@ -122,28 +137,29 @@ deployLeaf:
|
||||
script:
|
||||
- chmod +x ./toolchain/slack_notification.sh
|
||||
- ./toolchain/slack_notification.sh ${BUILD_RESULT}
|
||||
needs:
|
||||
- job: deploy
|
||||
|
||||
# Develop
|
||||
FailureNotification:
|
||||
variables:
|
||||
BUILD_RESULT: "FAILURE"
|
||||
when: on_failure
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- tags
|
||||
- develop
|
||||
<<: *slacknotification
|
||||
needs:
|
||||
- job: deploy
|
||||
|
||||
SuccessNotification:
|
||||
variables:
|
||||
BUILD_RESULT: "SUCCESS"
|
||||
when: on_success
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- tags
|
||||
- develop
|
||||
<<: *slacknotification
|
||||
needs:
|
||||
- job: deploy
|
||||
|
||||
# Merge checks
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "WalletFoundation",
|
||||
platforms: [.iOS(.v13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "WalletFoundation",
|
||||
targets: ["WalletFoundation"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "KeyChainAccess", path: "../../Vendors/spm/KeyChainAccess")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "WalletFoundation",
|
||||
dependencies: ["KeyChainAccess"],
|
||||
path: "./Sources"),
|
||||
.testTarget(
|
||||
name: "WalletFoundationTests",
|
||||
dependencies: ["WalletFoundation"],
|
||||
path: "Tests"//, // Test files
|
||||
// resources: [.copy("TestData")] // The test data files, copy files without modifying them
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// ApplicationSettings.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 8/28/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
fileprivate enum ApplicationSettingsKeys {
|
||||
|
||||
// TODO: - Need remove after 1.4.0 version
|
||||
static let ignoreDeviceSatusKeyOld = "Settings.device_ignore"
|
||||
static let ignoreCreateAccountSatusKeyOld = "Status.createAccount_ignore"
|
||||
static let isNetworkLogEnabledKeyOld = "Settings.network_copy"
|
||||
static let isDeviceTokenCopyEnabledKeyOld = "Settings.device_token_copy_enable"
|
||||
static let apiEnvironmentKeyOld = "Settings.api_environment"
|
||||
static let apiPaycashEnvironmentKeyOld = "Settings.api_paycash_environment"
|
||||
static let deviceDescriptionKeyOld = "Settings.device_id"
|
||||
static let deviceTokenKeyOld = "Settings.device_token"
|
||||
|
||||
/// Ignore device status on register flow
|
||||
static let ignoreDeviceSatusKey = "Settings.application.device_ignore"
|
||||
/// Ignore create account status on register flow
|
||||
static let ignoreCreateAccountSatusKey = "Status.application.createAccount_ignore"
|
||||
/// log all network activity
|
||||
static let isNetworkLogEnabledKey = "Settings.application.network_copy"
|
||||
/// Copy device token to Settings fields
|
||||
static let isDeviceTokenCopyEnabledKey = "Settings.application.device_token_copy_enable"
|
||||
/// Field in Settings for device description
|
||||
static let deviceDescriptionKey = "Settings.application.device_id"
|
||||
/// Field in Settings for device token
|
||||
static let deviceTokenKey = "Settings.application.device_token"
|
||||
|
||||
static let apiUsernameEnvironmentKey = "Settings.application.environment.usernames"
|
||||
static let apiBackendEnvironmentKey = "Settings.application.environment.backend"
|
||||
static let otherEnvironmentKey = "Settings.application.environment.other"
|
||||
static let smartEnvironmentKey = "Settings.application.environment.smart"
|
||||
}
|
||||
|
||||
public enum ApplicationSettings {
|
||||
|
||||
public static var ignoreDeviceSatus: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKey) }
|
||||
|
||||
public static var ignoreCreateAccountSatus: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKey) }
|
||||
|
||||
public static var isNetworkLogEnabled: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKey) }
|
||||
|
||||
public static var isDeviceTokenCopyEnabled: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKey) }
|
||||
|
||||
public static func device(description: String) { UserDefaults.standard.set(description, forKey: ApplicationSettingsKeys.deviceDescriptionKey) }
|
||||
|
||||
public static func device(token: String) { UserDefaults.standard.set(token, forKey: ApplicationSettingsKeys.deviceTokenKey) }
|
||||
|
||||
public static var apiUsernameEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.apiUsernameEnvironmentKey) }
|
||||
|
||||
public static var apiBackendEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.apiBackendEnvironmentKey) }
|
||||
|
||||
public static var otherEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.otherEnvironmentKey) }
|
||||
|
||||
public static var smartsEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.smartEnvironmentKey) }
|
||||
|
||||
public static func clearApiEmvironment() {
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceDescriptionKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceTokenKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiUsernameEnvironmentKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiBackendEnvironmentKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.otherEnvironmentKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.smartEnvironmentKey)
|
||||
|
||||
// TODO: - Need remove after 1.4.0 version
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiEnvironmentKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiPaycashEnvironmentKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceDescriptionKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceTokenKeyOld)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// CommonKey.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 11/25/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct CommonKey: RawRepresentable, Hashable {
|
||||
|
||||
public var rawValue: String
|
||||
|
||||
public init?(rawValue: String) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init(_ rawValue: String) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public func with(_ suffix: String) -> Self { .init(rawValue + "." + suffix) }
|
||||
|
||||
public static func key(_ key: String) -> Self { .init(key) }
|
||||
public static func key(_ key: String, suffix: String) -> Self { Self.key(key).with(suffix) }
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
//
|
||||
// WalletKeychain.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 11/27/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
|
||||
extension String {
|
||||
fileprivate static let password = "PWD"
|
||||
fileprivate static let biometric = "BIO"
|
||||
}
|
||||
|
||||
final public class WalletKeychain {
|
||||
|
||||
public typealias Key = CommonKey
|
||||
|
||||
public static let instance = WalletKeychain()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init() { }
|
||||
|
||||
// MARK: - Interface
|
||||
|
||||
public func exist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(.password)) }
|
||||
|
||||
public func bioExist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(.biometric) ) }
|
||||
|
||||
public subscript(biometric key: Key) -> String? {
|
||||
self.loadBiometricProtected(key: key.with(.biometric))
|
||||
.map({ String(data: $0, encoding: .utf8) }) ?? nil
|
||||
}
|
||||
|
||||
public subscript(_ key: Key, password password: String) -> String? {
|
||||
get { loadPassProtected(key: key.with(.password), password: password).map { String(data: $0, encoding: .utf8) } ?? nil }
|
||||
set { update(key, password: password, newValue: newValue) }
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func getPwdSecAccessControl() -> SecAccessControl {
|
||||
var access: SecAccessControl?
|
||||
var error: Unmanaged<CFError>?
|
||||
access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .applicationPassword, &error)
|
||||
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
|
||||
return access! // swiftlint:disable:this force_unwrapping
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func setPassProtected(key: Key, data: String, password: String) -> Bool {
|
||||
|
||||
let context = LAContext()
|
||||
context.setCredential(password.data(using: .utf8), type: .applicationPassword)
|
||||
|
||||
let query = [
|
||||
kSecClass as String: kSecClassGenericPassword as String,
|
||||
kSecAttrAccount as String: key.rawValue,
|
||||
kSecAttrAccessControl as String: getPwdSecAccessControl(),
|
||||
kSecValueData as String: (data.data(using: .utf8) ?? Data()) as NSData,
|
||||
kSecUseAuthenticationContext: context
|
||||
] as CFDictionary
|
||||
|
||||
let status: OSStatus = SecItemAdd(query, nil)
|
||||
if status == errSecSuccess {
|
||||
return true
|
||||
} else if status == errSecDuplicateItem {
|
||||
if removeProtected(key: key) {
|
||||
return setPassProtected(key: key, data: data, password: password)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func loadPassProtected(key: Key, password: String) -> Data? {
|
||||
let context = LAContext()
|
||||
context.setCredential(password.data(using: .utf8), type: .applicationPassword)
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key.rawValue,
|
||||
kSecReturnData as String: kCFBooleanTrue!,
|
||||
kSecAttrAccessControl as String: getPwdSecAccessControl(),
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecUseAuthenticationContext as String: context,
|
||||
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
|
||||
]
|
||||
|
||||
var dataTypeRef: AnyObject?
|
||||
let result = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
|
||||
if result == noErr,
|
||||
let value = dataTypeRef as? Data {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Biometric entries
|
||||
|
||||
private func getBiometricSecAccessControl() -> SecAccessControl {
|
||||
var access: SecAccessControl?
|
||||
var error: Unmanaged<CFError>?
|
||||
access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .biometryCurrentSet, &error)
|
||||
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
|
||||
return access! // swiftlint:disable:this force_unwrapping
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func setBiometricEntry(key: Key, data: String) -> Bool {
|
||||
|
||||
let query = [
|
||||
kSecClass as String: kSecClassGenericPassword as String,
|
||||
kSecAttrAccount as String: key.rawValue,
|
||||
kSecAttrAccessControl as String: getBiometricSecAccessControl(),
|
||||
kSecValueData as String: (data.data(using: .utf8) ?? Data()) as NSData,
|
||||
] as CFDictionary
|
||||
|
||||
let status: OSStatus = SecItemAdd(query, nil)
|
||||
|
||||
if status == errSecSuccess {
|
||||
return true
|
||||
} else if status == errSecDuplicateItem {
|
||||
if removeProtected(key: key) {
|
||||
return setBiometricEntry(key: key, data: data)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func loadBiometricProtected(key: Key) -> Data? {
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key.rawValue,
|
||||
kSecReturnData as String: kCFBooleanTrue as Any,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecUseOperationPrompt as String: "Access your data"
|
||||
]
|
||||
|
||||
var dataTypeRef: AnyObject?
|
||||
return SecItemCopyMatching(query as CFDictionary, &dataTypeRef) == noErr ? dataTypeRef as? Data : nil
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func update(_ key: Key, password: String, newValue: String?) {
|
||||
if let value = newValue {
|
||||
setPassProtected(key: key.with(.password), data: value, password: password)
|
||||
setBiometricEntry(key: key.with(.biometric), data: value)
|
||||
} else {
|
||||
removeProtected(key: key.with(.password))
|
||||
removeProtected(key: key.with(.biometric))
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func removeProtected(key: Key) -> Bool {
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key.rawValue
|
||||
]
|
||||
|
||||
return SecItemDelete(query as CFDictionary) == noErr
|
||||
}
|
||||
|
||||
private func checkProtectedExist(key: Key) -> Bool {
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key.rawValue,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
|
||||
]
|
||||
|
||||
let status = SecItemCopyMatching(query as CFDictionary, nil)
|
||||
switch status {
|
||||
case errSecSuccess, errSecInteractionNotAllowed: return true
|
||||
case errSecItemNotFound: return false
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Settings.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 11/29/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final public class Settings {
|
||||
|
||||
public static let shared = Settings()
|
||||
|
||||
private init() {}
|
||||
|
||||
public func contains(_ key: CommonKey) -> Bool { defaults.value(forKey: key.rawValue) != nil }
|
||||
|
||||
public subscript<T>(_ key: CommonKey) -> T? {
|
||||
get { defaults.value(forKey: key.rawValue) as? T }
|
||||
set { set(value: newValue, for: key) }
|
||||
}
|
||||
|
||||
public subscript<T: Codable>(_ key: CommonKey) -> [T]? {
|
||||
get {
|
||||
let decoder = JSONDecoder()
|
||||
guard let data = defaults.value(forKey: key.rawValue) as? Data,
|
||||
let value = try? decoder.decode([T].self, from: data) else { return nil }
|
||||
return value
|
||||
}
|
||||
set {
|
||||
let encoder = JSONEncoder()
|
||||
guard let value = newValue,
|
||||
let data = try? encoder.encode(value) else { return }
|
||||
set(value: data, for: key)
|
||||
}
|
||||
}
|
||||
|
||||
public subscript(_ key: CommonKey) -> Bool {
|
||||
get { defaults.bool(forKey: key.rawValue) }
|
||||
set { set(value: newValue, for: key) }
|
||||
}
|
||||
|
||||
public subscript(_ key: CommonKey) -> Int {
|
||||
get { (defaults.value(forKey: key.rawValue) as? Int) ?? 0 }
|
||||
set { set(value: newValue, for: key) }
|
||||
}
|
||||
|
||||
private func set(value: Any?, for key: CommonKey) {
|
||||
defaults.setValue(value, forKey: key.rawValue)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
private var defaults: UserDefaults { UserDefaults.standard }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// DeviceToken.swift
|
||||
//
|
||||
//
|
||||
// Created by NUT.Tech on 02.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import DeviceCheck
|
||||
import UIKit
|
||||
|
||||
public enum DeviceCheckTokenError: Error {
|
||||
case notSupported
|
||||
case generation
|
||||
}
|
||||
|
||||
public struct DeviceCheckToken {
|
||||
|
||||
public let token: String
|
||||
public let isSimulator: Bool
|
||||
|
||||
public static func generate() async -> Result<DeviceCheckToken, DeviceCheckTokenError> {
|
||||
|
||||
let token: String
|
||||
let isSimulator: Bool
|
||||
#if targetEnvironment(simulator)
|
||||
|
||||
isSimulator = true
|
||||
token = tokenConstant
|
||||
|
||||
#else
|
||||
|
||||
let device = DCDevice.current
|
||||
guard device.isSupported else { return .failure(.notSupported) }
|
||||
guard let data = try? await device.generateToken() else { return .failure(.generation) }
|
||||
|
||||
isSimulator = false
|
||||
token = data.base64EncodedString()
|
||||
|
||||
#endif
|
||||
|
||||
if ApplicationSettings.isDeviceTokenCopyEnabled {
|
||||
ApplicationSettings.device(token: token)
|
||||
}
|
||||
|
||||
return .success(DeviceCheckToken(token: token, isSimulator: isSimulator))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
fileprivate let tokenConstant = """
|
||||
AgAAAIArgRs5gVfPAEHSra0ejd0EUNk0+me89vLfv5ZingpyOOkgXXXyjPzYTzWmWSu+BYqcD47byirLZ++3dJccpF99hWppT7G5xAuU+y56WpSYsATWySfuxbSSMT9JSoOWz4QtiDmmVmUHbzCfHbTP3Tr3hsG+86KBBaoSqHtDRy+dtKlV32kDRbuuniBy3nseZsZoCggAAKTXDFKWHDZ55Ya1Cp+8s+dGOyi4C08v0P/8rbQHcjjkOphpLUqKaBZCykAaf5Ue1c1ul57OKeoyaDy9ShXGvwIKIcZrvZBBds3wwEFQuZBNPTG1ZvpIZ3npXscHWaKRd228V/bboEKarukYi3+lsxafZj+R8laD0Ex3nP3WZaIU4990oerdiu6wbdELlaNyrVF6SRJcZQhhflfnnyXHMqw6c41Hk3toMHsUd9wzdzcYmZB1PI5rsgSfnWrxAbBy5rYpH+ZkGkhcHFJqQIO4TB8ZKU0KSjmS8mBnVDT/1VPgiNDz/qI+KWiZ1xcuEwwvaEmD+Fk6Pt9GMRsfxI+SvldTgk8REqr9dBXt69xwM2FHBHP9k4okkaMbsU1qpZPKCwsnTBrkbvLn0zVn2+tfTukLx0O+uCPRMluXn3EXDrQ0aRHFbUtgoMqOx0JP+7tj/BaOAKkc3C2GaCRfEes2YmqZkM4pMve4Qh1jAsIwYtGcDz7AeGQ61kVrc4CFj3xj2OgC+IbBi7naOZqNvr1Be1YPKt8Vpig9YF5GueY9p4DRGlOG6UX2RY4wsZTt38Lxw5uBBkWuINjRyqsiN5obPZ3xLagfzfNsFDBYgBsGrib8nURVfSgAqIgrozOl+cppRB7xoN9QRti+HAJqKNd6sqpsPkXzqnDWPBD2Jdg2WCJE2bjJhTqyJ6L3lHFguOdIPc2P6F1CGx4bn1GtegOLlFxexOjMzfU86gJOhYjkVGHt8GD96ohRl75/fsv1reCI16pWt2x8p+Bbh4kko8wP3FtXiun+i6gPDMBhE30Ye5ATsIUIHFZjHOA8UfntaEyCSAngQebQ0X4UERcue+GKY4RqVfPhuVqJa3RHt35Ci/2g7VWllNs1NKYsPofAgTNO2n/kwGrnnIL+gQPNsO6XjnqDOjjT/eDfZX88eK4k6+8+MnW3+l8IAZOIh5JT++JZvBadrNZPV7G/2ME6G1FCIAZ4icCidbMzpj4sGc8dlJOg/B3457/Vt8CHLulhajQIsXQuqGDotgzirELTVQ+Z2eh3a8W/Pu+g8iNIUL1MCEzHg7RefM6tUescNDH6uPEuEeXKmrsoVbHdUvhuLIVbQGHMCjuGbmxmczcfIcRAPWkjwuVQOKBpoaD/Zep2gM71inpz6056bmvUuTMM9MV6sM87Fqrv6TvIRT/ch/i7Flmv56ERn/NGryafdcvDIu+JMs5U3yvb3STTSZmbh8RXVGIJjOZ1FJYLREUxxK7eEGM2JLCD+CxR7LuLRuN4AtpI4GIhFrbdVdDkywqNpvEY6aGEOFnD9NP6neBuHRhK/AzqpDE83uFf+1JiPPY7aHYVoQhCxkPs8ex0qJnjHaveKiWfSmaZ6JfY/vVByzJNr5XD6ZSQlJQJ3+xjRb+blTR0XcZ5BHI9ovQQAmGQljWpGPnD5CZQ7ah5kVoK1SbPqtxY6J5zQUTjtTpSe3l6By/nopXH6HSQXJGotOzb+eMOOHFhDC9ypq3urHY+Q1jXB18eR/xkXEIlZQsPBmwCLhoNltD57faLzlqgiwinHjqslntnvfsMkoIpQnWFwLYKh0biW9KM5ZWv7CMIxcc+Wjl1FVnyUrMzhz/IIW6WshcNmZWFSMpaKzozxIyFQZ/IjZHPrE1SI33PwGn/Tro8ZRSf9mpKJbA3uidrygVz6WQlJJwR3ujdgZ9aJ9WZQWVehVYj5mMg5P5MB/BG1G/TQNTLn72Root26hSB+8WMCAo/EEY3L5Qox+JabfDV+kBsHjFtDfyo7ghp/AOvkVzudzzk+F3ruc1bIDJlSfOQ5WRTkjmQGhKDIfDvLIi4Mt8bQTzZ3KPkLaU+hwuFE7m9c/MgzmLWK24oDsZ5nZ4oBeUNc+lThUuz7qnWENE1sIXij3zFljsoH+HOPb+zSt8m7iwszxMZ6T84LvqxqWVGEkI4Il2s41ti5QH74nOzTaci+5dYRVWOnY/hAwI51HE1sb3Pe+NsSHcfgtDW0E4Xx+Wx0uLdqbTlriXRnUIOoi9PVNR6XOdJ4nDOxyiOMMhnUooQ7lRqm0hCxw9nAEw5+PvYWWFXxPaupUfeVsjH+9dXW6bzosCGzTbVpHcDjPWie70r+Nma+oOwA4ARKHmGsbKcoO43xos6sqfbZTCCP9BIPQnZ8XbUen9G7eMs9ESigKoynKKVGNmsBXK4lU8xM/qLXXudViMdSPOZ6mghjJCNK0yA1v9l/ipZRHiTPFOttELH6Ip+fKDtfqdeEqCiPrSnVtWzehUKOUhlNJtexkOZcb2Dtq4L7JlJ0GkJg80vCvEYvArM2JpPqKDVr8hCBNC87u6zk9T3E+L2dfL30aiNVAGTl44Qw0pPerIr1a6m9Jkj690Pi3OI7UAgWaoQjxYm2my7DZMqtkL6CrT0NW9KnihXw701ngJysdKcZ0JkMDT2LzP+2Nj10WIOwkLxASexSQgSoyGk7yTYLUAyvwN1rhRtspXaiyOcfyzDwgTIU9Sn/jMbC6fv7GPReCsiFR8Xa6VCj37eFPXgBiOpAYtj/zMz/3S/io3LTqs7QG1M14CX31xSxu21tASOzaRhbd2RB2QCHXgpqv4593psE5EPjbRZt5DN2toQ4XJJ1A1/EcyDkEJ8+1gu34aVrqC6ejm/07/MQ7ISmUuPrJyCaPIW+PbkxF0VpYU5lJ9HP+LD7WggPwi8NVz8zFWNtyTM6aUuTNL69sBHpWlYeqCwpkJ+EcJFuaTnT27N4pFvwA==
|
||||
"""
|
||||
#endif
|
||||
@@ -0,0 +1,346 @@
|
||||
//
|
||||
// DeviceUUID.swift
|
||||
//
|
||||
//
|
||||
// Created Nut.Tech on 02.08.2022.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
import KeyChainAccess
|
||||
|
||||
/// Class allow retrive UUID with different life cycle.
|
||||
final public class DeviceUUID {
|
||||
|
||||
private enum Constants {
|
||||
|
||||
enum Keys {
|
||||
static let installationUUIDKey = "installationUUIDKey"
|
||||
static let deviceUUIDKey = "deviceUUIDKey"
|
||||
static let devicesUUIDsKey = "devicesUUIDsKey"
|
||||
static let devicesUUIDsToggleKey = "devicesUUIDsToggleKey"
|
||||
static let devicesUUIDs = "devicesUUIDs"
|
||||
}
|
||||
|
||||
enum Locals {
|
||||
static let DevicesUUIDsDidChangeNotification = NSNotification.Name(rawValue: "DevicesUUIDsDidChangeNotification")
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared instance for class
|
||||
static public var shared = DeviceUUID()
|
||||
|
||||
|
||||
/// Changes each time the app gets launched (persistent to session).
|
||||
lazy private(set) public var session: String = {
|
||||
self.generateUuid()
|
||||
}()
|
||||
|
||||
/// Changes each time the app gets installed (persistent to installation).
|
||||
lazy private(set) public var installation: String = {
|
||||
self.getValue(forKey: Constants.Keys.installationUUIDKey,
|
||||
defaultValue: nil,
|
||||
keychain: false,
|
||||
synchronizable: false)
|
||||
}()
|
||||
|
||||
/// Changes each time all the apps of the same vendor are uninstalled (this works exactly as identifierForVendor).
|
||||
lazy private(set) public var vendor: String? = {
|
||||
#if os(iOS)
|
||||
UIDevice.current
|
||||
.identifierForVendor?
|
||||
.uuidString
|
||||
.lowercased()
|
||||
.replacingOccurrences(of: "-", with: "")
|
||||
#elseif os(macOS)
|
||||
return nil
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Changes only on system reset, this is the best replacement to the good old udid (persistent to device)
|
||||
lazy private(set) public var device: String = {
|
||||
self.getValue(forKey: Constants.Keys.deviceUUIDKey,
|
||||
defaultValue: nil,
|
||||
synchronizable: false)
|
||||
}()
|
||||
|
||||
/// List of all uuidForDevice of the same user.
|
||||
/// In this way it's possible manage guest accounts across multiple devices easily
|
||||
lazy private(set) public var devices: [String] = {
|
||||
let devicesString = self.getValue(forKey: Constants.Keys.devicesUUIDsKey,
|
||||
defaultValue: self.device)
|
||||
return devicesString.components(separatedBy: "|")
|
||||
}()
|
||||
|
||||
/// Changes each time (no persistent), but allows to keep in memory more temporary uuids.
|
||||
public func uuid(forKey key: String) -> String {
|
||||
guard let uuid = self.uuids[key] else {
|
||||
let value = self.generateUuid()
|
||||
self.uuids[key] = value
|
||||
return value
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
|
||||
/// Changes each time (no persistent)
|
||||
public func generateUuid() -> String {
|
||||
let uuidRef = CFUUIDCreate(nil)
|
||||
let uuidStringRef = CFUUIDCreateString(nil, uuidRef)
|
||||
|
||||
return ((uuidStringRef as? String) ?? "")
|
||||
.lowercased()
|
||||
.replacingOccurrences(of: "-", with: "")
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var uuids = [String: String]()
|
||||
private var isCloudAvailable = false
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init() {
|
||||
self.initCloudUUIDsDevices()
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
func getValue(forKey key: String,
|
||||
defaultValue: String? = nil,
|
||||
userDefaults: Bool = true,
|
||||
keychain: Bool = true,
|
||||
service: String? = nil,
|
||||
accessGroup: String? = nil,
|
||||
synchronizable: Bool = true) -> String {
|
||||
|
||||
if let newValue = Self.getValue(forKey: key,
|
||||
userDefaults:
|
||||
userDefaults,
|
||||
keychain: keychain,
|
||||
service: service,
|
||||
accessGroup: accessGroup) {
|
||||
return newValue
|
||||
} else {
|
||||
let value = defaultValue ?? self.generateUuid()
|
||||
Self.setValue(value,
|
||||
forKey: key,
|
||||
userDefaults: userDefaults,
|
||||
keychain: keychain,
|
||||
service: service,
|
||||
accessGroup: accessGroup,
|
||||
synchronizable: synchronizable)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public func uuidForDeviceMigratingValue(forKey key: String,
|
||||
service: String? = nil,
|
||||
accessGroup: String? = nil,
|
||||
commitMigration: Bool) -> String? {
|
||||
|
||||
if let uuidToMigrate = Self.getValue(forKey: key,
|
||||
service: service,
|
||||
accessGroup: accessGroup) {
|
||||
return self.uuid(forDeviceMigratingValue: uuidToMigrate, commitMigration: commitMigration)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateUUIDDevice(value: String) {
|
||||
self.device = value
|
||||
Self.setValue(value,
|
||||
forKey: Constants.Keys.deviceUUIDKey,
|
||||
synchronizable: false)
|
||||
}
|
||||
|
||||
func uuid(forDeviceMigratingValue value: String, commitMigration: Bool) -> String? {
|
||||
|
||||
if self.isValidUUID(value) {
|
||||
|
||||
let oldValue = self.device
|
||||
let newValue = value
|
||||
|
||||
guard oldValue != newValue else { return oldValue }
|
||||
|
||||
if commitMigration {
|
||||
self.updateUUIDDevice(value: newValue)
|
||||
|
||||
let deviceSet = NSMutableOrderedSet(array: self.devices)
|
||||
deviceSet.add(newValue)
|
||||
deviceSet.remove(oldValue)
|
||||
|
||||
if let uuidsArray = deviceSet.array as? [String] {
|
||||
self.updateUUIDsDevices(with: uuidsArray)
|
||||
}
|
||||
self.syncCloudUUIDsDevices()
|
||||
|
||||
return self.device
|
||||
} else {
|
||||
return oldValue
|
||||
}
|
||||
} else {
|
||||
|
||||
let exception = NSException(name: NSExceptionName(rawValue: "Invalid uuid to migrate"),
|
||||
reason: "uuid value should be a string of 32 or 36 characters.")
|
||||
exception.raise()
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func initCloudUUIDsDevices() {
|
||||
|
||||
self.isCloudAvailable = false
|
||||
guard FileManager.default.ubiquityIdentityToken != nil else { return }
|
||||
self.isCloudAvailable = true
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(self.changesCloudUUIDsDevicesNotification),
|
||||
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
|
||||
object: nil)
|
||||
|
||||
self.syncCloudUUIDsDevices()
|
||||
}
|
||||
|
||||
private func syncCloudUUIDsDevices() {
|
||||
if self.isCloudAvailable {
|
||||
let iCloud = NSUbiquitousKeyValueStore.default
|
||||
//if keychain contains more device identifiers than icloud, maybe that icloud has been empty, so re-write these identifiers to iCloud
|
||||
for uuidOfUserDevice in self.devices {
|
||||
let uuidOfUserDeviceAsKey = "\(Constants.Keys.deviceUUIDKey)_\(uuidOfUserDevice)"
|
||||
|
||||
if iCloud.string(forKey: uuidOfUserDeviceAsKey) != uuidOfUserDevice {
|
||||
iCloud.set(uuidOfUserDevice, forKey: uuidOfUserDeviceAsKey)
|
||||
}
|
||||
}
|
||||
|
||||
//toggle a boolean value to force notification on other devices, useful for debug
|
||||
let uuidsOfUserDevicesToggler = !iCloud.bool(forKey: Constants.Keys.devicesUUIDsToggleKey)
|
||||
iCloud.set(uuidsOfUserDevicesToggler, forKey: Constants.Keys.devicesUUIDsToggleKey)
|
||||
iCloud.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateUUIDsDevices(with value: [String]) {
|
||||
self.devices = value
|
||||
Self.setValue(value.joined(separator: "|"),
|
||||
forKey: Constants.Keys.devicesUUIDsKey)
|
||||
}
|
||||
|
||||
private func isValidUUID(_ value: String) -> Bool {
|
||||
let pattern = "^[0-9a-f]{32}|[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$"
|
||||
guard let regExp = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let uuidValueRange = NSRange(location: 0, length: value.count)
|
||||
let matchRange = regExp.rangeOfFirstMatch(in: value, options: [], range: uuidValueRange)
|
||||
var matchValue: String?
|
||||
|
||||
if !NSEqualRanges(matchRange, NSRange(location: NSNotFound, length: 0)) {
|
||||
matchValue = (value as NSString).substring(with: matchRange)
|
||||
return matchValue == value ? true : false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func changesCloudUUIDsDevicesNotification(_ notification: Notification?) {
|
||||
|
||||
if self.isCloudAvailable {
|
||||
let uuidsSet = NSMutableOrderedSet(array: self.devices)
|
||||
let uuidsCount = uuidsSet.count
|
||||
|
||||
let iCloud = NSUbiquitousKeyValueStore.default
|
||||
let iCloudDict = iCloud.dictionaryRepresentation as NSDictionary
|
||||
|
||||
iCloudDict.enumerateKeysAndObjects { key, obj, stop in
|
||||
let uuidKey = key as? NSString
|
||||
if uuidKey?.range(of: Constants.Keys.deviceUUIDKey).location == 0 {
|
||||
if let uuidValue = obj as? String {
|
||||
if uuidKey?.range(of: uuidValue).location != NSNotFound,
|
||||
self.isValidUUID(uuidValue) {
|
||||
uuidsSet.add(uuidValue)
|
||||
} else {
|
||||
print("invalid uuid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if uuidsSet.count > uuidsCount,
|
||||
let uuidsArray = uuidsSet.array as? [String] {
|
||||
self.updateUUIDsDevices(with: uuidsArray)
|
||||
let userInfo = [Constants.Keys.devicesUUIDs: self.devices]
|
||||
NotificationCenter.default.post(name: Constants.Locals.DevicesUUIDsDidChangeNotification,
|
||||
object: self,
|
||||
userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static private func getValue(forKey key: String,
|
||||
userDefaults: Bool = true,
|
||||
keychain: Bool = true,
|
||||
service: String? = nil,
|
||||
accessGroup: String? = nil) -> String? {
|
||||
|
||||
let keychainStore = Self.makeKeychain(service: service, accessGroup: accessGroup)
|
||||
var value = try? keychainStore.getString(key)
|
||||
|
||||
if userDefaults,
|
||||
!value.isExist {
|
||||
value = UserDefaults.standard.string(forKey: key)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static private func setValue(_ value: String?,
|
||||
forKey key: String,
|
||||
userDefaults: Bool = true,
|
||||
keychain: Bool = true,
|
||||
service: String? = nil,
|
||||
accessGroup: String? = nil,
|
||||
synchronizable: Bool = true) -> Error? {
|
||||
|
||||
if let value = value,
|
||||
userDefaults {
|
||||
UserDefaults.standard.set(value, forKey: key)
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
|
||||
if let value = value, keychain {
|
||||
let keychainStore = Self.makeKeychain(service: service, accessGroup: accessGroup).synchronizable(synchronizable)
|
||||
do {
|
||||
try keychainStore.set(value, key: key)
|
||||
} catch {
|
||||
return error
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static private func makeKeychain(service: String? = nil, accessGroup: String? = nil) -> Keychain {
|
||||
|
||||
if let service = service {
|
||||
if let accessGroup = accessGroup {
|
||||
return Keychain(service: service, accessGroup: accessGroup)
|
||||
} else {
|
||||
return Keychain(service: service)
|
||||
}
|
||||
} else if let accessGroup = accessGroup {
|
||||
return Keychain(accessGroup: accessGroup)
|
||||
}
|
||||
|
||||
return Keychain()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// Array+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 9/15/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Array {
|
||||
|
||||
subscript(safe index: Index) -> Element? {
|
||||
return (self.startIndex..<self.endIndex) ~= index ? self[index] : nil
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Data+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 12/6/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Data {
|
||||
|
||||
func jsonDecoded<T: Decodable>(type: T.Type) -> T? { try? JSONDecoder().decode(type, from: self) }
|
||||
|
||||
func jsonDecoded<T: Decodable>(type: T.Type) -> [T]? { try? JSONDecoder().decode([T].self, from: self) }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Dictionary+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by NUT.Tech on 27.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Dictionary where Key == String, Value == Any {
|
||||
|
||||
func jsonSerialized(options: JSONSerialization.WritingOptions = []) -> Data? {
|
||||
try? JSONSerialization.data(withJSONObject: self, options: options)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// Encodable+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 12/6/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Encodable {
|
||||
|
||||
func jsonData() -> Data? {
|
||||
let encoder = JSONEncoder()
|
||||
return try? encoder.encode(self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Optional+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 08.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Optional {
|
||||
|
||||
var isExist: Bool {
|
||||
if case .some = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func orCreate(_ creation: @autoclosure () -> Wrapped) -> Wrapped {
|
||||
switch self {
|
||||
case let .some(value): return value
|
||||
case .none: return creation()
|
||||
}
|
||||
}
|
||||
|
||||
func orTypedCreate<Element: RawRepresentable>(_ creation: @autoclosure () -> Element) -> Element where Element.RawValue == Wrapped {
|
||||
switch self {
|
||||
case let .some(value): return Element(rawValue: value) ?? creation()
|
||||
case .none: return creation()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Publisher+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 12/20/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
public extension Publisher {
|
||||
|
||||
/// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is optional.
|
||||
/// The first time the upstream publisher emits an element, the previous element will be `nil`.
|
||||
///
|
||||
/// let range = (1...5)
|
||||
/// cancellable = range.publisher
|
||||
/// .withPrevious()
|
||||
/// .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
|
||||
/// // Prints: "(nil, 1) (Optional(1), 2) (Optional(2), 3) (Optional(3), 4) (Optional(4), 5) ".
|
||||
///
|
||||
/// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
|
||||
func withPrevious() -> AnyPublisher<(previous: Output?, current: Output), Failure> {
|
||||
scan(Optional<(Output?, Output)>.none) { ($0?.1, $1) }
|
||||
.compactMap { $0 }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is not optional.
|
||||
/// The first time the upstream publisher emits an element, the previous element will be the `initialPreviousValue`.
|
||||
///
|
||||
/// let range = (1...5)
|
||||
/// cancellable = range.publisher
|
||||
/// .withPrevious(0)
|
||||
/// .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
|
||||
/// // Prints: "(0, 1) (1, 2) (2, 3) (3, 4) (4, 5) ".
|
||||
///
|
||||
/// - Parameter initialPreviousValue: The initial value to use as the "previous" value when the upstream publisher emits for the first time.
|
||||
/// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
|
||||
func withPrevious(_ initialPreviousValue: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> {
|
||||
scan((initialPreviousValue, initialPreviousValue)) { ($0.1, $1) }.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
|
||||
func trimCompact() -> String {
|
||||
let value = self
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.replacingOccurrences(of: "\r", with: "")
|
||||
|
||||
let regex = try! NSRegularExpression(pattern: "[ ]{2,}", options: .caseInsensitive)
|
||||
return regex.stringByReplacingMatches(in: value,
|
||||
options: [],
|
||||
range: NSRange(0..<value.utf16.count),
|
||||
withTemplate: " ")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Either.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum Either<T: Decodable, U: Decodable>: Decodable {
|
||||
case firstType(T)
|
||||
case secondType(U)
|
||||
|
||||
public func unwrap() -> Any {
|
||||
switch self {
|
||||
case .firstType(let objectOfTypeT): return objectOfTypeT
|
||||
case .secondType(let objectOfTypeU): return objectOfTypeU
|
||||
}
|
||||
}
|
||||
|
||||
public func map<V>(firstTypeTransform: (T) -> V, secondTypeTransform: (U) -> V) -> V {
|
||||
switch self {
|
||||
case .firstType(let value):
|
||||
return firstTypeTransform(value)
|
||||
case .secondType(let value):
|
||||
return secondTypeTransform(value)
|
||||
}
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
if let value = try? T(from: decoder) {
|
||||
self = .firstType(value)
|
||||
} else if let value = try? U(from: decoder) {
|
||||
self = .secondType(value)
|
||||
} else {
|
||||
let context = DecodingError.Context(
|
||||
codingPath: decoder.codingPath,
|
||||
debugDescription:
|
||||
"Cannot decode \(T.self) or \(U.self)")
|
||||
throw DecodingError.dataCorrupted(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Operations.swift
|
||||
// Jura
|
||||
//
|
||||
// Created by Jura on 8/14/19.
|
||||
// Copyright © 2019 Jura. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
precedencegroup MonadicPrecedence {
|
||||
associativity: left
|
||||
higherThan: BitwiseShiftPrecedence
|
||||
}
|
||||
|
||||
infix operator >>- : MonadicPrecedence
|
||||
|
||||
@inline(__always)
|
||||
@discardableResult
|
||||
public func >>-<T, U>(a: T?, f: (T) throws -> U?) rethrows -> U? {
|
||||
switch a {
|
||||
case .some(let x):
|
||||
return try f(x)
|
||||
case .none:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: <<< / >>>
|
||||
|
||||
precedencegroup FunctionApplicationPrecedenceLeft {
|
||||
lowerThan: AssignmentPrecedence
|
||||
associativity: left
|
||||
}
|
||||
|
||||
infix operator >>> : FunctionApplicationPrecedenceLeft
|
||||
|
||||
@inline(__always)
|
||||
public func >>><T, U>(x: T, f: (T) throws -> U) rethrows -> U {
|
||||
return try f(x)
|
||||
}
|
||||
|
||||
precedencegroup FunctionApplicationPrecedenceRight {
|
||||
lowerThan: AssignmentPrecedence
|
||||
associativity: right
|
||||
}
|
||||
|
||||
infix operator <<< : FunctionApplicationPrecedenceRight
|
||||
|
||||
@inline(__always)
|
||||
public func <<<<T, U>(f: (T) throws -> U, x: T) rethrows -> U {
|
||||
return try f(x)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// ArrayTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 31.10.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
import WalletFoundation
|
||||
|
||||
final class ArrayTests: XCTestCase {
|
||||
|
||||
func testOuter() {
|
||||
let test = [0, 1, 2]
|
||||
XCTAssertNil(test[safe: 5])
|
||||
XCTAssertNil(test[safe: -1])
|
||||
}
|
||||
|
||||
func testInner() {
|
||||
let test = [1, 3, 5]
|
||||
var value = test[safe: 1]
|
||||
XCTAssertNotNil(value)
|
||||
XCTAssertEqual(value!, 3)
|
||||
|
||||
value = test[safe: 2]
|
||||
XCTAssertNotNil(value)
|
||||
XCTAssertEqual(value!, 5)
|
||||
|
||||
value = test[safe: 0]
|
||||
XCTAssertNotNil(value)
|
||||
XCTAssertEqual(value!, 1)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// OptionalsTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 11/8/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
import WalletFoundation
|
||||
|
||||
final class OptionalsTests: XCTestCase {
|
||||
|
||||
func testIsExists() {
|
||||
|
||||
var value: Int?
|
||||
XCTAssertFalse(value.isExist)
|
||||
|
||||
value = 42
|
||||
XCTAssertTrue(value.isExist)
|
||||
}
|
||||
|
||||
func testOrCreate() {
|
||||
|
||||
var value: String?
|
||||
XCTAssertEqual(value.orCreate("Hello"), "Hello")
|
||||
|
||||
value = "world"
|
||||
XCTAssertEqual(value.orCreate("Hello"), "world")
|
||||
}
|
||||
|
||||
func testOrTypedCreate() {
|
||||
|
||||
enum TestCases: String {
|
||||
case first
|
||||
case second
|
||||
}
|
||||
|
||||
var value: String?
|
||||
// Create new value because nil
|
||||
XCTAssertEqual(value.orTypedCreate(TestCases.first), .first)
|
||||
XCTAssertNotEqual(value.orTypedCreate(TestCases.first), .second)
|
||||
|
||||
value = "Hello"
|
||||
// Create new value because not matched
|
||||
XCTAssertEqual(value.orTypedCreate(TestCases.second), .second)
|
||||
XCTAssertNotEqual(value.orTypedCreate(TestCases.second), .first)
|
||||
|
||||
value = "first"
|
||||
// Not create new value and just match
|
||||
XCTAssertEqual(value.orTypedCreate(TestCases.second), .first)
|
||||
XCTAssertNotEqual(value.orTypedCreate(TestCases.second), .second)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,35 @@
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "WalletKit",
|
||||
platforms: [.iOS(.v13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "WalletKit",
|
||||
targets: ["WalletKit"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "eosswift", path: "../../Vendors/spm/eos-swift"),
|
||||
.package(name: "KeyChainAccess", path: "../../Vendors/spm/KeyChainAccess"),
|
||||
.package(name: "WalletFoundation", path: "../WalletFoundation"),
|
||||
.package(name: "WalletNetwork", path: "../WalletNetwork")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "WalletKit",
|
||||
dependencies: ["eosswift", "KeyChainAccess", "WalletFoundation", "WalletNetwork"],
|
||||
path: "./Sources"),
|
||||
.testTarget(
|
||||
name: "WalletKitTests",
|
||||
dependencies: ["WalletKit"],
|
||||
path: "Tests"//, // Test files
|
||||
// resources: [.copy("TestData")] // The test data files, copy files without modifying them
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// DeepLink.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 8/9/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct DeepLink {
|
||||
|
||||
public typealias DataParams = [AnyHashable: Any]
|
||||
public typealias InfoParams = [String: Any]
|
||||
|
||||
public let action: DeepLinkAction
|
||||
let data: Data?
|
||||
public let info: [String: Any]?
|
||||
public let url: URL?
|
||||
|
||||
public static func create(params: DataParams?) -> DeepLink? {
|
||||
|
||||
guard let params = params,
|
||||
let rawAction = params["action"] as? String,
|
||||
let action = DeepLinkAction(rawValue: rawAction),
|
||||
let data = (params["params"] as? String)?.data(using: .utf8)
|
||||
?? (try? JSONSerialization.data(withJSONObject: params["params"] as? [String: Any], options: [])) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return DeepLink(action: action, data: data, info: nil, url: URL(string: params["r"] as? String ?? ""))
|
||||
}
|
||||
|
||||
public static func create(info: DataParams?) -> DeepLink? {
|
||||
|
||||
guard let info = info as? [String: Any],
|
||||
let rawAction = info["action"] as? String,
|
||||
let action = DeepLinkAction(rawValue: rawAction) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return DeepLink(action: action, data: nil, info: info, url: nil)
|
||||
}
|
||||
|
||||
public func get<T: Decodable>(_ model: T.Type) -> T? {
|
||||
guard let data = self.data else { return nil }
|
||||
return try? JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
|
||||
public func dictionary() -> [String: Any] {
|
||||
guard let data = self.data else { return [:] }
|
||||
return (try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]) ?? [:]
|
||||
}
|
||||
}
|
||||
+9
-10
@@ -1,14 +1,13 @@
|
||||
//
|
||||
// CommonModelDeepLink.swift
|
||||
// PayCash
|
||||
// DeepLinkAction.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor on 15.10.2020.
|
||||
// Copyright © 2020 List. All rights reserved.
|
||||
// Created by Juraldinio on 8/9/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum DeepLinkAction: String {
|
||||
public enum DeepLinkAction: String {
|
||||
case connect = "connect_accounts"
|
||||
case chat = "chat"
|
||||
case transfer = "transfer"
|
||||
@@ -17,17 +16,17 @@ enum DeepLinkAction: String {
|
||||
case approveBuy = "approve_buy"
|
||||
case emission = "emission"
|
||||
case chatMessage = "chat.message"
|
||||
case transactionTransfer = "transactions.incoming_transfer"
|
||||
case transactionTransfer = "transaction.incoming_transfer"
|
||||
case transactionInheritance = "transactions.incoming_inheritance"
|
||||
case transactionEmission = "transactions.emission"
|
||||
case p2pDealNew = "orders.new_deal"
|
||||
case p2pDealComplete = "orders.completed_deal"
|
||||
case p2pDealCancel = "orders.cancelled_deal"
|
||||
case p2pDealDispute = "orders.dispute_deal"
|
||||
case cashDealNew = "guarantee_orders.new_deal"
|
||||
case cashDealConfirm = "guarantee_orders.confirmation_deal"
|
||||
case cashDealCancel = "guarantee_orders.cancelled_deal"
|
||||
case cashDealComplete = "guarantee_orders.completed_deal"
|
||||
// case cashDealNew = "guarantee_orders.new_deal"
|
||||
// case cashDealConfirm = "guarantee_orders.confirmation_deal"
|
||||
// case cashDealCancel = "guarantee_orders.cancelled_deal"
|
||||
// case cashDealComplete = "guarantee_orders.completed_deal"
|
||||
case news = "news"
|
||||
case competitivePrice = "competitive_price"
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//
|
||||
// Village.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 15.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
|
||||
public enum VillageError: Error {
|
||||
case passwordNotMatch
|
||||
}
|
||||
|
||||
final public class Village {
|
||||
|
||||
private enum Constants {
|
||||
static let passwordKey = CommonKey("Account.Service.password")
|
||||
}
|
||||
|
||||
private static var villages = [Village]()
|
||||
|
||||
private let environment: NetworkEnvironment
|
||||
private var houses = [VillageHouse]()
|
||||
|
||||
private var foreigner: Foreigner? {
|
||||
didSet {
|
||||
guard let foreigner = self.foreigner else { return }
|
||||
ApplicationSettings.device(description: "\(foreigner)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
/// Create instance of Village.
|
||||
public static func get(with environment: NetworkEnvironment) -> Village {
|
||||
if let village = Self.villages.first(where: { $0.environment.isEquals(other: environment) }) {
|
||||
return village
|
||||
}
|
||||
|
||||
let village = Village(environment: environment)
|
||||
Self.villages.append(village)
|
||||
return village
|
||||
}
|
||||
|
||||
/// Get device instance.
|
||||
public func device(force: Bool = false) async throws -> Device {
|
||||
|
||||
if force { self.foreigner = nil }
|
||||
|
||||
if let foreigner = self.foreigner { return foreigner }
|
||||
|
||||
if !force,
|
||||
let foreigner = Foreigner.restore() {
|
||||
do {
|
||||
try await foreigner.retrievStatus(using: self.environment)
|
||||
self.foreigner = foreigner
|
||||
return foreigner
|
||||
} catch { }
|
||||
}
|
||||
|
||||
do {
|
||||
let foreigner = try await Foreigner.create(for: DeviceUUID.shared.device, using: self.environment)
|
||||
foreigner.save()
|
||||
self.foreigner = foreigner
|
||||
return foreigner
|
||||
} catch {
|
||||
throw DeviceError.create
|
||||
}
|
||||
}
|
||||
|
||||
public func attestate(device: Device) async throws -> CertifiedDevice {
|
||||
try await Resident.permit(for: device, using: self.environment)
|
||||
}
|
||||
|
||||
/// Create wallet key.
|
||||
public func createWalletKey() throws -> WalletKey {
|
||||
try WalletKey.create()
|
||||
}
|
||||
|
||||
/// Create wallet case for hold wallet.
|
||||
public func createWalletCase(with name: String, key: WalletKey? = nil) async throws -> WalletCase {
|
||||
|
||||
let walletKey: WalletKey
|
||||
if let key = key {
|
||||
walletKey = key
|
||||
} else {
|
||||
walletKey = try WalletKey.create()
|
||||
}
|
||||
|
||||
return try await VillagerCoat.create(name: name, using: self.environment, key: walletKey)
|
||||
}
|
||||
|
||||
/// Create bank that contain Wallets.
|
||||
public func getBank(with password: String, on device: Device) -> Bank {
|
||||
|
||||
if let bank = self.houses.first(where: { $0.isEquals(password: password, device: device, environment: self.environment) }) {
|
||||
return bank
|
||||
}
|
||||
|
||||
if Self.password != password {
|
||||
Self.password = password
|
||||
}
|
||||
|
||||
let house = VillageHouse.create(for: password, on: device, environment: self.environment)
|
||||
self.houses.append(house)
|
||||
|
||||
return house
|
||||
}
|
||||
|
||||
public static func reset() {
|
||||
VillageHouse.removeAllVillagers()
|
||||
Self.password = nil
|
||||
}
|
||||
|
||||
public static var isPasswordExists: Bool { WalletKeychain.instance.exist(Constants.passwordKey) }
|
||||
|
||||
public static func getPassword(password: String?) -> String? {
|
||||
if let password {
|
||||
return WalletKeychain.instance[Constants.passwordKey, password: password]
|
||||
} else {
|
||||
return WalletKeychain.instance[biometric: Constants.passwordKey]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove this function after refactoring passwords
|
||||
public static func updateCommonPassword(password: String, old: String) throws {
|
||||
if WalletKeychain.instance[Constants.passwordKey, password: old] != nil {
|
||||
Self.password = password
|
||||
} else {
|
||||
throw VillageError.passwordNotMatch
|
||||
}
|
||||
}
|
||||
|
||||
private static var password: String? {
|
||||
get { WalletKeychain.instance[biometric: Constants.passwordKey] }
|
||||
set { WalletKeychain.instance[Constants.passwordKey, password: newValue ?? ""] = newValue }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// Tractor.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 15.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
import KeyChainAccess
|
||||
|
||||
final class Foreigner: Device, Codable {
|
||||
|
||||
private enum Constant {
|
||||
static let saveKey = "village.tractor.instance"
|
||||
static let service = CodingUserInfoKey(rawValue: "service")!
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case id
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init(uuid: String, id: String, isTrusted: Bool) {
|
||||
self.uuid = uuid
|
||||
self.id = id
|
||||
self.isTrusted = isTrusted
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.uuid = try container.decode(String.self, forKey: .uuid)
|
||||
self.id = try container.decode(String.self, forKey: .id)
|
||||
}
|
||||
|
||||
// MARK: - Device
|
||||
|
||||
let uuid: String
|
||||
let id: String
|
||||
private(set) var isTrusted: Bool = false
|
||||
private(set) var availableAccounts: Int?
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
var description: String { "uuid: \(self.uuid), id: \(self.id)" }
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func save() {
|
||||
guard let rawData = try? JSONEncoder().encode(self) else { return }
|
||||
let keychain = Keychain()
|
||||
try? keychain.set(rawData, key: Constant.saveKey)
|
||||
}
|
||||
|
||||
func retrievStatus(using environment: NetworkEnvironment) async throws {
|
||||
let service = DeviceService(environment: environment)
|
||||
do {
|
||||
let status = try await service.stateDevice(id: self.id)
|
||||
self.availableAccounts = status.availableAccounts
|
||||
self.isTrusted = status.isTrusted
|
||||
return
|
||||
} catch {
|
||||
throw DeviceError.status
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static func create(for cid: String, using environment: NetworkEnvironment) async throws -> Foreigner {
|
||||
let service = DeviceService(environment: environment)
|
||||
do {
|
||||
let device = try await service.createDevice(uuid: cid)
|
||||
return Foreigner(uuid: device.uuid, id: device.id, isTrusted: device.isTrusted)
|
||||
} catch {
|
||||
throw DeviceError.create
|
||||
}
|
||||
}
|
||||
|
||||
static func restore() -> Foreigner? {
|
||||
let keychain = Keychain()
|
||||
guard let rawData = try? keychain.getData(Constant.saveKey) else { return nil }
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
return try? decoder.decode(Foreigner.self, from: rawData)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Resident.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 25.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
|
||||
struct Resident: CertifiedDevice {
|
||||
|
||||
let device: Device
|
||||
let status: DeviceStatus
|
||||
|
||||
static func permit(for device: Device, using environment: NetworkEnvironment) async throws -> CertifiedDevice {
|
||||
|
||||
if device.isTrusted { return Resident(device: device, status: .valid) }
|
||||
|
||||
let result = await DeviceCheckToken.generate()
|
||||
guard let deviceToken = try? result.get() else {
|
||||
throw CertifiedDeviceError.token
|
||||
}
|
||||
|
||||
let service = DeviceService(environment: environment)
|
||||
do {
|
||||
let status = try await service.checkDevice(id: device.id, token: deviceToken.token)
|
||||
return Resident(device: device, status: status == .valid ? .valid : .invalid )
|
||||
} catch let NetworkServiceError.gqlApplication(error) {
|
||||
|
||||
let deviceError: CertifiedDeviceError
|
||||
switch error {
|
||||
case "Invalid": deviceError = .invalid
|
||||
case "DeviceNotFoundError": deviceError = .notFound
|
||||
case "DecryptError": deviceError = .decrypt
|
||||
case "UnexpectedDeviceKind": deviceError = .kind
|
||||
case "UnexpectedError": deviceError = .unknown
|
||||
default: deviceError = .unknown
|
||||
}
|
||||
|
||||
throw deviceError
|
||||
} catch {
|
||||
throw CertifiedDeviceError.unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 8/27/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
|
||||
final class VillageHouse: Bank {
|
||||
|
||||
private enum Constants {
|
||||
static let oldKey = "Account.Service.collection"
|
||||
static let key = "Wallet.bank.service"
|
||||
|
||||
enum Keys {
|
||||
static let current = CommonKey("Account.Service.current")
|
||||
static let collection = CommonKey("Account.Service.collection")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var password: String
|
||||
private let device: Device
|
||||
private let environment: NetworkEnvironment
|
||||
|
||||
private let activeSubject: CurrentValueSubject<Villager?, Never>
|
||||
private let villagersSubject: CurrentValueSubject<[Villager], Never>
|
||||
|
||||
private var cancelables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init(password: String, device: Device, environment: NetworkEnvironment) {
|
||||
|
||||
self.password = password
|
||||
self.device = device
|
||||
self.environment = environment
|
||||
|
||||
let collection = Self.restore()
|
||||
self.villagersSubject = CurrentValueSubject(collection)
|
||||
|
||||
let active = Self.active(in: collection)
|
||||
self.activeSubject = CurrentValueSubject(active)
|
||||
|
||||
self.villagersSubject
|
||||
.sink { [weak self] villagers in
|
||||
self?.save(villagers: villagers)
|
||||
}
|
||||
.store(in: &self.cancelables)
|
||||
|
||||
self.migrate(using: self.password)
|
||||
}
|
||||
|
||||
// MARK: - Bank
|
||||
|
||||
var active: Wallet? { self.activeSubject.value }
|
||||
|
||||
var wallets: [Wallet] { self.villagersSubject.value }
|
||||
|
||||
lazy var activePublisher: AnyPublisher<Wallet?, Never> = self.activeSubject.map { $0 }.eraseToAnyPublisher()
|
||||
|
||||
lazy var walletsPublisher: AnyPublisher<[Wallet], Never> = self.villagersSubject.map { $0 }.eraseToAnyPublisher()
|
||||
|
||||
func accept(password: String) -> Bool { self.password == password }
|
||||
|
||||
func remove(wallet: Wallet) throws {
|
||||
|
||||
var villagers = self.villagersSubject.value
|
||||
guard let villager = self.villager(by: wallet),
|
||||
let index = villagers.firstIndex(where: { $0 == villager }) else {
|
||||
throw BankError.notOwned
|
||||
}
|
||||
|
||||
villagers.remove(at: index)
|
||||
villager.clear()
|
||||
|
||||
self.villagersSubject.value = villagers
|
||||
|
||||
if let active = self.active,
|
||||
villager.isEquals(other: active) {
|
||||
try self.activate(wallet: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func activate(wallet: Wallet?) throws {
|
||||
|
||||
guard let wallet else {
|
||||
Settings.shared[Constants.Keys.current] = Data()
|
||||
self.activeSubject.value = nil
|
||||
return
|
||||
}
|
||||
|
||||
guard let villager = self.villager(by: wallet) else {
|
||||
throw BankError.notOwned
|
||||
}
|
||||
|
||||
// We can save only wallet in accepted state.
|
||||
if case .accepted = villager.state {
|
||||
Settings.shared[Constants.Keys.current] = villager.jsonData()
|
||||
}
|
||||
|
||||
self.activeSubject.value = villager
|
||||
}
|
||||
|
||||
func add(using walletCase: WalletCase) async throws -> Wallet {
|
||||
let villager = try await Villager.create(walletCase: walletCase, on: self.device, using: self.environment)
|
||||
villager.updatePrivateKey(using: self.password)
|
||||
|
||||
let villagers = self.villagersSubject.value
|
||||
self.villagersSubject.value = villagers + [villager]
|
||||
|
||||
return villager
|
||||
}
|
||||
|
||||
func add(using purses: [Purse]) throws {
|
||||
|
||||
let wallets = purses
|
||||
.filter { $0.bank?.isEquals(other: self) ?? false }
|
||||
.filter { purse in
|
||||
!self.wallets.contains(where: { $0.name == purse.name && $0.keyType.rawValue == purse.permission.permName })
|
||||
}
|
||||
.map { Villager.create(purse: $0) }
|
||||
|
||||
wallets
|
||||
.filter { $0.key.privateKey.isExist }
|
||||
.forEach { $0.updatePrivateKey(using: self.password) }
|
||||
|
||||
let villagers = self.villagersSubject.value
|
||||
self.villagersSubject.value = villagers + wallets
|
||||
}
|
||||
|
||||
func restore(using keys: WalletKey) async throws -> PurseHolder {
|
||||
|
||||
let holder = PurseHolder()
|
||||
let service = AccountHyperionService(environment: self.environment)
|
||||
let eosService = EOSService(environment: self.environment)
|
||||
|
||||
return try await holder.load(using: service, eosService: eosService, keys: keys, in: self)
|
||||
}
|
||||
|
||||
func refreshStatus(wallet: Wallet?) async throws {
|
||||
|
||||
let villagers: [Villager]
|
||||
if let wallet,
|
||||
let villager = self.villager(by: wallet) {
|
||||
villagers = [villager]
|
||||
} else {
|
||||
villagers = self.villagersSubject.value
|
||||
}
|
||||
|
||||
_ = await withTaskGroup(of: Void.self) { group in
|
||||
|
||||
villagers.forEach { villager in
|
||||
|
||||
switch villager.state {
|
||||
case .pending,
|
||||
.creating:
|
||||
group.addTask {
|
||||
try? await villager.refresh(using: self.environment)
|
||||
}
|
||||
case .accepted,
|
||||
.declined:
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await group.waitForAll()
|
||||
|
||||
self.save(villagers: villagers)
|
||||
}
|
||||
}
|
||||
|
||||
func isEquals(other: Bank) -> Bool {
|
||||
guard let house = other as? VillageHouse else { return false }
|
||||
return self.password == house.password &&
|
||||
self.device.isEquals(other: house.device) &&
|
||||
self.environment.isEquals(other: house.environment)
|
||||
}
|
||||
|
||||
func switchPassword(_ password: String, old: String) throws {
|
||||
|
||||
guard self.accept(password: old) else {
|
||||
throw BankError.passwordNotMatch
|
||||
}
|
||||
|
||||
self.password = password
|
||||
|
||||
old >>- { old in self.villagersSubject.value.forEach { $0.update(password: password, old: old) } }
|
||||
}
|
||||
|
||||
func update(_ keyUpdates: [WalletKeyUpdate], using password: String) async throws -> [WalletKeyUpdateResult] {
|
||||
|
||||
// TODO: - After move EOS to frameworks!
|
||||
|
||||
/*guard password == self.password else { throw BankError.passwordNotMatch }
|
||||
|
||||
let villagers = keyUpdates.compactMap({ self.villager(by: $0.wallet) })
|
||||
|
||||
guard villagers.count != keyUpdates.count else { throw BankError.notOwned }
|
||||
|
||||
guard let privateKey = self.privateKey(password) else {
|
||||
throw WalletError.privateKeyNotExists
|
||||
}
|
||||
|
||||
return WalletKeyUpdate(wallet: self, oldPrivateKey: "", transitionId: "")
|
||||
|
||||
return try await villager.update(key: key, password: self.password, using: self.environment)*/
|
||||
return []
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
func isEquals(password: String, device: Device, environment: NetworkEnvironment) -> Bool {
|
||||
return self.password == password &&
|
||||
self.device.isEquals(other: device) &&
|
||||
self.environment.isEquals(other: environment)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func save(villagers: [Villager]) {
|
||||
Settings.shared[Constants.Keys.collection] = villagers.jsonData()
|
||||
}
|
||||
|
||||
private static func restore() -> [Villager] {
|
||||
if let data: Data = Settings.shared[Constants.Keys.collection],
|
||||
let villagers: [Villager] = data.jsonDecoded(type: Villager.self) {
|
||||
return villagers
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
private func migrate(using password: String) {
|
||||
|
||||
let migrateWallets: [Villager]
|
||||
if let data = UserDefaults.standard.value(forKey: Constants.oldKey) as? Data,
|
||||
let collection = try? JSONDecoder().decode([Villager.OldVillager].self, from: data) {
|
||||
migrateWallets = collection.compactMap { $0.covert(password: password) }
|
||||
} else {
|
||||
migrateWallets = []
|
||||
}
|
||||
|
||||
print(migrateWallets)
|
||||
|
||||
// Delete previous key
|
||||
// UserDefaults.standard.set(nil, forKey: Constants.oldKey)
|
||||
|
||||
}
|
||||
|
||||
private func villager(by wallet: Wallet) -> Villager? {
|
||||
guard let villager = wallet as? Villager,
|
||||
self.villagersSubject.value.contains(where: { $0 == villager }) else {
|
||||
return nil
|
||||
}
|
||||
return villager
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static func create(for password: String, on device: Device, environment: NetworkEnvironment) -> VillageHouse {
|
||||
VillageHouse(password: password, device: device, environment: environment)
|
||||
}
|
||||
|
||||
private static func active(in collection: [Villager]) -> Villager? {
|
||||
|
||||
if let data: Data = Settings.shared[Constants.Keys.current],
|
||||
let villager: Villager = data.jsonDecoded(type: Villager.self),
|
||||
collection.contains(where: { $0 == villager }) {
|
||||
return villager
|
||||
}
|
||||
|
||||
guard let rawValue: String = Settings.shared[Constants.Keys.current] else { return nil }
|
||||
|
||||
let name = rawValue.components(separatedBy: "@").first
|
||||
let keyTypeString = rawValue.components(separatedBy: "@").last ?? ""
|
||||
let keyType = WalletKeyType(rawValue: keyTypeString) ?? WalletKeyType.active
|
||||
|
||||
return collection.first(where: { $0.name == name && $0.keyType == keyType })
|
||||
}
|
||||
|
||||
static func removeAllVillagers() {
|
||||
Settings.shared[Constants.Keys.collection] = Data()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
//
|
||||
// Villager.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 16.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
|
||||
extension String {
|
||||
fileprivate static let privateKey = "privateKey"
|
||||
}
|
||||
|
||||
final class Villager: Wallet, Codable, CustomStringConvertible {
|
||||
|
||||
struct OldVillager: Codable {
|
||||
var username: String
|
||||
var publicKey: String
|
||||
var keyType: String
|
||||
|
||||
func covert(password: String) -> Villager? {
|
||||
guard let keyType = WalletKeyType(rawValue: self.keyType),
|
||||
let key = try? WalletKey.restore(using: self.username, type: keyType, publicKey: self.publicKey, password: password) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Villager(name: self.username, key: key, keyType: keyType, state: .accepted)
|
||||
}
|
||||
}
|
||||
|
||||
private let stateSubject: CurrentValueSubject<WalletState, Never>
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init(walletCase: WalletCase, keyType: WalletKeyType, state: WalletState) {
|
||||
self.name = walletCase.name
|
||||
self.key = walletCase.key
|
||||
self.keyType = keyType
|
||||
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
|
||||
}
|
||||
|
||||
private init(name: String, key: WalletKey, keyType: WalletKeyType, state: WalletState) {
|
||||
self.name = name
|
||||
self.key = key
|
||||
self.keyType = keyType
|
||||
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
// Old values
|
||||
case username
|
||||
case publicKey
|
||||
//
|
||||
case name
|
||||
case key
|
||||
case keyType
|
||||
case state
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// For save old format with username field!
|
||||
let username = try container.decodeIfPresent(String.self, forKey: .username)
|
||||
if let username {
|
||||
self.name = username
|
||||
} else {
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
}
|
||||
|
||||
self.keyType = try container.decode(WalletKeyType.self, forKey: .keyType)
|
||||
|
||||
let state = try container.decode(WalletState.self, forKey: .state)
|
||||
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
|
||||
|
||||
// If first version we hold key on flat structure!
|
||||
if let key = try? WalletKey(from: decoder) {
|
||||
self.key = key
|
||||
} else {
|
||||
self.key = try container.decode(WalletKey.self, forKey: .key)
|
||||
}
|
||||
// TODO: - NEED COMPLETETASK
|
||||
// self.key = try WalletKey.restore(using: self.name, type: self.keyType, publicKey: publicKey, password: "")
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.name, forKey: .name)
|
||||
try container.encode(self.key, forKey: .key)
|
||||
try container.encode(self.keyType, forKey: .keyType)
|
||||
try container.encode(self.stateSubject.value, forKey: .state)
|
||||
}
|
||||
|
||||
// MARK: - Wallet
|
||||
|
||||
let name: String
|
||||
private(set) var key: WalletKey
|
||||
let keyType: WalletKeyType
|
||||
var state: WalletState { self.stateSubject.value }
|
||||
|
||||
lazy var statePublisher: AnyPublisher<WalletState, Never> = self.stateSubject.eraseToAnyPublisher()
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
static func == (lhs: Villager, rhs: Villager) -> Bool {
|
||||
return lhs.name == rhs.name
|
||||
&& lhs.key == rhs.key
|
||||
&& lhs.keyType == rhs.keyType
|
||||
&& lhs.state == rhs.state
|
||||
&& lhs.name == rhs.name
|
||||
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
var description: String { "(name: \(name), keyType: \(keyType), state: \(state), public: \(key.publicKey)" }
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
func refresh(using environment: NetworkEnvironment) async throws {
|
||||
|
||||
let service = AccountService(environment: environment)
|
||||
let state: WalletState
|
||||
|
||||
switch self.state {
|
||||
case .creating(let orderId),
|
||||
.pending(let orderId):
|
||||
do {
|
||||
let order = try await service.order(with: orderId)
|
||||
state = WalletState(order: order)
|
||||
} catch {
|
||||
throw WalletError.network(error)
|
||||
}
|
||||
default:
|
||||
state = self.state
|
||||
}
|
||||
|
||||
self.stateSubject.value = state
|
||||
}
|
||||
|
||||
func update(key: WalletKey, using passrod: String) -> Wallet {
|
||||
self.key = key
|
||||
self.updatePrivateKey(using: passrod)
|
||||
return self
|
||||
}
|
||||
|
||||
func privateKey(_ value: String?) -> String? {
|
||||
let privateKey: String?
|
||||
if let password = value {
|
||||
privateKey = WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: password]
|
||||
} else {
|
||||
privateKey = WalletKeychain.instance[biometric: .key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey)]
|
||||
}
|
||||
return privateKey
|
||||
}
|
||||
|
||||
func updatePrivateKey(using password: String) {
|
||||
guard let privateKey = self.key.privateKey else { return }
|
||||
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey),
|
||||
password: password] = privateKey
|
||||
}
|
||||
|
||||
// TODO: - Need review
|
||||
|
||||
func migrate(password: String) {
|
||||
let privateKey = WalletKeychain.instance[.key(self.name, suffix: .privateKey), password: password]
|
||||
WalletKeychain.instance[.key(self.name, suffix: .privateKey), password: ""] = nil
|
||||
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: password] = privateKey
|
||||
}
|
||||
|
||||
func update(password: String, old: String) {
|
||||
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey),
|
||||
password: password] = self.privateKey(old)
|
||||
}
|
||||
|
||||
func clear() {
|
||||
WalletKeychain.instance[.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey), password: ""] = nil
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static func create(purse: Purse) -> Villager {
|
||||
Villager(name: purse.name, key: purse.key, keyType: purse.keyType, state: .accepted)
|
||||
}
|
||||
|
||||
static func create(walletCase: WalletCase, on device: Device, using environment: NetworkEnvironment) async throws -> Villager {
|
||||
|
||||
let service = AccountService(environment: environment)
|
||||
do {
|
||||
// return Villager(walletCase: walletCase, state: .pending)
|
||||
let order = try await service.create(username: walletCase.name,
|
||||
pubKey: walletCase.key.publicKey,
|
||||
deviceId: device.id)
|
||||
|
||||
let state: WalletState
|
||||
switch order.status {
|
||||
case .creating: state = .creating(order.id)
|
||||
case .active, .executed: state = .pending(order.id)
|
||||
case .expired, .failed: state = .declined
|
||||
case .completed: state = .accepted
|
||||
}
|
||||
|
||||
return Villager(walletCase: walletCase, keyType: .owner, state: state)
|
||||
|
||||
} catch let NetworkServiceError.gqlApplication(error) {
|
||||
|
||||
if ApplicationSettings.ignoreCreateAccountSatus {
|
||||
return Villager(walletCase: walletCase, keyType: .owner, state: .creating("HELLO"))
|
||||
}
|
||||
|
||||
let walletError: WalletError
|
||||
switch error {
|
||||
case "invalid EOS public key": walletError = .invalidKey
|
||||
case "INVALID DEVICE ID": walletError = .invalidDevice
|
||||
case "UNKNOWN DEVICE": walletError = .device
|
||||
case "UNCHECKED": walletError = .unchecked
|
||||
case "UNTRUSTED": walletError = .untrusted
|
||||
case "DEVICE LIMIT REACHED": walletError = .deviceLimit
|
||||
case "DAILY LIMIT REACHED": walletError = .dailyLimit
|
||||
case "UNAVAILABLE": walletError = .unavailable
|
||||
case "ACCOUNT ALREADY EXISTS": walletError = .exists
|
||||
default: walletError = .unavailable
|
||||
}
|
||||
throw walletError
|
||||
|
||||
} catch {
|
||||
throw WalletError.unavailable
|
||||
}
|
||||
}
|
||||
|
||||
static func create(walletKey: WalletKey, using environment: NetworkEnvironment) async throws -> Villager {
|
||||
// TODO: - Add logic
|
||||
throw WalletError.unavailable
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// VillagerCoat.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 16.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletNetwork
|
||||
|
||||
final class VillagerCoat: WalletCase {
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init(name: String, key: WalletKey) {
|
||||
self.name = name
|
||||
self.key = key
|
||||
}
|
||||
|
||||
// MARK: - WalletCase
|
||||
|
||||
let name: String
|
||||
private(set) var key: WalletKey
|
||||
|
||||
@discardableResult
|
||||
func update(key: WalletKey) -> WalletKey {
|
||||
self.key = key
|
||||
return self.key
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func generateKey() -> WalletKey {
|
||||
if let key = try? WalletKey.create() {
|
||||
self.key = key
|
||||
}
|
||||
return self.key
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static func create(name: String, using environment: NetworkEnvironment, key: WalletKey) async throws -> VillagerCoat {
|
||||
|
||||
let walletName = name.lowercased()
|
||||
let service = AccountService(environment: environment)
|
||||
let isAvailable = await service.isAvailable(username: walletName)
|
||||
switch isAvailable {
|
||||
case .available: return VillagerCoat(name: walletName, key: key)
|
||||
case .alreadyTaken: throw WalletCaseError.exists(name: walletName)
|
||||
case .system: throw WalletCaseError.system
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// Bank.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 8/27/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import WalletNetwork
|
||||
|
||||
public enum BankError: Error {
|
||||
case empty
|
||||
case notOwned
|
||||
case passwordNotMatch
|
||||
}
|
||||
|
||||
/// Bank that contain wallets.
|
||||
public protocol Bank: AnyObject {
|
||||
/// Current active wallet.
|
||||
var active: Wallet? { get }
|
||||
/// Publisher for observe changes.
|
||||
var activePublisher: AnyPublisher<Wallet?, Never> { get }
|
||||
/// Wallets in bank.
|
||||
var wallets: [Wallet] { get }
|
||||
/// Publisher for observe wallet changes.
|
||||
var walletsPublisher: AnyPublisher<[Wallet], Never> { get }
|
||||
/// Check password
|
||||
func accept(password: String) -> Bool
|
||||
/// Activate wallet
|
||||
func activate(wallet: Wallet?) throws
|
||||
/// Create new wallet in bank.
|
||||
func add(using walletCase: WalletCase) async throws -> Wallet
|
||||
/// Add Purse to bank with converting to Wallet
|
||||
func add(using purses: [Purse]) throws
|
||||
/// Restore wallet by key.
|
||||
func restore(using keys: WalletKey) async throws -> PurseHolder
|
||||
/// Remove wallet from bank.
|
||||
func remove(wallet: Wallet) throws
|
||||
/// Check wallet status
|
||||
func refreshStatus(wallet: Wallet?) async throws
|
||||
/// Update WalletKey for Wallet
|
||||
func update(_ keyUpdates: [WalletKeyUpdate], using password: String) async throws -> [WalletKeyUpdateResult]
|
||||
/// Compare two banks
|
||||
func isEquals(other: Bank) -> Bool
|
||||
/// Switch password
|
||||
func switchPassword(_ password: String, old: String) throws
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// CertifiedDevice.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 25.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum CertifiedDeviceError: Error {
|
||||
/// Unavailable generate token for check
|
||||
case token
|
||||
/// Device not fount on server
|
||||
case notFound
|
||||
/// Invalid token
|
||||
case invalid
|
||||
/// Token decription error
|
||||
case decrypt
|
||||
/// Unexpected device kind
|
||||
case kind
|
||||
/// Unknown error
|
||||
case unknown
|
||||
/// Already in progress
|
||||
case progress
|
||||
}
|
||||
|
||||
/// Device status
|
||||
public enum DeviceStatus {
|
||||
/// Valid
|
||||
case valid
|
||||
/// Invalid
|
||||
case invalid
|
||||
}
|
||||
|
||||
public protocol CertifiedDevice {
|
||||
/// Device
|
||||
var device: Device { get }
|
||||
/// Device status
|
||||
var status: DeviceStatus { get }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// Device.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 15.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/// Errors for Device
|
||||
public enum DeviceError: Error {
|
||||
/// Failed while restore
|
||||
case restore
|
||||
/// Failed create device on server side
|
||||
case create
|
||||
/// Failed retrieve status
|
||||
case status
|
||||
}
|
||||
|
||||
public protocol Device: CustomStringConvertible {
|
||||
/// Device generated Identity UUID
|
||||
var uuid: String { get }
|
||||
/// Server generated Identity
|
||||
var id: String { get }
|
||||
/// Is device trusted
|
||||
var isTrusted: Bool { get }
|
||||
|
||||
func isEquals(other: Device) -> Bool
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
extension Device {
|
||||
func isEquals(other: Device) -> Bool {
|
||||
return self.uuid == other.uuid &&
|
||||
self.id == other.id &&
|
||||
self.isTrusted == other.isTrusted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// ErrorDomain.swift
|
||||
//
|
||||
//
|
||||
// Created by user on 31.10.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ErrorDomain: Error {
|
||||
case noEnvironment
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// PurseHolder.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 12/4/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
import EosioSwift
|
||||
import Combine
|
||||
|
||||
public struct Purse {
|
||||
public let name: String
|
||||
public let key: WalletKey
|
||||
public let keyType: WalletKeyType
|
||||
let permission: Permission
|
||||
var bank: Bank?
|
||||
}
|
||||
|
||||
public final class PurseHolder {
|
||||
|
||||
public enum PurseError: Error {
|
||||
case empty
|
||||
}
|
||||
|
||||
public private(set) var purses: [Purse] = []
|
||||
private var count: Int = 0
|
||||
|
||||
func load(using service: AccountHyperionService, eosService: EOSService, keys: WalletKey, in bank: Bank) async throws -> PurseHolder {
|
||||
|
||||
let publicKey = keys.publicKey
|
||||
let collection = await service.fetchNamesHyperion(publicKey: publicKey)
|
||||
|
||||
guard !collection.isEmpty else {
|
||||
throw PurseError.empty
|
||||
}
|
||||
|
||||
let _ = await withCheckedContinuation { continuation in
|
||||
|
||||
self.count = collection.count
|
||||
|
||||
collection.forEach { name in
|
||||
|
||||
DispatchQueue.global().async {
|
||||
|
||||
eosService.getAccount(name) { response in
|
||||
|
||||
self.count -= 1
|
||||
|
||||
switch response {
|
||||
case let .success(accountInfo):
|
||||
|
||||
let permissions = accountInfo.permissions
|
||||
.filter { permission in permission.requiredAuth.keys.contains(where: { $0.key == publicKey }) }
|
||||
.sorted { $0.permName > $1.permName }
|
||||
|
||||
permissions.forEach { permission in
|
||||
|
||||
if (permission.permName == WalletKeyType.owner.rawValue) {
|
||||
self.purses.append(Purse(name: name,
|
||||
key: keys,
|
||||
keyType: .owner,
|
||||
permission: permission,
|
||||
bank: bank))
|
||||
}
|
||||
|
||||
let hasAddedOwnerKey = self.purses.contains(where: { account in
|
||||
account.name == name &&
|
||||
account.permission.requiredAuth.keys.first?.key == permission.requiredAuth.keys.first?.key
|
||||
})
|
||||
|
||||
if let type = WalletKeyType(rawValue: permission.permName),
|
||||
type != WalletKeyType.owner,
|
||||
!hasAddedOwnerKey {
|
||||
self.purses.append(Purse(name: name,
|
||||
key: keys,
|
||||
keyType: type,
|
||||
permission: permission,
|
||||
bank: bank))
|
||||
}
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
if self.count == 0 {
|
||||
continuation.resume(returning: self.purses)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// Wallet.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 15.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletNetwork
|
||||
import Combine
|
||||
|
||||
/// Errors for wallet creation
|
||||
public enum WalletError: Error {
|
||||
/// Invalid EOS public key
|
||||
case invalidKey
|
||||
/// Device not found
|
||||
case invalidDevice
|
||||
/// Device not found
|
||||
case device
|
||||
/// Device does not do check phase
|
||||
case unchecked
|
||||
/// Device not trusted
|
||||
case untrusted
|
||||
/// Device limit reached
|
||||
case deviceLimit
|
||||
/// Daily limit reached
|
||||
case dailyLimit
|
||||
/// Unknown error
|
||||
case unavailable
|
||||
/// Network error
|
||||
case network(Error)
|
||||
/// Private key does not exists
|
||||
case privateKeyNotExists
|
||||
/// Wallet with such name already exists.
|
||||
case exists
|
||||
}
|
||||
|
||||
/// Wallet state
|
||||
public enum WalletState: RawRepresentable {
|
||||
/// Transaction sent to EOS.
|
||||
case creating(String)
|
||||
/// Creation in progress
|
||||
case pending(String)
|
||||
/// Created
|
||||
case accepted
|
||||
/// Creation failed
|
||||
case declined
|
||||
|
||||
private enum Constants {
|
||||
static let preffixCreate = "C"
|
||||
static let preffixPending = "P"
|
||||
}
|
||||
|
||||
// TODO:- remove public
|
||||
public init(order: WalletOrder) {
|
||||
switch order.status {
|
||||
case .completed:
|
||||
self = .accepted
|
||||
case .creating:
|
||||
self = .creating(order.id)
|
||||
case .executed, .active:
|
||||
self = .pending(order.id)
|
||||
case .failed, .expired:
|
||||
self = .declined
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RawRepresentable
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "accepted": self = .accepted
|
||||
case "declined": self = .declined
|
||||
default:
|
||||
let value = String(rawValue.dropFirst())
|
||||
let preffix = rawValue.first?.uppercased()
|
||||
if preffix == Constants.preffixPending {
|
||||
self = .pending(value)
|
||||
} else {
|
||||
self = .creating(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .accepted: return "accepted"
|
||||
case .declined: return "declined"
|
||||
case let .pending(value): return "\(Constants.preffixPending)\(value)"
|
||||
case let .creating(value): return "\(Constants.preffixCreate)\(value)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WalletState: Codable { }
|
||||
|
||||
public enum WalletKeyType: String, Codable, CustomStringConvertible {
|
||||
case owner
|
||||
case active
|
||||
|
||||
public var description: String { self.rawValue }
|
||||
}
|
||||
|
||||
public struct WalletKeyUpdateResult {
|
||||
public let wallet: Wallet
|
||||
public let oldPrivateKey: String
|
||||
public let transitionId: String
|
||||
}
|
||||
|
||||
public struct WalletKeyUpdate {
|
||||
public let wallet: Wallet
|
||||
public let key: WalletKey
|
||||
|
||||
public init(wallet: Wallet, key: WalletKey) {
|
||||
self.wallet = wallet
|
||||
self.key = key
|
||||
}
|
||||
}
|
||||
|
||||
/// Wallet
|
||||
public protocol Wallet: AnyObject {
|
||||
/// Name
|
||||
var name: String { get }
|
||||
/// Keys
|
||||
var key: WalletKey { get }
|
||||
/// Type
|
||||
var keyType: WalletKeyType { get }
|
||||
/// State
|
||||
var state: WalletState { get }
|
||||
var statePublisher: AnyPublisher<WalletState, Never> { get }
|
||||
|
||||
@discardableResult
|
||||
func update(key: WalletKey, using passrod: String) -> Wallet
|
||||
|
||||
func privateKey(_ value: String?) -> String?
|
||||
|
||||
/// Compare two Wallets
|
||||
func isEquals(other: Wallet) -> Bool
|
||||
}
|
||||
|
||||
public extension Wallet {
|
||||
|
||||
var isOnCreationState: Bool {
|
||||
switch self.state {
|
||||
case .declined,
|
||||
.accepted:
|
||||
return false
|
||||
case .creating,
|
||||
.pending:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var isOnActiveState: Bool {
|
||||
switch self.state {
|
||||
case .creating,
|
||||
.accepted:
|
||||
return true
|
||||
case .declined,
|
||||
.pending:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isEquals(other: Wallet) -> Bool {
|
||||
self.name == other.name &&
|
||||
self.key == other.key &&
|
||||
self.keyType == other.keyType &&
|
||||
self.state == other.state
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// WalletCase.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 17.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Wallet Case errors
|
||||
public enum WalletCaseError: Error {
|
||||
/// System error
|
||||
case system
|
||||
/// Wallet with name already exists
|
||||
case exists(name: String)
|
||||
}
|
||||
|
||||
/// Wallet case. This mean case that can contain wallet.
|
||||
public protocol WalletCase: AnyObject {
|
||||
/// Name
|
||||
var name: String { get }
|
||||
/// Key
|
||||
var key: WalletKey { get }
|
||||
/// Update key
|
||||
@discardableResult
|
||||
func update(key: WalletKey) -> WalletKey
|
||||
/// Generate new key
|
||||
@discardableResult
|
||||
func generateKey() -> WalletKey
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// WalletKey.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 17.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import eosswift
|
||||
|
||||
/// Errors for WalletKey
|
||||
public enum WalletKeyError: Error {
|
||||
/// System error
|
||||
case system
|
||||
/// Invalid public key
|
||||
case invalidPublic
|
||||
/// Invalid private key
|
||||
case invalidPrivate
|
||||
/// Invalid pair public and private keys
|
||||
case invalidPair
|
||||
///
|
||||
case unlock
|
||||
}
|
||||
|
||||
/// Represents Wallet key.
|
||||
/// This keys must pass checks.
|
||||
public enum WalletKey: Codable, Equatable {
|
||||
|
||||
/// Public key
|
||||
case publicKey(String)
|
||||
/// Public and Private key
|
||||
case bunch(publicKey: String, privateKey: String)
|
||||
|
||||
public var publicKey: String {
|
||||
switch self {
|
||||
case let .publicKey(value): return value
|
||||
case let .bunch(publicKey: value, privateKey: _): return value
|
||||
}
|
||||
}
|
||||
|
||||
public var privateKey: String? {
|
||||
switch self {
|
||||
case let .bunch(publicKey: _, privateKey: value): return value
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
func save() {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case publicKey
|
||||
case privateKey
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let publicKey = try container.decode(String.self, forKey: .publicKey)
|
||||
let privateKey = try container.decodeIfPresent(String.self, forKey: .privateKey)
|
||||
|
||||
if let privateKey {
|
||||
self = .bunch(publicKey: publicKey, privateKey: privateKey)
|
||||
} else {
|
||||
self = .publicKey(publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case let .publicKey(value):
|
||||
try container.encode(value, forKey: .publicKey)
|
||||
case let .bunch(publicKey: publicKey, privateKey: privateKey):
|
||||
try container.encode(publicKey, forKey: .publicKey)
|
||||
try container.encode(privateKey, forKey: .privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public static
|
||||
|
||||
public static func create(public: String, private: String) throws -> WalletKey {
|
||||
|
||||
let privateKey: EOSPrivateKey
|
||||
do {
|
||||
privateKey = try EOSPrivateKey(base58: `private`)
|
||||
} catch {
|
||||
throw WalletKeyError.invalidPrivate
|
||||
}
|
||||
|
||||
let publicKey: EOSPublicKey
|
||||
do {
|
||||
publicKey = try EOSPublicKey(base58: `public`)
|
||||
} catch {
|
||||
throw WalletKeyError.invalidPublic
|
||||
}
|
||||
|
||||
guard privateKey.publicKey.base58 == publicKey.base58 else {
|
||||
throw WalletKeyError.invalidPair
|
||||
}
|
||||
|
||||
return .bunch(publicKey: publicKey.base58, privateKey: privateKey.base58)
|
||||
}
|
||||
|
||||
public static func create(public: String) throws -> WalletKey {
|
||||
let publicKey: EOSPublicKey
|
||||
do {
|
||||
publicKey = try EOSPublicKey(base58: `public`)
|
||||
} catch {
|
||||
throw WalletKeyError.invalidPublic
|
||||
}
|
||||
|
||||
return .publicKey(publicKey.base58)
|
||||
}
|
||||
|
||||
public static func create(private: String) throws -> WalletKey {
|
||||
|
||||
let privateKey: EOSPrivateKey
|
||||
do {
|
||||
privateKey = try EOSPrivateKey(base58: `private`)
|
||||
} catch {
|
||||
throw WalletKeyError.invalidPrivate
|
||||
}
|
||||
|
||||
return .bunch(publicKey: privateKey.publicKey.base58, privateKey: privateKey.base58)
|
||||
}
|
||||
|
||||
/// Create key for wallet
|
||||
public static func create() throws -> WalletKey {
|
||||
guard let key = try? EOSPrivateKey() else { throw WalletKeyError.system }
|
||||
return .bunch(publicKey: key.publicKey.base58, privateKey: key.base58)
|
||||
}
|
||||
|
||||
// MARK: - Internal static
|
||||
|
||||
static func restore(using name: String, type: WalletKeyType, publicKey: String, password: String) throws -> WalletKey {
|
||||
|
||||
throw WalletKeyError.unlock
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// DeepLinkActionTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 31.10.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
import WalletKit
|
||||
|
||||
final class DeepLinkActionTests: XCTestCase {
|
||||
|
||||
func testConvertFromString() {
|
||||
XCTAssertEqual(DeepLinkAction.connect.rawValue, "connect_accounts")
|
||||
XCTAssertEqual(DeepLinkAction.chat.rawValue, "chat")
|
||||
XCTAssertEqual(DeepLinkAction.transfer.rawValue, "transfer")
|
||||
XCTAssertEqual(DeepLinkAction.walletAuth.rawValue, "wallet_auth")
|
||||
XCTAssertEqual(DeepLinkAction.tokenization.rawValue, "accept_tokenization")
|
||||
XCTAssertEqual(DeepLinkAction.approveBuy.rawValue, "approve_buy")
|
||||
XCTAssertEqual(DeepLinkAction.emission.rawValue, "emission")
|
||||
XCTAssertEqual(DeepLinkAction.chatMessage.rawValue, "chat.message")
|
||||
XCTAssertEqual(DeepLinkAction.transactionTransfer.rawValue, "transaction.incoming_transfer")
|
||||
XCTAssertEqual(DeepLinkAction.transactionInheritance.rawValue, "transactions.incoming_inheritance")
|
||||
XCTAssertEqual(DeepLinkAction.transactionEmission.rawValue, "transactions.emission")
|
||||
XCTAssertEqual(DeepLinkAction.p2pDealNew.rawValue, "orders.new_deal")
|
||||
XCTAssertEqual(DeepLinkAction.p2pDealComplete.rawValue, "orders.completed_deal")
|
||||
XCTAssertEqual(DeepLinkAction.p2pDealCancel.rawValue, "orders.cancelled_deal")
|
||||
XCTAssertEqual(DeepLinkAction.p2pDealDispute.rawValue, "orders.dispute_deal")
|
||||
XCTAssertEqual(DeepLinkAction.news.rawValue, "news")
|
||||
XCTAssertEqual(DeepLinkAction.competitivePrice.rawValue, "competitive_price")
|
||||
}
|
||||
|
||||
func testConvertToString() {
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "connect_accounts"), .connect)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "chat"), .chat)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "transfer"), .transfer)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "wallet_auth"), .walletAuth)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "accept_tokenization"), .tokenization)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "approve_buy"), .approveBuy)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "emission"), .emission)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "chat.message"), .chatMessage)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "transaction.incoming_transfer"), .transactionTransfer)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "transactions.incoming_inheritance"), .transactionInheritance)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "transactions.emission"), .transactionEmission)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "orders.new_deal"), .p2pDealNew)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "orders.completed_deal"), .p2pDealComplete)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "orders.cancelled_deal"), .p2pDealCancel)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "orders.dispute_deal"), .p2pDealDispute)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "news"), .news)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "competitive_price"), .competitivePrice)
|
||||
}
|
||||
|
||||
func testConvertFailed() {
|
||||
XCTAssertNil(DeepLinkAction(rawValue: "news1"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// DeepLinkTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 31.10.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
import WalletKit
|
||||
|
||||
final class DeepLinkTests: XCTestCase {
|
||||
|
||||
func testCreateWithParamsFailed() {
|
||||
XCTAssertNil(DeepLink.create(params: nil))
|
||||
XCTAssertNil(DeepLink.create(params: [:]))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "WalletNetwork",
|
||||
platforms: [.iOS(.v13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "WalletNetwork",
|
||||
targets: ["WalletNetwork"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "eosswift", path: "../../Vendors/spm/eos-swift"),
|
||||
.package(name: "EosioSwift", path: "../Vendors/spm/eosio-swift-1.0.0"),
|
||||
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.2")),
|
||||
.package(name: "WalletFoundation", path: "../WalletFoundation"),
|
||||
.package(name: "Mocker", path: "../../Vendors/spm/Mocker-3.0.1")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "WalletNetwork",
|
||||
dependencies: ["EosioSwift", "eosswift", "Alamofire", "WalletFoundation"],
|
||||
path: "./Sources"),
|
||||
.testTarget(
|
||||
name: "WalletNetworkTests",
|
||||
dependencies: ["WalletNetwork", "Mocker"],
|
||||
resources: [
|
||||
.process("Resources")
|
||||
])
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// AccountHyperionService.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 12/4/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
import WalletFoundation
|
||||
|
||||
public struct AccountHyperionService: NetworkService {
|
||||
|
||||
private struct Response: Codable {
|
||||
let accountNames: [String]
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accountNames = "account_names"
|
||||
}
|
||||
}
|
||||
|
||||
let environment: NetworkEnvironment
|
||||
|
||||
public init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
public func fetchNamesHyperion(publicKey: String) async -> [String] {
|
||||
|
||||
guard var components = URLComponents(url: self.environment.hyperion.url(), resolvingAgainstBaseURL: false) else {
|
||||
return []
|
||||
}
|
||||
|
||||
components.path = "/v2/state/get_key_accounts"
|
||||
components.queryItems = [URLQueryItem(name: "public_key", value: publicKey),
|
||||
URLQueryItem(name: "skip", value: "0"),
|
||||
URLQueryItem(name: "limit", value: "5000")]
|
||||
|
||||
guard let url = components.url else { return [] }
|
||||
|
||||
let response = try? await AF.request(
|
||||
url,
|
||||
method: .get,
|
||||
headers: self.environment.usernames.httpHeaders)
|
||||
.serializingDecodable(Response.self)
|
||||
.value
|
||||
|
||||
guard let response = response else { return [] }
|
||||
return response.accountNames
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// AccountService.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
import WalletFoundation
|
||||
|
||||
public struct AccountService: NetworkService {
|
||||
|
||||
public enum ServiceError: Error {
|
||||
case notificationEmptyAccount
|
||||
case notificationEmptyToken
|
||||
case notificationEmptyLanguage
|
||||
}
|
||||
|
||||
let environment: NetworkEnvironment
|
||||
let session: Session
|
||||
|
||||
public init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
if let configuration = environment.configuration {
|
||||
configuration.headers = HTTPHeaders.default
|
||||
self.session = Session(configuration: configuration)
|
||||
} else {
|
||||
self.session = AF
|
||||
}
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
public func create(username: String, pubKey: String, deviceId: String) async throws -> WalletOrder {
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .formatted(dateFormatter)
|
||||
|
||||
let variables = AccountCreateRequest.Variables(username: username, pubKey: pubKey, deviceId: deviceId)
|
||||
let result = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: AccountCreateRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders
|
||||
)
|
||||
.responseString { print(">>>>>> \($0)") }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<AccountCreateResponse>.self, decoder: decoder)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(response): return response.order
|
||||
case let .secondType(error): throw error.networkServiceError
|
||||
default: throw NetworkServiceError.uncatched
|
||||
}
|
||||
}
|
||||
|
||||
public func order(with id: String) async throws -> WalletOrder {
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .formatted(dateFormatter)
|
||||
|
||||
let variables = AccountOrderRequest.Variables(uid: id)
|
||||
let result = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: AccountOrderRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders
|
||||
)
|
||||
.responseString { print(">>>>>> \($0)") }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<AccountOrderResponse>.self, decoder: decoder)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(response): return response.order
|
||||
case let .secondType(error): throw error.networkServiceError
|
||||
default: throw NetworkServiceError.uncatched
|
||||
}
|
||||
}
|
||||
|
||||
public enum AvailableType {
|
||||
case available
|
||||
case alreadyTaken
|
||||
case system
|
||||
}
|
||||
|
||||
public func isAvailable(username: String) async -> AvailableType {
|
||||
|
||||
guard !username.isEmpty else { return .system }
|
||||
|
||||
let variables = AccountCheckNameRequest.Variables(username: username)
|
||||
|
||||
let response = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: AccountCheckNameRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders)
|
||||
.responseString { print("checkWalletName: \($0)") }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<AccountCheckNameResponse>.self)
|
||||
.value
|
||||
|
||||
guard let response = response else { return .system }
|
||||
switch response.result {
|
||||
case .firstType: return .available
|
||||
case let .secondType(type):
|
||||
switch type {
|
||||
case let .application(error: errorType) where errorType == "ALREADY_TAKEN":
|
||||
return .alreadyTaken
|
||||
default: return .system
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNotification(token: String, accounts: [String], language: String) async throws {
|
||||
|
||||
if accounts.isEmpty { throw ServiceError.notificationEmptyAccount }
|
||||
|
||||
if token.isEmpty { throw ServiceError.notificationEmptyToken }
|
||||
|
||||
if language.isEmpty { throw ServiceError.notificationEmptyLanguage }
|
||||
|
||||
let variables = AccountNotificationTokenRequest.Variables(
|
||||
deviceToken: token,
|
||||
deviceType: "IOS",
|
||||
eosAccounts: accounts,
|
||||
langCode: language,
|
||||
application: "MALINKA"
|
||||
)
|
||||
|
||||
let result = try? await self.session.request(
|
||||
self.environment.backend.url,
|
||||
method: .post,
|
||||
parameters: AccountNotificationTokenRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.backend.httpHeaders)
|
||||
// .responseString { print("updateNotification: \($0)") }
|
||||
.serializingDecodable(GraphQLResult<AccountNotificationTokenResponse>.self)
|
||||
.value
|
||||
|
||||
print(result ?? "")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// AccountNodeService.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 16.01.2023.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import Foundation
|
||||
|
||||
public struct NodeService: NetworkService {
|
||||
|
||||
let environment: NetworkEnvironment
|
||||
let session: Session
|
||||
|
||||
public init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
if let configuration = environment.configuration {
|
||||
configuration.headers = HTTPHeaders.default
|
||||
self.session = Session(configuration: configuration)
|
||||
} else {
|
||||
self.session = AF
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchActions(account: String,
|
||||
limit: Int,
|
||||
skip: Int? = nil) async throws -> [NodeAction] {
|
||||
guard let url = URL(string: "https://eos.greymass.com") else { return [] } //self.environment.hyperion.url()
|
||||
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return [] }
|
||||
|
||||
components.path = "/v1/history/get_actions"
|
||||
components.queryItems = [URLQueryItem(name: "account_name", value: account),
|
||||
URLQueryItem(name: "offset", value: "\(-limit)")]
|
||||
if let skip {
|
||||
components.queryItems?.append(URLQueryItem(name: "pos", value: "\(skip)"))
|
||||
}
|
||||
|
||||
do {
|
||||
let result = try await self.session.request(
|
||||
components,
|
||||
method: .get
|
||||
).serializingDecodable(NodeResponse.self)
|
||||
.value
|
||||
return result.actions
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// AccountCheckNameRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 11.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AccountCheckNameRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let username: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let variables: Variables
|
||||
let operationName = "checkWalletName"
|
||||
let query: String = #"""
|
||||
query checkWalletName(
|
||||
$username: String!
|
||||
) {
|
||||
checkWalletName(username: $username) { errors }
|
||||
}
|
||||
"""#.trimCompact()
|
||||
}
|
||||
|
||||
struct AccountCheckNameResponse: GraphQLResponse {
|
||||
static let node = "checkWalletName"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// AccountCreateRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 24.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AccountCreateRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let username: String
|
||||
let pubKey: String
|
||||
let deviceId: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let variables: Variables
|
||||
let operationName = "CreateAccount"
|
||||
let query: String = #"""
|
||||
mutation CreateAccount($username: String!, $pubKey: String!, $deviceId: String!) {
|
||||
createAccount(username: $username, pubKey: $pubKey, deviceId: $deviceId) {
|
||||
errors, order { timeout, id, modified, status }, id
|
||||
}
|
||||
}
|
||||
"""#.trimCompact()
|
||||
}
|
||||
|
||||
struct AccountCreateResponse: GraphQLResponse {
|
||||
|
||||
static let node = "createAccount"
|
||||
|
||||
let id: Int
|
||||
let order: WalletOrder
|
||||
}
|
||||
+19
-11
@@ -1,24 +1,25 @@
|
||||
//
|
||||
// AddPushNotificationDeviceTokenGraphQLRequest.swift
|
||||
// PayCash
|
||||
// AccountNotificationTokenRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Saveliy Stavitsky on 7/30/21.
|
||||
// Copyright © 2021 AM. All rights reserved.
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
struct UpdatePushNotificationDeviceTokenGraphQLRequest: Encodable {
|
||||
|
||||
struct ResponseData: Decodable { }
|
||||
|
||||
struct AccountNotificationTokenRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let deviceToken: String
|
||||
let deviceType: String
|
||||
let eosAccounts: [String]
|
||||
let langCode: String
|
||||
let application: String
|
||||
}
|
||||
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let variables: Variables
|
||||
let operationName = "updatePushNotificationDeviceToken"
|
||||
let query: String = #"""
|
||||
@@ -27,15 +28,22 @@ struct UpdatePushNotificationDeviceTokenGraphQLRequest: Encodable {
|
||||
$deviceType: DeviceTypeEnum,
|
||||
$eosAccounts: [String]!,
|
||||
$langCode: Lang!
|
||||
$application: ApplicationEnum
|
||||
) {
|
||||
updatePushNotificationDeviceToken(
|
||||
deviceToken: $deviceToken,
|
||||
deviceType: $deviceType,
|
||||
eosAccounts: $eosAccounts,
|
||||
langCode: $langCode
|
||||
langCode: $langCode,
|
||||
application: $application
|
||||
) {
|
||||
errors
|
||||
}
|
||||
}
|
||||
"""#
|
||||
"""#.trimCompact()
|
||||
|
||||
}
|
||||
|
||||
struct AccountNotificationTokenResponse: GraphQLResponse {
|
||||
static let node = "updatePushNotificationDeviceToken"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// AccountOrderRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 29.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AccountOrderRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let uid: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let variables: Variables
|
||||
let operationName = "accountOrder"
|
||||
let query: String = #"""
|
||||
query accountOrder(
|
||||
$uid: String!
|
||||
) {
|
||||
accountOrder(uid: $uid) { timeout, id, modified, status, created }
|
||||
}
|
||||
"""#.trimCompact()
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct AccountOrderResponse: GraphQLResponse {
|
||||
|
||||
private enum Common: String, CodingKey {
|
||||
case order = "accountOrder"
|
||||
}
|
||||
|
||||
static let node = ""
|
||||
|
||||
let order: WalletOrder
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: Common.self)
|
||||
self.order = try container.decode(WalletOrder.self, forKey: .order)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 8/28/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct WalletOrder: Codable {
|
||||
|
||||
public enum Status: String, Codable {
|
||||
/// Order created.
|
||||
case active = "ACTIVE"
|
||||
/// Order expired.
|
||||
case expired = "EXPIRED"
|
||||
/// Sent to blockchain.
|
||||
case executed = "EXECUTED"
|
||||
/// Accepting EOS transaction. Already sent, but not all blocks created.
|
||||
case creating = "TRX_IN_CHAIN"
|
||||
/// Failed.
|
||||
case failed = "FAILED"
|
||||
/// Created and completed.
|
||||
case completed = "COMPLETED"
|
||||
}
|
||||
|
||||
public let timeout: Date
|
||||
public let modified: Date
|
||||
public let status: Status
|
||||
public let id: String
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// NodeAct.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 10.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeAct: Decodable {
|
||||
public let data: NodeData
|
||||
public let name: String
|
||||
public let authorization: [NodeActAuthorization]
|
||||
public let account: String
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// NodeActAuthorization.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 10.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeActAuthorization: Codable {
|
||||
public let actor: String
|
||||
public let permission: String
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// NodeAction.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 10.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeAction: Decodable {
|
||||
public let accountActionSeq: Int
|
||||
public let actionTrace: NodeActionTrace
|
||||
public let globalActionSeq: Int
|
||||
public let irreversible: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accountActionSeq = "account_action_seq"
|
||||
case actionTrace = "action_trace"
|
||||
case globalActionSeq = "global_action_seq"
|
||||
case irreversible
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// NodeActionTrace.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 11.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeActionTrace: Decodable {
|
||||
public let act: NodeAct
|
||||
public let blockNum: Int
|
||||
public let blockTime: String
|
||||
public let receiver: String?
|
||||
public let trxId: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case blockNum = "block_num"
|
||||
case blockTime = "block_time"
|
||||
case trxId = "trx_id"
|
||||
case receiver
|
||||
case act
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// NodeData.swift
|
||||
//
|
||||
// Created by NUT.TECH on 10.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeData: Decodable {
|
||||
public let data: [String: Any]
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
// Create a decoding container using DynamicCodingKeys
|
||||
// The container will contain all the JSON first level key
|
||||
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
|
||||
|
||||
var tempData = [String: Any]()
|
||||
|
||||
// Loop through each key
|
||||
for key in container.allKeys {
|
||||
if let value = try? container.decode(NodeData.self, forKey: key) {
|
||||
tempData[key.stringValue] = value.data
|
||||
} else if let value = try? container.decode(String.self, forKey: key) {
|
||||
tempData[key.stringValue] = value
|
||||
} else if let value = try? container.decode(Int.self, forKey: key) {
|
||||
tempData[key.stringValue] = value
|
||||
}
|
||||
}
|
||||
|
||||
self.data = tempData
|
||||
}
|
||||
|
||||
private struct DynamicCodingKeys: CodingKey {
|
||||
// Use for string-keyed dictionary
|
||||
var stringValue: String
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
}
|
||||
|
||||
// Use for integer-keyed dictionary
|
||||
var intValue: Int?
|
||||
init?(intValue: Int) {
|
||||
// We are not using this, thus just return nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// NodeResponse.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 11.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeResponse: Decodable {
|
||||
public let actions: [NodeAction]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// CommonEnvironment.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 04.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct CommonEnvironment: NetworkEnvironment {
|
||||
|
||||
public let configuration: URLSessionConfiguration?
|
||||
public let usernames: NetworkStaticAPIRecord
|
||||
public let hyperion: NetworkDynamicAPIRecord
|
||||
public let backend: NetworkStaticAPIRecord
|
||||
public let node: NetworkDynamicAPIRecord
|
||||
|
||||
public init(usernames: NetworkStaticAPIRecord,
|
||||
backend: NetworkStaticAPIRecord,
|
||||
hyperion: NetworkDynamicAPIRecord,
|
||||
node: NetworkDynamicAPIRecord) {
|
||||
self.configuration = nil
|
||||
self.usernames = usernames
|
||||
self.backend = backend
|
||||
self.hyperion = hyperion
|
||||
self.node = node
|
||||
}
|
||||
|
||||
public init(usernames: URL,
|
||||
backend: URL,
|
||||
hyperion: @escaping NetworkDynamicAPIRecord.Dynamic,
|
||||
node: @escaping NetworkDynamicAPIRecord.Dynamic,
|
||||
headers: [String: String] = [:]) {
|
||||
let userRecord = NetworkStaticAPIRecord(url: usernames, headers: headers)
|
||||
let backendRecord = NetworkStaticAPIRecord(url: backend, headers: headers)
|
||||
let hyperionRecord = NetworkDynamicAPIRecord(url: hyperion, headers: headers)
|
||||
let nodeRecord = NetworkDynamicAPIRecord(url: node, headers: headers)
|
||||
|
||||
self.init(usernames: userRecord, backend: backendRecord, hyperion: hyperionRecord, node: nodeRecord)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// GraphQLResult.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletFoundation
|
||||
|
||||
protocol GraphQLResponse: Decodable {
|
||||
static var node: String { get }
|
||||
}
|
||||
|
||||
struct GraphQLError: Decodable {
|
||||
let message: String
|
||||
}
|
||||
|
||||
enum GraphQLResponseError: Decodable {
|
||||
|
||||
enum Place: CustomStringConvertible {
|
||||
case node
|
||||
case failed
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .node: return "node"
|
||||
case .failed: return "failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case system(errors: [GraphQLError])
|
||||
case application(error: String)
|
||||
case decode(Place)
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
self = .system(errors: [])
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
var networkServiceError: NetworkServiceError {
|
||||
switch self {
|
||||
case let .system(errors: errors): return .gqlSystem(errors.map { $0.message })
|
||||
case let .application(error: error): return .gqlApplication(error)
|
||||
case let .decode(place): return .gqlDecode(place.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let KEY_ERRORS = "errors"
|
||||
|
||||
struct GraphQLResult<Resp: GraphQLResponse>: Decodable {
|
||||
|
||||
let result: Either<Resp, GraphQLResponseError>
|
||||
|
||||
private enum Common: String, CodingKey {
|
||||
case data
|
||||
case errors
|
||||
}
|
||||
|
||||
private struct DynamicCodingKeys: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int?
|
||||
|
||||
init?(stringValue: String) { self.stringValue = stringValue }
|
||||
init?(intValue: Int) { return nil }
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: Common.self)
|
||||
// System error catch
|
||||
if let errors = try? container.decode([GraphQLError].self, forKey: .errors) {
|
||||
self.result = .secondType(.system(errors: errors))
|
||||
return
|
||||
}
|
||||
|
||||
// If node is empty
|
||||
guard !Resp.node.isEmpty else {
|
||||
if let response = try? container.decode(Resp.self, forKey: .data) {
|
||||
self.result = .firstType(response)
|
||||
} else {
|
||||
self.result = .secondType(.decode(.failed))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let dataContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .data)
|
||||
|
||||
guard let key = DynamicCodingKeys(stringValue: Resp.node) else {
|
||||
self.result = .secondType(.decode(.node))
|
||||
return
|
||||
}
|
||||
|
||||
if let responseContainer = try? dataContainer.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: key),
|
||||
let errorKey = DynamicCodingKeys(stringValue: KEY_ERRORS),
|
||||
let error = try? responseContainer.decode(String.self, forKey: errorKey) {
|
||||
self.result = .secondType(.application(error: error))
|
||||
} else if let response = try? dataContainer.decode(Resp.self, forKey: key) {
|
||||
self.result = .firstType(response)
|
||||
} else {
|
||||
self.result = .secondType(.decode(.failed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// NetworkService.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 04.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
|
||||
public struct NetworkStaticAPIRecord {
|
||||
|
||||
public typealias Headers = [String: String]
|
||||
|
||||
public let url: URL
|
||||
public let headers: Headers?
|
||||
|
||||
public init(url: URL, headers: Headers?) {
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
}
|
||||
|
||||
var httpHeaders: HTTPHeaders? {
|
||||
guard let headers = self.headers else { return nil }
|
||||
return HTTPHeaders(headers.compactMap { key, value in HTTPHeader(name: key, value: value) })
|
||||
}
|
||||
}
|
||||
|
||||
public struct NetworkDynamicAPIRecord {
|
||||
|
||||
public typealias Headers = [String: String]
|
||||
public typealias Dynamic = () -> URL
|
||||
|
||||
public let url: Dynamic
|
||||
public let headers: Headers?
|
||||
|
||||
public init(url: @escaping Dynamic, headers: Headers?) {
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
}
|
||||
|
||||
var httpHeaders: HTTPHeaders? {
|
||||
guard let headers = self.headers else { return nil }
|
||||
return HTTPHeaders(headers.compactMap { key, value in HTTPHeader(name: key, value: value) })
|
||||
}
|
||||
}
|
||||
|
||||
public protocol NetworkEnvironment {
|
||||
var configuration: URLSessionConfiguration? { get }
|
||||
var usernames: NetworkStaticAPIRecord { get }
|
||||
var backend: NetworkStaticAPIRecord { get }
|
||||
var hyperion: NetworkDynamicAPIRecord { get }
|
||||
var node: NetworkDynamicAPIRecord { get }
|
||||
|
||||
func isEquals(other: NetworkEnvironment) -> Bool
|
||||
}
|
||||
|
||||
public protocol NetworkService {
|
||||
init(environment: NetworkEnvironment)
|
||||
}
|
||||
|
||||
public enum NetworkServiceError: Error {
|
||||
case invalidUrl
|
||||
case gqlSystem([String])
|
||||
case gqlApplication(String)
|
||||
case gqlDecode(String)
|
||||
case uncatched
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
extension NetworkEnvironment {
|
||||
public func isEquals(other: NetworkEnvironment) -> Bool {
|
||||
return self.configuration == other.configuration &&
|
||||
self.usernames.url == other.usernames.url &&
|
||||
self.usernames.headers == other.usernames.headers &&
|
||||
self.backend.url == other.backend.url &&
|
||||
self.backend.headers == other.backend.headers &&
|
||||
self.hyperion.url() == other.hyperion.url() &&
|
||||
self.hyperion.headers == other.hyperion.headers &&
|
||||
self.node.url() == other.node.url() &&
|
||||
self.node.headers == other.node.headers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// DeviceService.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
|
||||
public struct DeviceService: NetworkService {
|
||||
|
||||
let environment: NetworkEnvironment
|
||||
let session: Session
|
||||
|
||||
public init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
if let configuration = environment.configuration {
|
||||
configuration.headers = HTTPHeaders.default
|
||||
self.session = Session(configuration: configuration)
|
||||
} else {
|
||||
self.session = AF
|
||||
}
|
||||
}
|
||||
|
||||
public func createDevice(uuid: String) async throws -> Device {
|
||||
|
||||
let variables = RegisterDeviceRequest.Variables(cid: uuid)
|
||||
|
||||
let result = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: RegisterDeviceRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders
|
||||
)
|
||||
// .responseString { print($0) }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<RegisterDeviceResponse>.self)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(device): return Device(uuid: uuid, id: device.uid, isTrusted: device.isTrustedNow)
|
||||
case let .secondType(error): throw error.networkServiceError
|
||||
default: throw NetworkServiceError.uncatched
|
||||
}
|
||||
}
|
||||
|
||||
public func stateDevice(id: String) async throws -> StateDevice {
|
||||
|
||||
let variables = StateDeviceRequest.Variables(uid: id)
|
||||
|
||||
let result = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: StateDeviceRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders
|
||||
)
|
||||
// .responseString { print(">>>>>> \($0)") }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<StateDeviceResponse>.self)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(state): return StateDevice(availableAccounts: state.availableAccounts,
|
||||
isTrusted: state.isTrustedNow)
|
||||
case let .secondType(error): throw error.networkServiceError
|
||||
default: throw NetworkServiceError.uncatched
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func checkDevice(id: String, token: String) async throws -> DeviceStatus {
|
||||
|
||||
let variables = CheckDeviceRequest.Variables(uid: id, token: token)
|
||||
|
||||
let result = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: CheckDeviceRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders
|
||||
)
|
||||
// .responseString { print($0) }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<CheckDeviceResponse>.self)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(status): return status.status
|
||||
case let .secondType(error): throw error.networkServiceError
|
||||
default: throw NetworkServiceError.uncatched
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// CheckDeviceRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 24.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct CheckDeviceRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let uid: String
|
||||
let token: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let operationName = "CheckDevice"
|
||||
let query: String = #"""
|
||||
mutation CheckDevice($uid: String!, $token: String!) {
|
||||
checkDevice(uid: $uid, token: $token) {
|
||||
status
|
||||
}
|
||||
}
|
||||
"""#.trimCompact()
|
||||
let variables: Variables
|
||||
}
|
||||
|
||||
struct CheckDeviceResponse: GraphQLResponse {
|
||||
|
||||
static let node = "checkDevice"
|
||||
|
||||
let status: DeviceStatus
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// RegisterDeviceRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 04.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct RegisterDeviceRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let cid: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let operationName = "RegisterDevice"
|
||||
let query: String = #"""
|
||||
mutation RegisterDevice($cid: String) {
|
||||
registerDevice(kind: IOS, cid: $cid) {
|
||||
isTrustedNow
|
||||
uid
|
||||
}
|
||||
}
|
||||
"""#.trimCompact()
|
||||
let variables: Variables
|
||||
}
|
||||
|
||||
struct RegisterDeviceResponse: GraphQLResponse {
|
||||
|
||||
static let node = "registerDevice"
|
||||
|
||||
let isTrustedNow: Bool
|
||||
let uid: String
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// StateDeviceRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 29.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct StateDeviceRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let uid: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let variables: Variables
|
||||
let operationName = "device"
|
||||
let query: String = #"""
|
||||
query device(
|
||||
$uid: String!
|
||||
) {
|
||||
device(uid: $uid) { availableAccounts, isTrustedNow }
|
||||
}
|
||||
"""#.trimCompact()
|
||||
}
|
||||
|
||||
struct StateDeviceResponse: GraphQLResponse {
|
||||
static let node = "device"
|
||||
|
||||
let availableAccounts: Int
|
||||
let isTrustedNow: Bool
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Device.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Model represent device
|
||||
public struct Device {
|
||||
/// Device generated Identity UUID
|
||||
public let uuid: String
|
||||
/// Server generated Identity
|
||||
public let id: String
|
||||
/// Is device trusted
|
||||
public let isTrusted: Bool
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// DeviceStatus.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 23.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum DeviceStatus: String, Decodable {
|
||||
case valid = "Valid"
|
||||
case invalid = "Invalid"
|
||||
case unexpected = "UnexpectedError"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// StateDevice.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 29.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct StateDevice {
|
||||
public let availableAccounts: Int
|
||||
public let isTrusted: Bool
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// EOSService.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 31.07.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
import WalletFoundation
|
||||
import EosioSwift
|
||||
import eosswift
|
||||
|
||||
public struct EOSService: NetworkService {
|
||||
|
||||
let environment: NetworkEnvironment
|
||||
|
||||
public init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
public func requireNodes() async -> [String] {
|
||||
|
||||
let result = try? await AF.request(
|
||||
self.environment.backend.url,
|
||||
method: .post,
|
||||
parameters: EOSNodeRequest(),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.backend.httpHeaders)
|
||||
// .responseString(completionHandler: { print(">>>~~~>>> \($0)") })
|
||||
.serializingDecodable(GraphQLResult<EOSNodesResponse>.self)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(value):
|
||||
return value.nodes.map { $0.url }
|
||||
default:
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func requireHyperions() async -> [String] {
|
||||
|
||||
let result = try? await AF.request(
|
||||
self.environment.backend.url,
|
||||
method: .post,
|
||||
parameters: EOSHyperionsRequest(),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.backend.httpHeaders)
|
||||
.serializingDecodable(GraphQLResult<EOSHyperionsResponse>.self)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(value):
|
||||
return value.hyperions.map { $0.url }
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public func getAccount(_ account: String, completion: @escaping (EosioResult<EosioRpcAccountResponse, EosioError>) -> Void) {
|
||||
EosioRpcProvider(endpoint: self.environment.node.url(), headers: self.environment.node.headers)
|
||||
.getAccount(requestParameters: EosioRpcAccountRequest(accountName: account), completion: completion)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// EOSHyperionsRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by NUT.Tech on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EOSHyperionsRequest: Encodable {
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let query: String = #"""
|
||||
query {
|
||||
getEOSHyperions {
|
||||
hyperions {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
"""#.trimCompact()
|
||||
}
|
||||
|
||||
struct EOSHyperionsResponse: GraphQLResponse {
|
||||
|
||||
static let node = "getEOSHyperions"
|
||||
|
||||
struct Hyperion: Decodable {
|
||||
let url: String
|
||||
}
|
||||
|
||||
let hyperions: [Hyperion]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// EOSNodesQueryRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EOSNodeRequest: Encodable {
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let operationName = "getEOSNodes"
|
||||
let query: String = #"""
|
||||
query getEOSNodes {
|
||||
getEOSNodes {
|
||||
nodes {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
"""#.trimCompact()
|
||||
let variables: [String: String]? = nil
|
||||
}
|
||||
|
||||
struct EOSNodesResponse: GraphQLResponse {
|
||||
|
||||
static let node = "getEOSNodes"
|
||||
|
||||
struct Node: Decodable {
|
||||
let url: String
|
||||
}
|
||||
|
||||
let nodes: [Node]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
//
|
||||
// DeviceServiceTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 13.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
import WalletFoundation
|
||||
@testable
|
||||
import WalletNetwork
|
||||
import Mocker
|
||||
|
||||
final class DeviceServiceTests: XCTestCase {
|
||||
|
||||
override func tearDown() {
|
||||
Mocker.removeAll()
|
||||
}
|
||||
|
||||
// MARK: - CreateDevice
|
||||
|
||||
// When receive incorrect status we must throw uncatched exception.
|
||||
func testCreateDeviceFailUncatched() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/create")!
|
||||
let uuid = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 500,
|
||||
data: [ .post: Data() ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
_ = try await service.createDevice(uuid: uuid)
|
||||
XCTFail("Require to fail")
|
||||
} catch NetworkServiceError.uncatched {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTFail("Require '\(NetworkServiceError.uncatched)' exception")
|
||||
}
|
||||
}
|
||||
|
||||
// When we receive incorrect data that we can't decode.
|
||||
func testCreateDeviceFailDecode() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/create")!
|
||||
let uuid = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .post: DeviceServiceData.createDeviceDecodeFailed ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
_ = try await service.createDevice(uuid: uuid)
|
||||
} catch NetworkServiceError.gqlDecode(let message) {
|
||||
XCTAssertEqual(message, "failed")
|
||||
} catch {
|
||||
XCTFail("Catch exceprion '\(error)' but require NetworkServiceError.gqlDecode")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// When we successed create device.
|
||||
func testCreateDeviceSuccessTrusted() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/create")!
|
||||
let uuid = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .post: DeviceServiceData.createDeviceTrusted ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
let device = try await service.createDevice(uuid: uuid)
|
||||
XCTAssertTrue(device.isTrusted)
|
||||
XCTAssertEqual(device.id, "a88c4532a09024c245e9")
|
||||
XCTAssertEqual(device.uuid, uuid)
|
||||
} catch {
|
||||
XCTFail("Catch exceprion \(error)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test send correct request.
|
||||
func testCreateDeviceSuccessRequest() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/query")!
|
||||
let uuid = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
let requestExpectation = XCTestExpectation(description: "Request")
|
||||
let completionExpectation = XCTestExpectation(description: "Completion")
|
||||
|
||||
var mock = Mock(url: url, dataType: .json, statusCode: 200, data: [.post: Data()])
|
||||
mock.onRequestHandler = OnRequestHandler(httpBodyType: [String: Either<String, [String: String]>].self) { request, object in
|
||||
|
||||
XCTAssertEqual(url, mock.request.url)
|
||||
XCTAssertEqual(request.method, .post)
|
||||
|
||||
XCTAssertNotNil(object)
|
||||
|
||||
let registerDevice = RegisterDeviceRequest(variables: RegisterDeviceRequest.Variables(cid: uuid))
|
||||
|
||||
XCTAssertNotNil(object!["query"])
|
||||
object!["query"]!.map(firstTypeTransform: {
|
||||
XCTAssertEqual($0, registerDevice.query)
|
||||
}, secondTypeTransform: {
|
||||
XCTFail("Require 'String' type but receive \($0)")
|
||||
})
|
||||
|
||||
XCTAssertNotNil(object!["operationName"])
|
||||
object!["operationName"]!.map(firstTypeTransform: {
|
||||
XCTAssertEqual($0, registerDevice.operationName)
|
||||
}, secondTypeTransform: {
|
||||
XCTFail("Require 'String' type but receive \($0)")
|
||||
})
|
||||
|
||||
XCTAssertNotNil(object!["variables"])
|
||||
object!["variables"]!.map(firstTypeTransform: {
|
||||
XCTFail("Require '[String: String]' type but receive \($0)")
|
||||
}, secondTypeTransform: {
|
||||
XCTAssertNotNil($0["cid"])
|
||||
XCTAssertEqual($0["cid"], registerDevice.variables.cid)
|
||||
})
|
||||
|
||||
requestExpectation.fulfill()
|
||||
}
|
||||
mock.completion = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
mock.register()
|
||||
|
||||
_ = try? await service.createDevice(uuid: uuid)
|
||||
|
||||
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
|
||||
|
||||
}
|
||||
|
||||
// MARK: - StateDevice
|
||||
|
||||
// Receive null values in fields - we must throw exception.
|
||||
func testQueryStateDeviceException() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/query")!
|
||||
let id = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .post: DeviceServiceData.queryDeviceStateNull ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
_ = try await service.stateDevice(id: id)
|
||||
XCTFail("Require catch exceprion NetworkServiceError.gqlDecode")
|
||||
} catch {
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Receive untrusted status.
|
||||
func testQueryStateDeviceUntrusted() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/query")!
|
||||
let id = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .post: DeviceServiceData.queryDeviceStateUntrusted ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
let state = try await service.stateDevice(id: id)
|
||||
XCTAssertFalse(state.isTrusted)
|
||||
XCTAssertEqual(state.availableAccounts, 0)
|
||||
} catch {
|
||||
XCTFail("Catch exceprion \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Receive trusted status.
|
||||
func testQueryStateDeviceTrusted() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/query")!
|
||||
let id = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .post: DeviceServiceData.queryDeviceStateTrusted ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
let state = try await service.stateDevice(id: id)
|
||||
XCTAssertTrue(state.isTrusted)
|
||||
XCTAssertEqual(state.availableAccounts, 2)
|
||||
} catch {
|
||||
XCTFail("Catch exceprion \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Check sent correct request.
|
||||
func testQueryStateDeviceRequest() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/query")!
|
||||
let id = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
let requestExpectation = XCTestExpectation(description: "Request")
|
||||
let completionExpectation = XCTestExpectation(description: "Completion")
|
||||
|
||||
var mock = Mock(url: url, dataType: .json, statusCode: 200, data: [.post: Data()])
|
||||
mock.onRequestHandler = OnRequestHandler(httpBodyType: [String: Either<String, [String: String]>].self) { request, object in
|
||||
|
||||
XCTAssertEqual(url, mock.request.url)
|
||||
XCTAssertEqual(request.method, .post)
|
||||
|
||||
XCTAssertNotNil(object)
|
||||
|
||||
let stateRequest = StateDeviceRequest(variables: StateDeviceRequest.Variables(uid: id))
|
||||
|
||||
XCTAssertNotNil(object!["query"])
|
||||
object!["query"]!.map(firstTypeTransform: {
|
||||
XCTAssertEqual($0, stateRequest.query)
|
||||
}, secondTypeTransform: {
|
||||
XCTFail("Require 'String' type but receive \($0)")
|
||||
})
|
||||
|
||||
XCTAssertNotNil(object!["operationName"])
|
||||
object!["operationName"]!.map(firstTypeTransform: {
|
||||
XCTAssertEqual($0, stateRequest.operationName)
|
||||
}, secondTypeTransform: {
|
||||
XCTFail("Require 'String' type but receive \($0)")
|
||||
})
|
||||
|
||||
XCTAssertNotNil(object!["variables"])
|
||||
object!["variables"]!.map(firstTypeTransform: {
|
||||
XCTFail("Require '[String: String]' type but receive \($0)")
|
||||
}, secondTypeTransform: {
|
||||
XCTAssertNotNil($0["uid"])
|
||||
XCTAssertEqual($0["uid"], stateRequest.variables.uid)
|
||||
})
|
||||
|
||||
requestExpectation.fulfill()
|
||||
}
|
||||
mock.completion = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
mock.register()
|
||||
|
||||
_ = try? await service.stateDevice(id: id)
|
||||
|
||||
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
|
||||
}
|
||||
|
||||
// MARK: - CheckDevice
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// DeviceServiceData.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 13.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class DeviceServiceData {
|
||||
|
||||
static let createDeviceTrusted = try! Data(contentsOf: Bundle.module.url(forResource: "CreateDeviceSuccessTrusted",
|
||||
withExtension: "json")!)
|
||||
static let createDeviceDecodeFailed = try! Data(contentsOf: Bundle.module.url(forResource: "CreateDeviceSuccessNull",
|
||||
withExtension: "json")!)
|
||||
|
||||
static let queryDeviceStateNull = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessNull",
|
||||
withExtension: "json")!)
|
||||
|
||||
static let queryDeviceStateUntrusted = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessUntrusted",
|
||||
withExtension: "json")!)
|
||||
|
||||
static let queryDeviceStateTrusted = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessTrusted",
|
||||
withExtension: "json")!)
|
||||
|
||||
static let nodeActionsExample = try! Data(contentsOf: Bundle.module.url(forResource: "v1historyTestmalinka1",
|
||||
withExtension: "json")!)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// NetworkEnvironmentImpl.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 13.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletNetwork
|
||||
|
||||
struct NetworkEnvironmentImpl: NetworkEnvironment {
|
||||
|
||||
let configuration: URLSessionConfiguration?
|
||||
let usernames: NetworkStaticAPIRecord
|
||||
let backend: NetworkStaticAPIRecord
|
||||
let hyperion: NetworkDynamicAPIRecord
|
||||
let node: NetworkDynamicAPIRecord
|
||||
|
||||
static func createStubUsernames(configuration: URLSessionConfiguration,
|
||||
url: URL,
|
||||
headers: NetworkStaticAPIRecord.Headers? = nil) -> NetworkEnvironment {
|
||||
let record = NetworkStaticAPIRecord(url: url, headers: headers)
|
||||
|
||||
let dumpUrl = URL(string: "Empty")!
|
||||
return NetworkEnvironmentImpl(configuration: configuration,
|
||||
usernames: record,
|
||||
backend: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
|
||||
hyperion: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil),
|
||||
node: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil))
|
||||
}
|
||||
|
||||
static func createStubNodes(configuration: URLSessionConfiguration,
|
||||
url: URL,
|
||||
headers: NetworkStaticAPIRecord.Headers? = nil) -> NetworkEnvironment {
|
||||
let record = NetworkDynamicAPIRecord(url: { url }, headers: headers)
|
||||
|
||||
let dumpUrl = URL(string: "Empty")!
|
||||
return NetworkEnvironmentImpl(configuration: configuration,
|
||||
usernames: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
|
||||
backend: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
|
||||
hyperion: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil),
|
||||
node: record)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// NodeActionModelsTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 13.01.2023.
|
||||
//
|
||||
|
||||
import WalletNetwork
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
final class NodeActionModelsTests: XCTestCase {
|
||||
|
||||
func testNodeActAuthorization() {
|
||||
let data = """
|
||||
{
|
||||
"actor": "testmalinka1",
|
||||
"permission": "owner"
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let nodeAct = try? JSONDecoder().decode(NodeActAuthorization.self, from: data)
|
||||
XCTAssertNotNil(nodeAct, "Error parsing NodeActAuthorization object")
|
||||
XCTAssertEqual(nodeAct?.actor, "testmalinka1", "Error parsing NodeActAuthorization.actor value")
|
||||
XCTAssertEqual(nodeAct?.permission, "owner", "Error parsing NodeActAuthorization.owner value")
|
||||
}
|
||||
|
||||
func testNodeData() {
|
||||
let data = """
|
||||
{
|
||||
"from": "testmalinka1",
|
||||
"memo": "buyram:testmalinka1",
|
||||
"quantity": "0.1000 EOS",
|
||||
"to": "malinkawallt"
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let nodeData = try? JSONDecoder().decode(NodeData.self, from: data)
|
||||
XCTAssertNotNil(nodeData, "Error parsing NodeData object")
|
||||
guard let parsedData = nodeData?.data else {
|
||||
XCTFail("Error parsing data value")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(parsedData["from"] as? String, "testmalinka1", "Error parsing data value")
|
||||
XCTAssertEqual(parsedData["memo"] as? String, "buyram:testmalinka1", "Error parsing data value")
|
||||
XCTAssertEqual(parsedData["quantity"] as? String, "0.1000 EOS", "Error parsing data value")
|
||||
XCTAssertEqual(parsedData["to"] as? String, "malinkawallt", "Error parsing data value")
|
||||
}
|
||||
|
||||
func testNodeAct() {
|
||||
let data = """
|
||||
{
|
||||
"account": "eosio.token",
|
||||
"authorization": [
|
||||
{
|
||||
"actor": "testmalinka1",
|
||||
"permission": "owner"
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"from": "testmalinka1",
|
||||
"memo": "buyram:testmalinka1",
|
||||
"quantity": "0.1000 EOS",
|
||||
"to": "malinkawallt"
|
||||
},
|
||||
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
|
||||
"name": "transfer"
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let nodeAct = try? JSONDecoder().decode(NodeAct.self, from: data)
|
||||
XCTAssertNotNil(nodeAct, "Error parsing NodeAct object")
|
||||
XCTAssertNotNil(nodeAct?.data, "Error parsing NodeAct.data object")
|
||||
XCTAssertNotNil(nodeAct?.authorization, "buyram:testmalinka1", file: "Error parsing NodeAct.authorization objects")
|
||||
XCTAssertEqual(nodeAct?.authorization.count, 1, "Error parsing NodeAct.authorization objects")
|
||||
XCTAssertEqual(nodeAct?.name, "transfer", "Error parsing NodeAct.name value")
|
||||
XCTAssertEqual(nodeAct?.account, "eosio.token", "Error parsing NodeAct.account value")
|
||||
}
|
||||
|
||||
func testNodeActionTrace() {
|
||||
guard let url = Bundle.module.url(forResource: "NodeActionTrace", withExtension: "json"),
|
||||
let data = try? Data(contentsOf: url),
|
||||
let result = try? JSONDecoder().decode(NodeActionTrace.self, from: data)
|
||||
else {
|
||||
XCTFail("Error reading JSON file")
|
||||
return
|
||||
}
|
||||
XCTAssertNotNil(result.act, "Error parsing NodeActionTrace.act object")
|
||||
XCTAssertEqual(result.blockNum, 288713481, "Error parsing NodeActionTrace.blockNum value")
|
||||
XCTAssertEqual(result.blockTime, "2023-01-12T15:27:01.500", "Error parsing NodeActionTrace.blockTime value")
|
||||
XCTAssertEqual(result.receiver, "malinkawallt", "Error parsing NodeActionTrace.receiver value")
|
||||
XCTAssertEqual(result.trxId, "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c", "Error parsing NodeActionTrace.trxId value")
|
||||
}
|
||||
|
||||
func testNodeAction() {
|
||||
guard let url = Bundle.module.url(forResource: "NodeAction", withExtension: "json"),
|
||||
let data = try? Data(contentsOf: url),
|
||||
let result = try? JSONDecoder().decode(NodeAction.self, from: data)
|
||||
else {
|
||||
XCTFail("Error reading JSON file")
|
||||
return
|
||||
}
|
||||
XCTAssertNotNil(result.actionTrace, "Error parsing NodeAction.actionTrace value")
|
||||
XCTAssertEqual(result.accountActionSeq, 2858, "Error parsing NodeAction.accountActionSeq value")
|
||||
XCTAssertEqual(result.globalActionSeq, 357585823156, "Error parsing NodeAction.globalActionSeq value")
|
||||
XCTAssertTrue(result.irreversible, "Error parsing NodeAction.irreversible value")
|
||||
}
|
||||
|
||||
func testNodeResponse() {
|
||||
guard let url = Bundle.module.url(forResource: "NodeResponse", withExtension: "json"),
|
||||
let data = try? Data(contentsOf: url),
|
||||
let result = try? JSONDecoder().decode(NodeResponse.self, from: data)
|
||||
else {
|
||||
XCTFail("Error reading JSON file")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertNotNil(result.actions, "Error parsing NodeResponse")
|
||||
XCTAssertEqual(result.actions.count, 1, "Error parsing NodeResponse.actions")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// NodeServiceTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 16.01.2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import WalletNetwork
|
||||
import Mocker
|
||||
|
||||
final class NodeServiceTests: XCTestCase {
|
||||
|
||||
override func tearDown() {
|
||||
Mocker.removeAll()
|
||||
}
|
||||
|
||||
func testFetchNodeActions() async {
|
||||
let accountName = "testmalinka3"
|
||||
let offset = -100
|
||||
let url = URL(string: "https://eos.greymass.com/v1/history/get_actions?account_name=\(accountName)&offset=\(offset)")!
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubNodes(configuration: configuration, url: url)
|
||||
let service = NodeService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
ignoreQuery: true,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .get: DeviceServiceData.nodeActionsExample ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
let state = try await service.fetchActions(account: accountName,
|
||||
limit: offset)
|
||||
guard let firstAction = state.first else {
|
||||
XCTFail("Error parsing NodeActions")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(state.count, 100, "Incorrect number of node actions")
|
||||
XCTAssertEqual(firstAction.accountActionSeq, 2759, "Error parsing NodeAction.accountActionSeq")
|
||||
XCTAssertEqual(firstAction.globalActionSeq, 357563493447, "Error parsing NodeAction.globalActionSeq")
|
||||
XCTAssertTrue(firstAction.irreversible, "Error parsing NodeAction.irreversible")
|
||||
XCTAssertNotNil(firstAction.actionTrace, "Error parsing NodeAction.actionTrace")
|
||||
XCTAssertEqual(firstAction.actionTrace.blockTime, "2023-01-09T13:55:06.000", "Error parsing NodeAction.actionTrace.blockTime")
|
||||
XCTAssertEqual(firstAction.actionTrace.blockNum, 288184258, "Error parsing NodeAction.actionTrace.blockNum")
|
||||
XCTAssertEqual(firstAction.actionTrace.trxId, "a4dfd44c82cf0a3be1930bf6e4ec6ea51c1b331a4e77864a0d2cae4d06e8683d", "Error parsing NodeAction.actionTrace.trxId")
|
||||
XCTAssertEqual(firstAction.actionTrace.receiver, "testmalinka1", "Error parsing NodeAction.actionTrace.receiver")
|
||||
let act = firstAction.actionTrace.act
|
||||
XCTAssertEqual(act.account, "eosio.token", "Error parsing NodeActionTrace.act.account")
|
||||
XCTAssertEqual(act.name, "transfer", "Error parsing NodeActionTrace.act.name")
|
||||
XCTAssertEqual(act.authorization.count, 1, "Error parsing NodeActionTrace.act.authorization")
|
||||
XCTAssertNotNil(act.data, "Error parsing NodeActionTrace.act.data")
|
||||
guard let authorization = act.authorization.first else {
|
||||
XCTFail("Error parsing NodeAction.actionTrace.act.authorization")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(authorization.permission, "owner", "Error parsing NodeActionTrace.act.authorization.permission")
|
||||
XCTAssertEqual(authorization.actor, "testmalinka1", "Error parsing NodeActionTrace.act.authorization.actor")
|
||||
} catch {
|
||||
XCTFail("Catch exceprion \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"data": {"registerDevice": {"isTrustedNow": false, "uid": null}}}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"data": {"registerDevice": {"isTrustedNow": true, "uid": "a88c4532a09024c245e9"}}}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"account_action_seq": 2858,
|
||||
"action_trace": {
|
||||
"account_ram_deltas": [],
|
||||
"act": {
|
||||
"account": "eosio.token",
|
||||
"authorization": [
|
||||
{
|
||||
"actor": "testmalinka1",
|
||||
"permission": "owner"
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"from": "testmalinka1",
|
||||
"memo": "buyram:testmalinka1",
|
||||
"quantity": "0.1000 EOS",
|
||||
"to": "malinkawallt"
|
||||
},
|
||||
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
|
||||
"name": "transfer"
|
||||
},
|
||||
"action_ordinal": 4,
|
||||
"block_num": 288713481,
|
||||
"block_time": "2023-01-12T15:27:01.500",
|
||||
"closest_unnotified_ancestor_action_ordinal": 2,
|
||||
"context_free": false,
|
||||
"creator_action_ordinal": 2,
|
||||
"elapsed": 2,
|
||||
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
|
||||
"receipt": {
|
||||
"abi_sequence": 4,
|
||||
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
|
||||
"auth_sequence": [
|
||||
[
|
||||
"testmalinka1",
|
||||
2192
|
||||
]
|
||||
],
|
||||
"code_sequence": 4,
|
||||
"global_sequence": 357585823156,
|
||||
"receiver": "malinkawallt",
|
||||
"recv_sequence": 14905
|
||||
},
|
||||
"receiver": "malinkawallt",
|
||||
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
|
||||
},
|
||||
"block_num": 288713481,
|
||||
"block_time": "2023-01-12T15:27:01.500",
|
||||
"global_action_seq": 357585823156,
|
||||
"irreversible": true
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"account_ram_deltas": [],
|
||||
"act": {
|
||||
"account": "eosio.token",
|
||||
"authorization": [
|
||||
{
|
||||
"actor": "testmalinka1",
|
||||
"permission": "owner"
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"from": "testmalinka1",
|
||||
"memo": "buyram:testmalinka1",
|
||||
"quantity": "0.1000 EOS",
|
||||
"to": "malinkawallt"
|
||||
},
|
||||
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
|
||||
"name": "transfer"
|
||||
},
|
||||
"action_ordinal": 4,
|
||||
"block_num": 288713481,
|
||||
"block_time": "2023-01-12T15:27:01.500",
|
||||
"closest_unnotified_ancestor_action_ordinal": 2,
|
||||
"context_free": false,
|
||||
"creator_action_ordinal": 2,
|
||||
"elapsed": 2,
|
||||
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
|
||||
"receipt": {
|
||||
"abi_sequence": 4,
|
||||
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
|
||||
"auth_sequence": [
|
||||
[
|
||||
"testmalinka1",
|
||||
2192
|
||||
]
|
||||
],
|
||||
"code_sequence": 4,
|
||||
"global_sequence": 357585823156,
|
||||
"receiver": "malinkawallt",
|
||||
"recv_sequence": 14905
|
||||
},
|
||||
"receiver": "malinkawallt",
|
||||
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"account_action_seq": 2858,
|
||||
"action_trace": {
|
||||
"account_ram_deltas": [],
|
||||
"act": {
|
||||
"account": "eosio.token",
|
||||
"authorization": [
|
||||
{
|
||||
"actor": "testmalinka1",
|
||||
"permission": "owner"
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"from": "testmalinka1",
|
||||
"memo": "buyram:testmalinka1",
|
||||
"quantity": "0.1000 EOS",
|
||||
"to": "malinkawallt"
|
||||
},
|
||||
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
|
||||
"name": "transfer"
|
||||
},
|
||||
"action_ordinal": 4,
|
||||
"block_num": 288713481,
|
||||
"block_time": "2023-01-12T15:27:01.500",
|
||||
"closest_unnotified_ancestor_action_ordinal": 2,
|
||||
"context_free": false,
|
||||
"creator_action_ordinal": 2,
|
||||
"elapsed": 2,
|
||||
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
|
||||
"receipt": {
|
||||
"abi_sequence": 4,
|
||||
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
|
||||
"auth_sequence": [
|
||||
[
|
||||
"testmalinka1",
|
||||
2192
|
||||
]
|
||||
],
|
||||
"code_sequence": 4,
|
||||
"global_sequence": 357585823156,
|
||||
"receiver": "malinkawallt",
|
||||
"recv_sequence": 14905
|
||||
},
|
||||
"receiver": "malinkawallt",
|
||||
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
|
||||
},
|
||||
"block_num": 288713481,
|
||||
"block_time": "2023-01-12T15:27:01.500",
|
||||
"global_action_seq": 357585823156,
|
||||
"irreversible": true
|
||||
}
|
||||
],
|
||||
"head_block_num": 288838483,
|
||||
"last_irreversible_block": 288838158
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"data": {
|
||||
"device": {
|
||||
"availableAccounts": null,
|
||||
"isTrustedNow": null
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"data": {
|
||||
"device": {
|
||||
"availableAccounts": 2,
|
||||
"isTrustedNow": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"data": {
|
||||
"device": {
|
||||
"availableAccounts": 0,
|
||||
"isTrustedNow": false
|
||||
}
|
||||
}
|
||||
}
|
||||
+5276
File diff suppressed because it is too large
Load Diff
@@ -1,72 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>JMalinkaWallet.ipa</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>architectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>bitcode</key>
|
||||
<false/>
|
||||
<key>buildNumber</key>
|
||||
<string>12</string>
|
||||
<key>certificate</key>
|
||||
<dict>
|
||||
<key>SHA1</key>
|
||||
<string>35C003923C51D78DF01DBCBFF8DAC6666C09412D</string>
|
||||
<key>dateExpires</key>
|
||||
<string>8/4/22</string>
|
||||
<key>type</key>
|
||||
<string>iOS Distribution</string>
|
||||
</dict>
|
||||
<key>entitlements</key>
|
||||
<dict>
|
||||
<key>application-identifier</key>
|
||||
<string>GENPCTDS3G.com.juraldinio.wallet</string>
|
||||
<key>aps-environment</key>
|
||||
<string>production</string>
|
||||
<key>beta-reports-active</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:paycashonline.test-app.link</string>
|
||||
<string>applinks:paycashonline-alternate.test-app.link</string>
|
||||
<string>applinks:paycashonline-alternate.app.link</string>
|
||||
<string>applinks:paycashonline.app.link</string>
|
||||
</array>
|
||||
<key>com.apple.developer.devicecheck.appattest-environment</key>
|
||||
<string>production</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>GENPCTDS3G</string>
|
||||
<key>get-task-allow</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>JMalinkaWallet.app</string>
|
||||
<key>profile</key>
|
||||
<dict>
|
||||
<key>UUID</key>
|
||||
<string>215599e9-99cb-4fbc-832f-12be30a2bcb0</string>
|
||||
<key>dateExpires</key>
|
||||
<string>8/4/22</string>
|
||||
<key>name</key>
|
||||
<string>MalinkaWallet</string>
|
||||
</dict>
|
||||
<key>symbols</key>
|
||||
<true/>
|
||||
<key>team</key>
|
||||
<dict>
|
||||
<key>id</key>
|
||||
<string>GENPCTDS3G</string>
|
||||
<key>name</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
<key>versionNumber</key>
|
||||
<string>1.0.0</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>generateAppStoreInformation</key>
|
||||
<false/>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<true/>
|
||||
<key>method</key>
|
||||
<string>app-store</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>com.juraldinio.wallet</key>
|
||||
<string>MalinkaWallet</string>
|
||||
</dict>
|
||||
<key>signingCertificate</key>
|
||||
<string>Apple Distribution</string>
|
||||
<key>signingStyle</key>
|
||||
<string>manual</string>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>teamID</key>
|
||||
<string>GENPCTDS3G</string>
|
||||
<key>uploadBitcode</key>
|
||||
<false/>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "KeyChainAccess",
|
||||
platforms: [.iOS(.v13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "KeyChainAccess",
|
||||
targets: ["KeyChainAccess"]),
|
||||
],
|
||||
dependencies: [
|
||||
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "KeyChainAccess",
|
||||
dependencies: [],
|
||||
path: "./Sources"),
|
||||
]
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Mocker",
|
||||
platforms: [
|
||||
.macOS(.v10_15),
|
||||
.iOS(.v11),
|
||||
.tvOS(.v12),
|
||||
.watchOS(.v6)],
|
||||
products: [
|
||||
.library(name: "Mocker", targets: ["Mocker"])
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "Mocker"
|
||||
),
|
||||
.testTarget(
|
||||
name: "MockerTests",
|
||||
dependencies: ["Mocker"],
|
||||
resources: [
|
||||
.process("Resources")
|
||||
]
|
||||
)
|
||||
],
|
||||
swiftLanguageVersions: [.v5])
|
||||
@@ -0,0 +1,400 @@
|
||||
<p align="center">
|
||||
<img width="900px" src="Assets/artwork.jpg">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://api.travis-ci.org/WeTransfer/Mocker.svg?branch=master"/>
|
||||
<img src="https://img.shields.io/cocoapods/v/Mocker.svg?style=flat"/>
|
||||
<img src="https://img.shields.io/cocoapods/l/Mocker.svg?style=flat"/>
|
||||
<img src="https://img.shields.io/cocoapods/p/Mocker.svg?style=flat"/>
|
||||
<img src="https://img.shields.io/badge/language-swift4.2-f48041.svg?style=flat"/>
|
||||
<img src="https://img.shields.io/badge/carthage-compatible-4BC51D.svg?style=flat"/>
|
||||
<img src="https://img.shields.io/badge/spm-compatible-4BC51D.svg?style=flat"/>
|
||||
<img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat"/>
|
||||
</p>
|
||||
|
||||
Mocker is a library written in Swift which makes it possible to mock data requests using a custom `URLProtocol`.
|
||||
|
||||
- [Features](#features)
|
||||
- [Requirements](#requirements)
|
||||
- [Usage](#usage)
|
||||
- [Activating the Mocker](#activating-the-mocker)
|
||||
- [Custom URLSessions](#custom-urlsessions)
|
||||
- [Alamofire](#alamofire)
|
||||
- [Register Mocks](#register-mocks)
|
||||
- [Create your mocked data](#create-your-mocked-data)
|
||||
- [JSON Requests](#json-requests)
|
||||
- [File extensions](#file-extensions)
|
||||
- [Custom HEAD and GET response](#custom-head-and-get-response)
|
||||
- [Delayed responses](#delayed-responses)
|
||||
- [Redirect responses](#redirect-responses)
|
||||
- [Ignoring URLs](#ignoring-urls)
|
||||
- [Mock callbacks](#mock-callbacks)
|
||||
- [Unregister Mocks](#unregister-mocks)
|
||||
- [Clear all registered mocks](#clear-all-registered-mocks)
|
||||
- [Communication](#communication)
|
||||
- [Installation](#installation)
|
||||
- [Release Notes](#release-notes)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
_Run all your data request unit tests offline_ 🎉
|
||||
|
||||
- [x] Create mocked data requests based on an URL
|
||||
- [x] Create mocked data requests based on a file extension
|
||||
- [x] Works with `URLSession` using a custom protocol class
|
||||
- [x] Supports popular frameworks like `Alamofire`
|
||||
|
||||
## Usage
|
||||
|
||||
Unit tests are written for the `Mocker` which can help you to see how it works.
|
||||
|
||||
### Activating the Mocker
|
||||
The mocker will automatically be activated for the default URL loading system like `URLSession.shared` after you've registered your first `Mock`.
|
||||
|
||||
##### Custom URLSessions
|
||||
To make it work with your custom `URLSession`, the `MockingURLProtocol` needs to be registered:
|
||||
|
||||
```swift
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
let urlSession = URLSession(configuration: configuration)
|
||||
```
|
||||
|
||||
##### Alamofire
|
||||
Quite similar like registering on a custom `URLSession`.
|
||||
|
||||
```swift
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
let sessionManager = Alamofire.Session(configuration: configuration)
|
||||
```
|
||||
|
||||
### Register Mocks
|
||||
##### Create your mocked data
|
||||
It's recommended to create a class with all your mocked data accessible. An example of this can be found in the unit tests of this project:
|
||||
|
||||
```swift
|
||||
public final class MockedData {
|
||||
public static let botAvatarImageResponseHead: Data = try! Data(contentsOf: Bundle(for: MockedData.self).url(forResource: "Resources/Responses/bot-avatar-image-head", withExtension: "data")!)
|
||||
public static let botAvatarImageFileUrl: URL = Bundle(for: MockedData.self).url(forResource: "wetransfer_bot_avater", withExtension: "png")!
|
||||
public static let exampleJSON: URL = Bundle(for: MockedData.self).url(forResource: "Resources/JSON Files/example", withExtension: "json")!
|
||||
}
|
||||
```
|
||||
|
||||
##### JSON Requests
|
||||
``` swift
|
||||
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
|
||||
|
||||
let mock = Mock(url: originalURL, contentType: .json, statusCode: 200, data: [
|
||||
.get : try! Data(contentsOf: MockedData.exampleJSON) // Data containing the JSON response
|
||||
])
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
|
||||
guard let data = data, let jsonDictionary = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
|
||||
return
|
||||
}
|
||||
|
||||
// jsonDictionary contains your JSON sample file data
|
||||
// ..
|
||||
|
||||
}.resume()
|
||||
```
|
||||
|
||||
##### Empty Responses
|
||||
``` swift
|
||||
let originalURL = URL(string: "https://www.wetransfer.com/api/foobar")!
|
||||
var request = URLRequest(url: originalURL)
|
||||
request.httpMethod = "PUT"
|
||||
|
||||
let mock = Mock(request: request, statusCode: 204)
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
|
||||
// ....
|
||||
}.resume()
|
||||
```
|
||||
|
||||
##### Ignoring the query
|
||||
Some URLs like authentication URLs contain timestamps or UUIDs in the query. To mock these you can ignore the Query for a certain URL:
|
||||
|
||||
``` swift
|
||||
/// Would transform to "https://www.example.com/api/authentication" for example.
|
||||
let originalURL = URL(string: "https://www.example.com/api/authentication?oauth_timestamp=151817037")!
|
||||
|
||||
let mock = Mock(url: originalURL, ignoreQuery: true, contentType: .json, statusCode: 200, data: [
|
||||
.get : try! Data(contentsOf: MockedData.exampleJSON) // Data containing the JSON response
|
||||
])
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: originalURL) { (data, response, error) in
|
||||
guard let data = data, let jsonDictionary = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else {
|
||||
return
|
||||
}
|
||||
|
||||
// jsonDictionary contains your JSON sample file data
|
||||
// ..
|
||||
|
||||
}.resume()
|
||||
```
|
||||
|
||||
##### File extensions
|
||||
```swift
|
||||
let imageURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
|
||||
|
||||
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
|
||||
.get: try! Data(contentsOf: MockedData.botAvatarImageFileUrl)
|
||||
]).register()
|
||||
|
||||
URLSession.shared.dataTask(with: imageURL) { (data, response, error) in
|
||||
let botAvatarImage: UIImage = UIImage(data: data!)! // This is the image from your resources.
|
||||
}.resume()
|
||||
```
|
||||
|
||||
##### Custom HEAD and GET response
|
||||
```swift
|
||||
let exampleURL = URL(string: "https://www.wetransfer.com/api/endpoint")!
|
||||
|
||||
Mock(url: exampleURL, contentType: .json, statusCode: 200, data: [
|
||||
.head: try! Data(contentsOf: MockedData.headResponse),
|
||||
.get: try! Data(contentsOf: MockedData.exampleJSON)
|
||||
]).register()
|
||||
|
||||
URLSession.shared.dataTask(with: exampleURL) { (data, response, error) in
|
||||
// data is your mocked data
|
||||
}.resume()
|
||||
```
|
||||
|
||||
##### Custom DataType
|
||||
In addition to the already build in static `DataType` implementations it is possible to create custom ones that will be used as the value to the `Content-Type` header key.
|
||||
|
||||
```swift
|
||||
let xmlURL = URL(string: "https://www.wetransfer.com/sample-xml.xml")!
|
||||
|
||||
Mock(fileExtensions: "png", contentType: .init(name: "xml", headerValue: "text/xml"), statusCode: 200, data: [
|
||||
.get: try! Data(contentsOf: MockedData.sampleXML)
|
||||
]).register()
|
||||
|
||||
URLSession.shared.dataTask(with: xmlURL) { (data, response, error) in
|
||||
let sampleXML: Data = data // This is the xml from your resources.
|
||||
}.resume(
|
||||
```
|
||||
|
||||
|
||||
##### Delayed responses
|
||||
Sometimes you want to test if the cancellation of requests is working. In that case, the mocked request should not finish immediately and you need a delay. This can be added easily:
|
||||
|
||||
```swift
|
||||
let exampleURL = URL(string: "https://www.wetransfer.com/api/endpoint")!
|
||||
|
||||
var mock = Mock(url: exampleURL, contentType: .json, statusCode: 200, data: [
|
||||
.head: try! Data(contentsOf: MockedData.headResponse),
|
||||
.get: try! Data(contentsOf: MockedData.exampleJSON)
|
||||
])
|
||||
mock.delay = DispatchTimeInterval.seconds(5)
|
||||
mock.register()
|
||||
```
|
||||
|
||||
##### Redirect responses
|
||||
Sometimes you want to mock short URLs or other redirect URLs. This is possible by saving the response and mocking the redirect location, which can be found inside the response:
|
||||
|
||||
```
|
||||
Date: Tue, 10 Oct 2017 07:28:33 GMT
|
||||
Location: https://wetransfer.com/redirect
|
||||
```
|
||||
|
||||
By creating a mock for the short URL and the redirect URL, you can mock redirect and test this behavior:
|
||||
|
||||
```swift
|
||||
let urlWhichRedirects: URL = URL(string: "https://we.tl/redirect")!
|
||||
Mock(url: urlWhichRedirects, contentType: .html, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.redirectGET)]).register()
|
||||
Mock(url: URL(string: "https://wetransfer.com/redirect")!, contentType: .json, statusCode: 200, data: [.get: try! Data(contentsOf: MockedData.exampleJSON)]).register()
|
||||
```
|
||||
|
||||
##### Ignoring URLs
|
||||
As the Mocker catches all URLs by default when registered, you might end up with a `fatalError` thrown in cases you don't need a mocked request. In that case, you can ignore the URL:
|
||||
|
||||
```swift
|
||||
let ignoredURL = URL(string: "www.wetransfer.com")!
|
||||
Mocker.ignore(ignoredURL)
|
||||
```
|
||||
|
||||
However, if you need the Mocker to catch only mocked URLs and ignore every other URL, you can set the `mode` attribute to `.optin`.
|
||||
|
||||
```swift
|
||||
Mocker.mode = .optin
|
||||
```
|
||||
|
||||
If you want to set the original mode back, you have just to set it to `.optout`.
|
||||
|
||||
```swift
|
||||
Mocker.mode = .optout
|
||||
```
|
||||
|
||||
##### Mock errors
|
||||
|
||||
You can request a `Mock` to return an error, allowing testing of error handling.
|
||||
|
||||
```swift
|
||||
Mock(url: originalURL, contentType: .json, statusCode: 500, data: [.get: Data()],
|
||||
requestError: TestExampleError.example).register()
|
||||
|
||||
URLSession.shared.dataTask(with: originalURL) { (data, urlresponse, err) in
|
||||
XCTAssertNil(data)
|
||||
XCTAssertNil(urlresponse)
|
||||
XCTAssertNotNil(err)
|
||||
if let err = err {
|
||||
// there's not a particularly elegant way to verify an instance
|
||||
// of an error, but this is a convenient workaround for testing
|
||||
// purposes
|
||||
XCTAssertEqual("example", String(describing: err))
|
||||
}
|
||||
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
```
|
||||
|
||||
##### Mock callbacks
|
||||
You can register on `Mock` callbacks to make testing easier.
|
||||
|
||||
```swift
|
||||
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
|
||||
mock.onRequestHandler = OnRequestHandler(httpBodyType: [[String:String]].self, callback: { request, postBodyArguments in
|
||||
XCTAssertEqual(request.url, mock.request.url)
|
||||
XCTAssertEqual(expectedParameters, postBodyArguments)
|
||||
onRequestExpectation.fulfill()
|
||||
})
|
||||
mock.completion = {
|
||||
endpointIsCalledExpectation.fulfill()
|
||||
}
|
||||
mock.register()
|
||||
```
|
||||
|
||||
##### Mock expectations
|
||||
Instead of setting the `completion` and `onRequest` you can also make use of expectations:
|
||||
|
||||
```swift
|
||||
var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
|
||||
let requestExpectation = expectationForRequestingMock(&mock)
|
||||
let completionExpectation = expectationForCompletingMock(&mock)
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
|
||||
|
||||
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
|
||||
```
|
||||
|
||||
### Unregister Mocks
|
||||
##### Clear all registered mocks
|
||||
You can clear all registered mocks:
|
||||
|
||||
```swift
|
||||
Mocker.removeAll()
|
||||
```
|
||||
|
||||
## Communication
|
||||
|
||||
- If you **found a bug**, open an issue.
|
||||
- If you **have a feature request**, open an issue.
|
||||
- If you **want to contribute**, submit a pull request.
|
||||
|
||||
## Installation
|
||||
|
||||
### Carthage
|
||||
|
||||
[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
|
||||
|
||||
You can install Carthage with [Homebrew](http://brew.sh/) using the following command:
|
||||
|
||||
```bash
|
||||
$ brew update
|
||||
$ brew install carthage
|
||||
```
|
||||
|
||||
To integrate Mocker into your Xcode project using Carthage, specify it in your `Cartfile`:
|
||||
|
||||
```ogdl
|
||||
github "WeTransfer/Mocker" ~> 2.3.0
|
||||
```
|
||||
|
||||
Run `carthage update` to build the framework and drag the built `Mocker.framework` into your Xcode project.
|
||||
|
||||
### Swift Package Manager
|
||||
|
||||
The [Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. It’s integrated with the Swift build system to automate the process of downloading, compiling, and linking dependencies.
|
||||
|
||||
#### Manifest File
|
||||
|
||||
Add Mocker as a package to your `Package.swift` file and then specify it as a dependency of the Target in which you wish to use it.
|
||||
|
||||
```swift
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "MyProject",
|
||||
platforms: [
|
||||
.macOS(.v10_15)
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/WeTransfer/Mocker.git", .upToNextMajor(from: "2.3.0"))
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "MyProject",
|
||||
dependencies: ["Mocker"]),
|
||||
.testTarget(
|
||||
name: "MyProjectTests",
|
||||
dependencies: ["MyProject"]),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
#### Xcode
|
||||
|
||||
To add Mocker as a [dependency](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app) to your Xcode project, select *File > Swift Packages > Add Package Dependency* and enter the repository URL.
|
||||
|
||||
#### Resolving Build Errors
|
||||
If you get the following error: *cannot find auto-link library XCTest and XCTestSwiftSupport*, set the following property under Build Options from No to Yes.
|
||||
ENABLE_TESTING_SEARCH_PATHS to YES
|
||||
|
||||
### Manually
|
||||
|
||||
If you prefer not to use any of the aforementioned dependency managers, you can integrate Mocker into your project manually.
|
||||
|
||||
#### Embedded Framework
|
||||
|
||||
- Open up Terminal, `cd` into your top-level project directory, and run the following command "if" your project is not initialized as a git repository:
|
||||
|
||||
```bash
|
||||
$ git init
|
||||
```
|
||||
|
||||
- Add Mocker as a git [submodule](http://git-scm.com/docs/git-submodule) by running the following command:
|
||||
|
||||
```bash
|
||||
$ git submodule add https://github.com/WeTransfer/Mocker.git
|
||||
```
|
||||
|
||||
- Open the new `Mocker ` folder, and drag the `Mocker.xcodeproj` into the Project Navigator of your application's Xcode project.
|
||||
|
||||
> It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter.
|
||||
|
||||
- Select the `Mocker.xcodeproj` in the Project Navigator and verify the deployment target matches that of your application target.
|
||||
- Next, select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the "Targets" heading in the sidebar.
|
||||
- In the tab bar at the top of that window, open the "General" panel.
|
||||
- Click on the `+` button under the "Embedded Binaries" section.
|
||||
- Select `Mocker.framework`.
|
||||
- And that's it!
|
||||
|
||||
> The `Mocker.framework` is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device.
|
||||
|
||||
---
|
||||
|
||||
## Release Notes
|
||||
|
||||
See [CHANGELOG.md](https://github.com/WeTransfer/Mocker/blob/master/Changelog.md) for a list of changes.
|
||||
|
||||
## License
|
||||
|
||||
Mocker is available under the MIT license. See the LICENSE file for more info.
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Mock+DataType.swift
|
||||
// Mocker
|
||||
//
|
||||
// Created by Weiß, Alexander on 26.07.22.
|
||||
// Copyright © 2022 WeTransfer. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Mock {
|
||||
/// The types of content of a request. Will be used as Content-Type header inside a `Mock`.
|
||||
public struct DataType {
|
||||
|
||||
/// Name of the data type.
|
||||
public let name: String
|
||||
|
||||
/// The header value of the data type.
|
||||
public let headerValue: String
|
||||
|
||||
public init(name: String, headerValue: String) {
|
||||
self.name = name
|
||||
self.headerValue = headerValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Mock.DataType {
|
||||
public static let json = Mock.DataType(name: "json", headerValue: "application/json; charset=utf-8")
|
||||
public static let html = Mock.DataType(name: "html", headerValue: "text/html; charset=utf-8")
|
||||
public static let imagePNG = Mock.DataType(name: "imagePNG", headerValue: "image/png")
|
||||
public static let pdf = Mock.DataType(name: "pdf", headerValue: "application/pdf")
|
||||
public static let mp4 = Mock.DataType(name: "mp4", headerValue: "video/mp4")
|
||||
public static let zip = Mock.DataType(name: "zip", headerValue: "application/zip")
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
//
|
||||
// Mock.swift
|
||||
// Rabbit
|
||||
//
|
||||
// Created by Antoine van der Lee on 04/05/2017.
|
||||
// Copyright © 2017 WeTransfer. All rights reserved.
|
||||
//
|
||||
// Mocker is only used for tests. In tests we don't even check on this SwiftLint warning, but Mocker is available through Rabbit for usage out of Rabbit. Disable for this case.
|
||||
// swiftlint:disable force_unwrapping
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
/// A Mock which can be used for mocking data requests with the `Mocker` by calling `Mocker.register(...)`.
|
||||
public struct Mock: Equatable {
|
||||
|
||||
/// HTTP method definitions.
|
||||
///
|
||||
/// See https://tools.ietf.org/html/rfc7231#section-4.3
|
||||
public enum HTTPMethod: String {
|
||||
case options = "OPTIONS"
|
||||
case get = "GET"
|
||||
case head = "HEAD"
|
||||
case post = "POST"
|
||||
case put = "PUT"
|
||||
case patch = "PATCH"
|
||||
case delete = "DELETE"
|
||||
case trace = "TRACE"
|
||||
case connect = "CONNECT"
|
||||
}
|
||||
|
||||
public typealias OnRequest = (_ request: URLRequest, _ httpBodyArguments: [String: Any]?) -> Void
|
||||
|
||||
/// The type of the data which designates the Content-Type header.
|
||||
@available(*, deprecated, message: "Calling this property is unsafe after migrating to the `contentType` initializers, and will be removed in an upcoming release. Use `contentType` instead.")
|
||||
public var dataType: DataType {
|
||||
return contentType!
|
||||
}
|
||||
|
||||
/// The type of the data which designates the Content-Type header. If set to `nil`, no Content-Type header is added to the headers.
|
||||
public let contentType: DataType?
|
||||
|
||||
/// If set, the error that URLProtocol will report as a result rather than returning data from the mock
|
||||
public let requestError: Error?
|
||||
|
||||
/// The headers to send back with the response.
|
||||
public let headers: [String: String]
|
||||
|
||||
/// The HTTP status code to return with the response.
|
||||
public let statusCode: Int
|
||||
|
||||
/// The URL value generated based on the Mock data. Force unwrapped on purpose. If you access this URL while it's not set, this is a programming error.
|
||||
public var url: URL {
|
||||
if urlToMock == nil && !data.keys.contains(.get) {
|
||||
assertionFailure("For non GET mocks you should use the `request` property so the HTTP method is set.")
|
||||
}
|
||||
return urlToMock ?? generatedURL
|
||||
}
|
||||
|
||||
/// The URL to mock as set implicitely from the init.
|
||||
private let urlToMock: URL?
|
||||
|
||||
/// The URL generated from all the data set on this mock.
|
||||
private let generatedURL: URL
|
||||
|
||||
/// The `URLRequest` to use if you did not set a specific URL.
|
||||
public let request: URLRequest
|
||||
|
||||
/// If `true`, checking the URL will ignore the query and match only for the scheme, host and path.
|
||||
public let ignoreQuery: Bool
|
||||
|
||||
/// The file extensions to match for.
|
||||
public let fileExtensions: [String]?
|
||||
|
||||
/// The data which will be returned as the response based on the HTTP Method.
|
||||
private let data: [HTTPMethod: Data]
|
||||
|
||||
/// Add a delay to a certain mock, which makes the response returned later.
|
||||
public var delay: DispatchTimeInterval?
|
||||
|
||||
/// Allow response cache.
|
||||
public var cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed
|
||||
|
||||
/// The callback which will be executed everytime this `Mock` was completed. Can be used within unit tests for validating that a request has been executed. The callback must be set before calling `register`.
|
||||
public var completion: (() -> Void)?
|
||||
|
||||
/// The callback which will be executed everytime this `Mock` was started. Can be used within unit tests for validating that a request has been started. The callback must be set before calling `register`.
|
||||
@available(*, deprecated, message: "Use `onRequestHandler` instead.")
|
||||
public var onRequest: OnRequest? {
|
||||
set {
|
||||
onRequestHandler = OnRequestHandler(legacyCallback: newValue)
|
||||
}
|
||||
get {
|
||||
onRequestHandler?.legacyCallback
|
||||
}
|
||||
}
|
||||
|
||||
/// The on request handler which will be executed everytime this `Mock` was started. Can be used within unit tests for validating that a request has been started. The handler must be set before calling `register`.
|
||||
public var onRequestHandler: OnRequestHandler?
|
||||
|
||||
/// Can only be set internally as it's used by the `expectationForRequestingMock(_:)` method.
|
||||
var onRequestExpectation: XCTestExpectation?
|
||||
|
||||
/// Can only be set internally as it's used by the `expectationForCompletingMock(_:)` method.
|
||||
var onCompletedExpectation: XCTestExpectation?
|
||||
|
||||
private init(url: URL? = nil, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], requestError: Error? = nil, additionalHeaders: [String: String] = [:], fileExtensions: [String]? = nil) {
|
||||
guard data.count > 0 else {
|
||||
preconditionFailure("At least one entry is required in the data dictionary")
|
||||
}
|
||||
|
||||
self.urlToMock = url
|
||||
let generatedURL = URL(string: "https://mocked.wetransfer.com/\(contentType?.name ?? "no-content")/\(statusCode)/\(data.keys.first!.rawValue)")!
|
||||
self.generatedURL = generatedURL
|
||||
var request = URLRequest(url: url ?? generatedURL)
|
||||
request.httpMethod = data.keys.first!.rawValue
|
||||
self.request = request
|
||||
self.ignoreQuery = ignoreQuery
|
||||
self.requestError = requestError
|
||||
self.contentType = contentType
|
||||
self.statusCode = statusCode
|
||||
self.data = data
|
||||
self.cacheStoragePolicy = cacheStoragePolicy
|
||||
|
||||
var headers = additionalHeaders
|
||||
if let contentType = contentType {
|
||||
headers["Content-Type"] = contentType.headerValue
|
||||
}
|
||||
self.headers = headers
|
||||
|
||||
self.fileExtensions = fileExtensions?.map({ $0.replacingOccurrences(of: ".", with: "") })
|
||||
}
|
||||
|
||||
/// Creates a `Mock` for the given data type. The mock will be automatically matched based on a URL created from the given parameters.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - dataType: The type of the data which designates the Content-Type header.
|
||||
/// - statusCode: The HTTP status code to return with the response.
|
||||
/// - data: The data which will be returned as the response based on the HTTP Method.
|
||||
/// - additionalHeaders: Additional headers to be added to the response.
|
||||
@available(*, deprecated, renamed: "init(contentType:statusCode:data:additionalHeaders:)")
|
||||
public init(dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
|
||||
self.init(
|
||||
url: nil,
|
||||
contentType: dataType,
|
||||
statusCode: statusCode,
|
||||
data: data,
|
||||
additionalHeaders: additionalHeaders,
|
||||
fileExtensions: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a `Mock` for the given content type. The mock will be automatically matched based on a URL created from the given parameters.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
|
||||
/// - statusCode: The HTTP status code to return with the response.
|
||||
/// - data: The data which will be returned as the response based on the HTTP Method.
|
||||
/// - additionalHeaders: Additional headers to be added to the response.
|
||||
public init(contentType: DataType?, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
|
||||
self.init(
|
||||
url: nil,
|
||||
contentType: contentType,
|
||||
statusCode: statusCode,
|
||||
data: data,
|
||||
additionalHeaders: additionalHeaders,
|
||||
fileExtensions: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a `Mock` for the given URL.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The URL to match for and to return the mocked data for.
|
||||
/// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
|
||||
/// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
|
||||
/// - dataType: The type of the data which designates the Content-Type header.
|
||||
/// - statusCode: The HTTP status code to return with the response.
|
||||
/// - data: The data which will be returned as the response based on the HTTP Method.
|
||||
/// - additionalHeaders: Additional headers to be added to the response.
|
||||
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
|
||||
@available(*, deprecated, renamed: "init(url:ignoreQuery:cacheStoragePolicy:contentType:statusCode:data:additionalHeaders:requestError:)")
|
||||
public init(url: URL, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
|
||||
self.init(
|
||||
url: url,
|
||||
ignoreQuery: ignoreQuery,
|
||||
cacheStoragePolicy: cacheStoragePolicy,
|
||||
contentType: dataType,
|
||||
statusCode: statusCode,
|
||||
data: data,
|
||||
requestError: requestError,
|
||||
additionalHeaders: additionalHeaders,
|
||||
fileExtensions: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a `Mock` for the given URL.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The URL to match for and to return the mocked data for.
|
||||
/// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
|
||||
/// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
|
||||
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
|
||||
/// - statusCode: The HTTP status code to return with the response.
|
||||
/// - data: The data which will be returned as the response based on the HTTP Method.
|
||||
/// - additionalHeaders: Additional headers to be added to the response.
|
||||
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
|
||||
public init(url: URL, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
|
||||
self.init(
|
||||
url: url,
|
||||
ignoreQuery: ignoreQuery,
|
||||
cacheStoragePolicy: cacheStoragePolicy,
|
||||
contentType: contentType,
|
||||
statusCode: statusCode,
|
||||
data: data,
|
||||
requestError: requestError,
|
||||
additionalHeaders: additionalHeaders,
|
||||
fileExtensions: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a `Mock` for the given file extensions. The mock will only be used for urls matching the extension.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - fileExtensions: The file extension to match for.
|
||||
/// - dataType: The type of the data which designates the Content-Type header.
|
||||
/// - statusCode: The HTTP status code to return with the response.
|
||||
/// - data: The data which will be returned as the response based on the HTTP Method.
|
||||
/// - additionalHeaders: Additional headers to be added to the response.
|
||||
@available(*, deprecated, renamed: "init(fileExtensions:contentType:statusCode:data:additionalHeaders:)")
|
||||
public init(fileExtensions: String..., dataType: DataType, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
|
||||
self.init(
|
||||
url: nil,
|
||||
contentType: dataType,
|
||||
statusCode: statusCode,
|
||||
data: data,
|
||||
additionalHeaders: additionalHeaders,
|
||||
fileExtensions: fileExtensions
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a `Mock` for the given file extensions. The mock will only be used for urls matching the extension.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - fileExtensions: The file extension to match for.
|
||||
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
|
||||
/// - statusCode: The HTTP status code to return with the response.
|
||||
/// - data: The data which will be returned as the response based on the HTTP Method.
|
||||
/// - additionalHeaders: Additional headers to be added to the response.
|
||||
public init(fileExtensions: String..., contentType: DataType? = nil, statusCode: Int, data: [HTTPMethod: Data], additionalHeaders: [String: String] = [:]) {
|
||||
self.init(
|
||||
url: nil,
|
||||
contentType: contentType,
|
||||
statusCode: statusCode,
|
||||
data: data,
|
||||
additionalHeaders: additionalHeaders,
|
||||
fileExtensions: fileExtensions
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a `Mock` for the given `URLRequest`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - request: The URLRequest, from which the URL and request method is used to match for and to return the mocked data for.
|
||||
/// - ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
|
||||
/// - cacheStoragePolicy: The caching strategy. Defaults to `notAllowed`.
|
||||
/// - contentType: The type of the data which designates the Content-Type header. Defaults to `nil`, which means that no Content-Type header is added to the headers.
|
||||
/// - statusCode: The HTTP status code to return with the response.
|
||||
/// - data: The data which will be returned as the response. Defaults to an empty `Data` instance.
|
||||
/// - additionalHeaders: Additional headers to be added to the response.
|
||||
/// - requestError: If provided, the URLSession will report the passed error rather than returning data. Defaults to `nil`.
|
||||
public init(request: URLRequest, ignoreQuery: Bool = false, cacheStoragePolicy: URLCache.StoragePolicy = .notAllowed, contentType: DataType? = nil, statusCode: Int, data: Data = Data(), additionalHeaders: [String: String] = [:], requestError: Error? = nil) {
|
||||
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else {
|
||||
preconditionFailure("Unexpected http method")
|
||||
}
|
||||
|
||||
self.init(
|
||||
url: request.url,
|
||||
ignoreQuery: ignoreQuery,
|
||||
cacheStoragePolicy: cacheStoragePolicy,
|
||||
contentType: contentType,
|
||||
statusCode: statusCode,
|
||||
data: [requestHTTPMethod: data],
|
||||
requestError: requestError,
|
||||
additionalHeaders: additionalHeaders,
|
||||
fileExtensions: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Registers the mock with the shared `Mocker`.
|
||||
public func register() {
|
||||
Mocker.register(self)
|
||||
}
|
||||
|
||||
/// Returns `Data` based on the HTTP Method of the passed request.
|
||||
///
|
||||
/// - Parameter request: The request to match data for.
|
||||
/// - Returns: The `Data` which matches the request. Will be `nil` if no data is registered for the request `HTTPMethod`.
|
||||
func data(for request: URLRequest) -> Data? {
|
||||
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else { return nil }
|
||||
return data[requestHTTPMethod]
|
||||
}
|
||||
|
||||
/// Used to compare the Mock data with the given `URLRequest`.
|
||||
static func == (mock: Mock, request: URLRequest) -> Bool {
|
||||
guard let requestHTTPMethod = Mock.HTTPMethod(rawValue: request.httpMethod ?? "") else { return false }
|
||||
|
||||
if let fileExtensions = mock.fileExtensions {
|
||||
// If the mock contains a file extension, this should always be used to match for.
|
||||
guard let pathExtension = request.url?.pathExtension else { return false }
|
||||
return fileExtensions.contains(pathExtension)
|
||||
} else if mock.ignoreQuery {
|
||||
return mock.request.url!.baseString == request.url?.baseString && mock.data.keys.contains(requestHTTPMethod)
|
||||
}
|
||||
|
||||
return mock.request.url!.absoluteString == request.url?.absoluteString && mock.data.keys.contains(requestHTTPMethod)
|
||||
}
|
||||
|
||||
public static func == (lhs: Mock, rhs: Mock) -> Bool {
|
||||
let lhsHTTPMethods: [String] = lhs.data.keys.compactMap { $0.rawValue }
|
||||
let rhsHTTPMethods: [String] = rhs.data.keys.compactMap { $0.rawValue }
|
||||
|
||||
if let lhsFileExtensions = lhs.fileExtensions, let rhsFileExtensions = rhs.fileExtensions, (!lhsFileExtensions.isEmpty || !rhsFileExtensions.isEmpty) {
|
||||
/// The mocks are targeting file extensions specifically, check on those.
|
||||
return lhsFileExtensions == rhsFileExtensions && lhsHTTPMethods == rhsHTTPMethods
|
||||
}
|
||||
|
||||
return lhs.request.url!.absoluteString == rhs.request.url!.absoluteString && lhsHTTPMethods == rhsHTTPMethods
|
||||
}
|
||||
}
|
||||
|
||||
extension URL {
|
||||
/// Returns the base URL string build with the scheme, host and path. "https://www.wetransfer.com/v1/test?param=test" would be "https://www.wetransfer.com/v1/test".
|
||||
var baseString: String? {
|
||||
guard let scheme = scheme, let host = host else { return nil }
|
||||
return scheme + "://" + host + path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// Mocker.swift
|
||||
// Rabbit
|
||||
//
|
||||
// Created by Antoine van der Lee on 04/05/2017.
|
||||
// Copyright © 2017 WeTransfer. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
/// Can be used for registering Mocked data, returned by the `MockingURLProtocol`.
|
||||
public struct Mocker {
|
||||
private struct IgnoredRule: Equatable {
|
||||
let urlToIgnore: URL
|
||||
let ignoreQuery: Bool
|
||||
|
||||
/// Checks if the passed URL should be ignored.
|
||||
///
|
||||
/// - Parameter url: The URL to check for.
|
||||
/// - Returns: `true` if it should be ignored, `false` if the URL doesn't correspond to ignored rules.
|
||||
func shouldIgnore(_ url: URL) -> Bool {
|
||||
if ignoreQuery {
|
||||
return urlToIgnore.baseString == url.baseString
|
||||
}
|
||||
|
||||
return urlToIgnore.absoluteString == url.absoluteString
|
||||
}
|
||||
}
|
||||
|
||||
public enum HTTPVersion: String {
|
||||
case http1_0 = "HTTP/1.0"
|
||||
case http1_1 = "HTTP/1.1"
|
||||
case http2_0 = "HTTP/2.0"
|
||||
}
|
||||
|
||||
/// The way Mocker handles unregistered urls
|
||||
public enum Mode {
|
||||
/// The default mode: only URLs registered with the `ignore(_ url: URL)` method are ignored for mocking.
|
||||
///
|
||||
/// - Registered mocked URL: Mocked.
|
||||
/// - Registered ignored URL: Ignored by Mocker, default process is applied as if the Mocker doesn't exist.
|
||||
/// - Any other URL: Raises an error.
|
||||
case optout
|
||||
|
||||
/// Only registered mocked URLs are mocked, all others pass through.
|
||||
///
|
||||
/// - Registered mocked URL: Mocked.
|
||||
/// - Any other URL: Ignored by Mocker, default process is applied as if the Mocker doesn't exist.
|
||||
case optin
|
||||
}
|
||||
|
||||
/// The mode defines how unknown URLs are handled. Defaults to `optout` which means requests without a mock will fail.
|
||||
public static var mode: Mode = .optout
|
||||
|
||||
/// The shared instance of the Mocker, can be used to register and return mocks.
|
||||
internal static var shared = Mocker()
|
||||
|
||||
/// The HTTP Version to use in the mocked response.
|
||||
public static var httpVersion: HTTPVersion = HTTPVersion.http1_1
|
||||
|
||||
/// The registrated mocks.
|
||||
private(set) var mocks: [Mock] = []
|
||||
|
||||
/// URLs to ignore for mocking.
|
||||
public var ignoredURLs: [URL] {
|
||||
ignoredRules.map { $0.urlToIgnore }
|
||||
}
|
||||
|
||||
private var ignoredRules: [IgnoredRule] = []
|
||||
|
||||
/// For Thread Safety access.
|
||||
private let queue = DispatchQueue(label: "mocker.mocks.access.queue", attributes: .concurrent)
|
||||
|
||||
private init() {
|
||||
// Whenever someone is requesting the Mocker, we want the URL protocol to be activated.
|
||||
_ = URLProtocol.registerClass(MockingURLProtocol.self)
|
||||
}
|
||||
|
||||
/// Register new Mocked data. If a mock for the same URL and HTTPMethod exists, it will be overwritten.
|
||||
///
|
||||
/// - Parameter mock: The Mock to be registered for future requests.
|
||||
public static func register(_ mock: Mock) {
|
||||
shared.queue.async(flags: .barrier) {
|
||||
/// Delete the Mock if it was already registered.
|
||||
shared.mocks.removeAll(where: { $0 == mock })
|
||||
shared.mocks.append(mock)
|
||||
}
|
||||
}
|
||||
|
||||
/// Register an URL to ignore for mocking. This will let the URL work as if the Mocker doesn't exist.
|
||||
///
|
||||
/// - Parameter url: The URL to mock.
|
||||
/// - Parameter ignoreQuery: If `true`, checking the URL will ignore the query and match only for the scheme, host and path. Defaults to `false`.
|
||||
public static func ignore(_ url: URL, ignoreQuery: Bool = false) {
|
||||
shared.queue.async(flags: .barrier) {
|
||||
let rule = IgnoredRule(urlToIgnore: url, ignoreQuery: ignoreQuery)
|
||||
shared.ignoredRules.append(rule)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the passed URL should be handled by the Mocker. If the URL is registered to be ignored, it will not handle the URL.
|
||||
///
|
||||
/// - Parameter url: The URL to check for.
|
||||
/// - Returns: `true` if it should be mocked, `false` if the URL is registered as ignored.
|
||||
public static func shouldHandle(_ request: URLRequest) -> Bool {
|
||||
switch mode {
|
||||
case .optout:
|
||||
guard let url = request.url else { return false }
|
||||
return shared.queue.sync {
|
||||
!shared.ignoredRules.contains(where: { $0.shouldIgnore(url) })
|
||||
}
|
||||
case .optin:
|
||||
return mock(for: request) != nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes all registered mocks. Use this method in your tearDown function to make sure a Mock is not used in any other test.
|
||||
public static func removeAll() {
|
||||
shared.queue.sync(flags: .barrier) {
|
||||
shared.mocks.removeAll()
|
||||
shared.ignoredRules.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve a Mock for the given request. Matches on `request.url` and `request.httpMethod`.
|
||||
///
|
||||
/// - Parameter request: The request to search for a mock.
|
||||
/// - Returns: A mock if found, `nil` if there's no mocked data registered for the given request.
|
||||
static func mock(for request: URLRequest) -> Mock? {
|
||||
shared.queue.sync {
|
||||
/// First check for specific URLs
|
||||
if let specificMock = shared.mocks.first(where: { $0 == request && $0.fileExtensions == nil }) {
|
||||
return specificMock
|
||||
}
|
||||
/// Second, check for generic file extension Mocks
|
||||
return shared.mocks.first(where: { $0 == request })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// MockingURLProtocol.swift
|
||||
// Rabbit
|
||||
//
|
||||
// Created by Antoine van der Lee on 04/05/2017.
|
||||
// Copyright © 2017 WeTransfer. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
/// The protocol which can be used to send Mocked data back. Use the `Mocker` to register `Mock` data
|
||||
open class MockingURLProtocol: URLProtocol {
|
||||
|
||||
enum Error: Swift.Error, LocalizedError, CustomDebugStringConvertible {
|
||||
case missingMockedData(url: String)
|
||||
case explicitMockFailure(url: String)
|
||||
|
||||
var errorDescription: String? {
|
||||
return debugDescription
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
switch self {
|
||||
case .missingMockedData(let url):
|
||||
return "Missing mock for URL: \(url)"
|
||||
case .explicitMockFailure(url: let url):
|
||||
return "Induced error for URL: \(url)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var responseWorkItem: DispatchWorkItem?
|
||||
|
||||
/// Returns Mocked data based on the mocks register in the `Mocker`. Will end up in an error when no Mock data is found for the request.
|
||||
override public func startLoading() {
|
||||
guard
|
||||
let mock = Mocker.mock(for: request),
|
||||
let response = HTTPURLResponse(url: mock.request.url!, statusCode: mock.statusCode, httpVersion: Mocker.httpVersion.rawValue, headerFields: mock.headers),
|
||||
let data = mock.data(for: request)
|
||||
else {
|
||||
print("\n\n 🚨 No mocked data found for url \(String(describing: request.url?.absoluteString)) method \(String(describing: request.httpMethod)). Did you forget to use `register()`? 🚨 \n\n")
|
||||
client?.urlProtocol(self, didFailWithError: Error.missingMockedData(url: String(describing: request.url?.absoluteString)))
|
||||
return
|
||||
}
|
||||
|
||||
if let onRequestHandler = mock.onRequestHandler {
|
||||
onRequestHandler.handleRequest(request)
|
||||
}
|
||||
mock.onRequestExpectation?.fulfill()
|
||||
|
||||
guard let delay = mock.delay else {
|
||||
finishRequest(for: mock, data: data, response: response)
|
||||
return
|
||||
}
|
||||
|
||||
self.responseWorkItem = DispatchWorkItem(block: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.finishRequest(for: mock, data: data, response: response)
|
||||
})
|
||||
|
||||
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).asyncAfter(deadline: .now() + delay, execute: responseWorkItem!)
|
||||
}
|
||||
|
||||
private func finishRequest(for mock: Mock, data: Data, response: HTTPURLResponse) {
|
||||
if let redirectLocation = data.redirectLocation {
|
||||
self.client?.urlProtocol(self, wasRedirectedTo: URLRequest(url: redirectLocation), redirectResponse: response)
|
||||
} else if let requestError = mock.requestError {
|
||||
self.client?.urlProtocol(self, didFailWithError: requestError)
|
||||
} else {
|
||||
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: mock.cacheStoragePolicy)
|
||||
self.client?.urlProtocol(self, didLoad: data)
|
||||
self.client?.urlProtocolDidFinishLoading(self)
|
||||
}
|
||||
|
||||
mock.completion?()
|
||||
mock.onCompletedExpectation?.fulfill()
|
||||
}
|
||||
|
||||
/// Implementation does nothing, but is needed for a valid inheritance of URLProtocol.
|
||||
override public func stopLoading() {
|
||||
responseWorkItem?.cancel()
|
||||
}
|
||||
|
||||
/// Simply sends back the passed request. Implementation is needed for a valid inheritance of URLProtocol.
|
||||
override public class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
||||
return request
|
||||
}
|
||||
|
||||
/// Overrides needed to define a valid inheritance of URLProtocol.
|
||||
override public class func canInit(with request: URLRequest) -> Bool {
|
||||
return Mocker.shouldHandle(request)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Data {
|
||||
/// Returns the redirect location from the raw HTTP response if exists.
|
||||
var redirectLocation: URL? {
|
||||
let locationComponent = String(data: self, encoding: String.Encoding.utf8)?.components(separatedBy: "\n").first(where: { (value) -> Bool in
|
||||
return value.contains("Location:")
|
||||
})
|
||||
|
||||
guard let redirectLocationString = locationComponent?.components(separatedBy: "Location:").last, let redirectLocation = URL(string: redirectLocationString.trimmingCharacters(in: NSCharacterSet.whitespaces)) else {
|
||||
return nil
|
||||
}
|
||||
return redirectLocation
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// OnRequestHandler.swift
|
||||
//
|
||||
//
|
||||
// Created by Antoine van der Lee on 03/11/2022.
|
||||
// Copyright © 2022 WeTransfer. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
|
||||
/// A handler for verifying outgoing requests.
|
||||
public struct OnRequestHandler {
|
||||
|
||||
public typealias OnRequest<HTTPBody> = (_ request: URLRequest, _ httpBody: HTTPBody?) -> Void
|
||||
|
||||
private let internalCallback: (_ request: URLRequest) -> Void
|
||||
let legacyCallback: Mock.OnRequest?
|
||||
|
||||
/// Creates a new request handler using the given `HTTPBody` type, which can be any `Decodable`.
|
||||
/// - Parameters:
|
||||
/// - httpBodyType: The decodable type to use for parsing the request body.
|
||||
/// - callback: The callback which will be called just before the request executes.
|
||||
public init<HTTPBody: Decodable>(httpBodyType: HTTPBody.Type?, callback: @escaping OnRequest<HTTPBody>) {
|
||||
self.internalCallback = { request in
|
||||
guard
|
||||
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
|
||||
let decodedObject = try? JSONDecoder().decode(HTTPBody.self, from: httpBody)
|
||||
else {
|
||||
callback(request, nil)
|
||||
return
|
||||
}
|
||||
callback(request, decodedObject)
|
||||
}
|
||||
legacyCallback = nil
|
||||
}
|
||||
|
||||
/// Creates a new request handler using the given callback to call on request without parsing the body arguments.
|
||||
/// - Parameter requestCallback: The callback which will be executed just before the request executes, containing the request.
|
||||
public init(requestCallback: @escaping (_ request: URLRequest) -> Void) {
|
||||
self.internalCallback = requestCallback
|
||||
legacyCallback = nil
|
||||
}
|
||||
|
||||
/// Creates a new request handler using the given callback to call on request without parsing the body arguments and without passing the request.
|
||||
/// - Parameter callback: The callback which will be executed just before the request executes.
|
||||
public init(callback: @escaping () -> Void) {
|
||||
self.internalCallback = { _ in
|
||||
callback()
|
||||
}
|
||||
legacyCallback = nil
|
||||
}
|
||||
|
||||
/// Creates a new request handler using the given callback to call on request.
|
||||
/// - Parameter jsonDictionaryCallback: The callback that executes just before the request executes, containing the HTTP Body Arguments as a JSON Object Dictionary.
|
||||
public init(jsonDictionaryCallback: @escaping ((_ request: URLRequest, _ httpBodyArguments: [String: Any]?) -> Void)) {
|
||||
self.internalCallback = { request in
|
||||
guard
|
||||
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
|
||||
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [String: Any]
|
||||
else {
|
||||
jsonDictionaryCallback(request, nil)
|
||||
return
|
||||
}
|
||||
jsonDictionaryCallback(request, jsonObject)
|
||||
}
|
||||
self.legacyCallback = nil
|
||||
}
|
||||
|
||||
/// Creates a new request handler using the given callback to call on request.
|
||||
/// - Parameter jsonDictionaryCallback: The callback that executes just before the request executes, containing the HTTP Body Arguments as a JSON Object Array.
|
||||
public init(jsonArrayCallback: @escaping ((_ request: URLRequest, _ httpBodyArguments: [[String: Any]]?) -> Void)) {
|
||||
self.internalCallback = { request in
|
||||
guard
|
||||
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
|
||||
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [[String: Any]]
|
||||
else {
|
||||
jsonArrayCallback(request, nil)
|
||||
return
|
||||
}
|
||||
jsonArrayCallback(request, jsonObject)
|
||||
}
|
||||
self.legacyCallback = nil
|
||||
}
|
||||
|
||||
init(legacyCallback: Mock.OnRequest?) {
|
||||
self.internalCallback = { request in
|
||||
guard
|
||||
let httpBody = request.httpBodyStreamData() ?? request.httpBody,
|
||||
let jsonObject = try? JSONSerialization.jsonObject(with: httpBody, options: .fragmentsAllowed) as? [String: Any]
|
||||
else {
|
||||
legacyCallback?(request, nil)
|
||||
return
|
||||
}
|
||||
legacyCallback?(request, jsonObject)
|
||||
}
|
||||
self.legacyCallback = legacyCallback
|
||||
}
|
||||
|
||||
func handleRequest(_ request: URLRequest) {
|
||||
internalCallback(request)
|
||||
}
|
||||
}
|
||||
|
||||
private extension URLRequest {
|
||||
/// We need to use the http body stream data as the URLRequest once launched converts the `httpBody` to this stream of data.
|
||||
func httpBodyStreamData() -> Data? {
|
||||
guard let bodyStream = self.httpBodyStream else { return nil }
|
||||
|
||||
bodyStream.open()
|
||||
|
||||
// Will read 16 chars per iteration. Can use bigger buffer if needed
|
||||
let bufferSize: Int = 16
|
||||
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
||||
var data = Data()
|
||||
|
||||
while bodyStream.hasBytesAvailable {
|
||||
let readData = bodyStream.read(buffer, maxLength: bufferSize)
|
||||
data.append(buffer, count: readData)
|
||||
}
|
||||
|
||||
buffer.deallocate()
|
||||
bodyStream.close()
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// XCTest+Mocker.swift
|
||||
// Mocker
|
||||
//
|
||||
// Created by Antoine van der Lee on 27/05/2020.
|
||||
// Copyright © 2020 WeTransfer. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
public extension XCTestCase {
|
||||
func expectationForRequestingMock(_ mock: inout Mock) -> XCTestExpectation {
|
||||
let mockExpectation = expectation(description: "\(mock) should be requested")
|
||||
mock.onRequestExpectation = mockExpectation
|
||||
return mockExpectation
|
||||
}
|
||||
|
||||
func expectationForCompletingMock(_ mock: inout Mock) -> XCTestExpectation {
|
||||
let mockExpectation = expectation(description: "\(mock) should be finishing")
|
||||
mock.onCompletedExpectation = mockExpectation
|
||||
return mockExpectation
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// MockTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Antoine van der Lee on 21/04/2021.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
@testable import Mocker
|
||||
|
||||
final class MockTests: XCTestCase {
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
Mocker.mode = .optout
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
Mocker.removeAll()
|
||||
Mocker.mode = .optout
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
/// It should match two file extension mocks correctly.
|
||||
func testFileExtensionMocksComparing() {
|
||||
let mock200 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
|
||||
let secondMock200 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
|
||||
let mock400 = Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 400, data: [.put: Data()])
|
||||
let mockJPEG = Mock(fileExtensions: "jpeg", contentType: .imagePNG, statusCode: 200, data: [.put: Data()])
|
||||
|
||||
XCTAssertEqual(mock200, secondMock200)
|
||||
XCTAssertEqual(mock200, mock400)
|
||||
XCTAssertNotEqual(mock200, mockJPEG)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// MockedData.swift
|
||||
// Mocker
|
||||
//
|
||||
// Created by Antoine van der Lee on 11/08/2017.
|
||||
// Copyright © 2017 WeTransfer. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Contains all available Mocked data.
|
||||
public final class MockedData {
|
||||
public static let botAvatarImageFileUrl: URL = Bundle.module.url(forResource: "wetransfer_bot_avatar", withExtension: "png")!
|
||||
public static let exampleJSON: URL = Bundle.module.url(forResource: "example", withExtension: "json")!
|
||||
public static let redirectGET: URL = Bundle.module.url(forResource: "sample-redirect-get", withExtension: "data")!
|
||||
}
|
||||
|
||||
extension Bundle {
|
||||
#if !SWIFT_PACKAGE
|
||||
static let module = Bundle(for: MockedData.self)
|
||||
#endif
|
||||
}
|
||||
|
||||
internal extension URL {
|
||||
/// Returns a `Data` representation of the current `URL`. Force unwrapping as it's only used for tests.
|
||||
var data: Data {
|
||||
return try! Data(contentsOf: self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
//
|
||||
// MockerTests.swift
|
||||
// MockerTests
|
||||
//
|
||||
// Created by Antoine van der Lee on 11/08/2017.
|
||||
// Copyright © 2017 WeTransfer. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
@testable import Mocker
|
||||
|
||||
final class MockerTests: XCTestCase {
|
||||
struct Framework {
|
||||
let name: String?
|
||||
let owner: String?
|
||||
|
||||
init(jsonDictionary: [String: Any]) {
|
||||
name = jsonDictionary["name"] as? String
|
||||
owner = jsonDictionary["owner"] as? String
|
||||
}
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
Mocker.mode = .optout
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
Mocker.removeAll()
|
||||
Mocker.mode = .optout
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
/// It should returned the register mocked image data as response.
|
||||
func testImageURLDataRequest() {
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let originalURL = URL(string: "https://avatars3.githubusercontent.com/u/26250426?v=4&s=400")!
|
||||
|
||||
let mockedData = MockedData.botAvatarImageFileUrl.data
|
||||
let mock = Mock(url: originalURL, contentType: .imagePNG, statusCode: 200, data: [
|
||||
.get: mockedData
|
||||
])
|
||||
|
||||
mock.register()
|
||||
URLSession.shared.dataTask(with: originalURL) { (data, _, error) in
|
||||
XCTAssertNil(error)
|
||||
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should returned the register mocked image data as response for register file types.
|
||||
func testImageExtensionDataRequest() {
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")
|
||||
|
||||
let mockedData = MockedData.botAvatarImageFileUrl.data
|
||||
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
|
||||
.get: mockedData
|
||||
]).register()
|
||||
|
||||
URLSession.shared.dataTask(with: originalURL!) { (data, _, error) in
|
||||
XCTAssertNil(error)
|
||||
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should ignore file extension mocks if a specific URL is mocked.
|
||||
func testSpecificURLOverGenericMocks() {
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
|
||||
|
||||
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 400, data: [
|
||||
.get: Data()
|
||||
]).register()
|
||||
|
||||
let mockedData = MockedData.botAvatarImageFileUrl.data
|
||||
Mock(url: originalURL, ignoreQuery: true, contentType: .imagePNG, statusCode: 200, data: [
|
||||
.get: mockedData
|
||||
]).register()
|
||||
|
||||
URLSession.shared.dataTask(with: originalURL) { (data, _, error) in
|
||||
XCTAssertNil(error)
|
||||
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should correctly ignore queries if set.
|
||||
func testIgnoreQueryMocking() {
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png?width=200&height=200")!
|
||||
|
||||
let mockedData = MockedData.botAvatarImageFileUrl.data
|
||||
Mock(url: originalURL, ignoreQuery: true, contentType: .imagePNG, statusCode: 200, data: [
|
||||
.get: mockedData
|
||||
]).register()
|
||||
|
||||
/// Make it different compared to the mocked URL.
|
||||
let customURL = URL(string: originalURL.absoluteString + "&" + UUID().uuidString)!
|
||||
|
||||
URLSession.shared.dataTask(with: customURL) { (data, _, error) in
|
||||
XCTAssertNil(error)
|
||||
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should return the mocked JSON.
|
||||
func testJSONRequest() {
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
|
||||
|
||||
Mock(url: originalURL, contentType: .json, statusCode: 200, data: [
|
||||
.get: MockedData.exampleJSON.data
|
||||
]).register()
|
||||
|
||||
URLSession.shared.dataTask(with: originalURL) { (data, _, _) in
|
||||
|
||||
guard let data = data else {
|
||||
XCTFail("Data is nil")
|
||||
return
|
||||
}
|
||||
|
||||
guard let jsonDictionary = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
|
||||
XCTFail("Wrong data response \(String(describing: data))")
|
||||
expectation.fulfill()
|
||||
return
|
||||
}
|
||||
|
||||
let framework = Framework(jsonDictionary: jsonDictionary)
|
||||
XCTAssertEqual(framework.name, "Mocker")
|
||||
XCTAssertEqual(framework.owner, "WeTransfer")
|
||||
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// No Content-Type should be included in the headers
|
||||
func testNoContentType() {
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let originalURL = URL(string: "https://www.wetransfer.com/api/foobar")!
|
||||
var request = URLRequest(url: originalURL)
|
||||
request.httpMethod = "PUT"
|
||||
|
||||
Mock(request: request, statusCode: 202).register()
|
||||
|
||||
URLSession.shared.dataTask(with: request) { (data, response, _) in
|
||||
guard let response = response as? HTTPURLResponse else {
|
||||
XCTFail("Unexpected response")
|
||||
return
|
||||
}
|
||||
|
||||
// data is only nil if there is an error
|
||||
XCTAssertEqual(data, Data())
|
||||
XCTAssertNil(response.allHeaderFields["Content-Type"])
|
||||
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should return the additional headers.
|
||||
func testAdditionalHeaders() {
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let headers = ["Testkey": "testvalue"]
|
||||
let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: headers)
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: mock.request) { (_, response, error) in
|
||||
XCTAssertNil(error)
|
||||
XCTAssertEqual(((response as? HTTPURLResponse)?.allHeaderFields["Testkey"] as? String), "testvalue", "Additional headers should be added.")
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should override existing mocks.
|
||||
func testMockOverriding() {
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: ["testkey": "testvalue"])
|
||||
mock.register()
|
||||
|
||||
let newMock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()], additionalHeaders: ["Newkey": "newvalue"])
|
||||
newMock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: mock.request) { (_, response, error) in
|
||||
XCTAssertNil(error)
|
||||
XCTAssertEqual(((response as? HTTPURLResponse)?.allHeaderFields["Newkey"] as? String), "newvalue", "Additional headers should be added.")
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should work with a custom URLSession.
|
||||
func testCustomURLSession() {
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let originalURL = URL(string: "https://www.wetransfer.com/sample-image.png")
|
||||
|
||||
let mockedData = MockedData.botAvatarImageFileUrl.data
|
||||
Mock(fileExtensions: "png", contentType: .imagePNG, statusCode: 200, data: [
|
||||
.get: mockedData
|
||||
]).register()
|
||||
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
let urlSession = URLSession(configuration: configuration)
|
||||
|
||||
urlSession.dataTask(with: originalURL!) { (data, _, error) in
|
||||
XCTAssertNil(error)
|
||||
XCTAssertEqual(data, mockedData, "Image should be returned mocked")
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should be possible to test cancellation of requests with a delayed mock.
|
||||
func testDelayedMockCancelation() {
|
||||
let expectation = self.expectation(description: "Data request should be cancelled")
|
||||
var mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
|
||||
mock.delay = DispatchTimeInterval.seconds(5)
|
||||
mock.register()
|
||||
|
||||
let task = URLSession.shared.dataTask(with: mock.request) { (_, _, error) in
|
||||
XCTAssertEqual(error?._code, NSURLErrorCancelled)
|
||||
expectation.fulfill()
|
||||
}
|
||||
|
||||
task.resume()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
|
||||
task.cancel()
|
||||
})
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should correctly handle redirect responses.
|
||||
func testRedirectResponse() throws {
|
||||
#if os(Linux)
|
||||
throw XCTSkip("The URLSession swift-corelibs-foundation implementation doesn't currently handle redirects directly")
|
||||
#endif
|
||||
let expectation = self.expectation(description: "Data request should be cancelled")
|
||||
let urlWhichRedirects: URL = URL(string: "https://we.tl/redirect")!
|
||||
Mock(url: urlWhichRedirects, contentType: .html, statusCode: 200, data: [.get: MockedData.redirectGET.data]).register()
|
||||
Mock(url: URL(string: "https://wetransfer.com/redirect")!, contentType: .json, statusCode: 200, data: [.get: MockedData.exampleJSON.data]).register()
|
||||
|
||||
URLSession.shared.dataTask(with: urlWhichRedirects) { (data, _, _) in
|
||||
|
||||
guard let data = data else {
|
||||
XCTFail("Data is nil")
|
||||
return
|
||||
}
|
||||
|
||||
guard let jsonDictionary = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
|
||||
XCTFail("Wrong data response \(String(describing: data))")
|
||||
expectation.fulfill()
|
||||
return
|
||||
}
|
||||
|
||||
let framework = Framework(jsonDictionary: jsonDictionary)
|
||||
XCTAssertEqual(framework.name, "Mocker")
|
||||
XCTAssertEqual(framework.owner, "WeTransfer")
|
||||
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should be possible to ignore URLs and not let them be handled.
|
||||
func testIgnoreURLs() {
|
||||
|
||||
let ignoredURL = URL(string: "www.wetransfer.com")!
|
||||
|
||||
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
|
||||
Mocker.ignore(ignoredURL)
|
||||
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
|
||||
}
|
||||
|
||||
/// It should be possible to ignore URLs and not let them be handled.
|
||||
func testIgnoreURLsIgnoreQueries() {
|
||||
|
||||
let ignoredURL = URL(string: "https://www.wetransfer.com/sample-image.png")!
|
||||
let ignoredURLQueries = URL(string: "https://www.wetransfer.com/sample-image.png?width=200&height=200")!
|
||||
|
||||
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURLQueries)))
|
||||
Mocker.ignore(ignoredURL, ignoreQuery: true)
|
||||
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURLQueries)))
|
||||
}
|
||||
|
||||
/// It should be possible to compose a url relative to a base and still have it match the full url
|
||||
func testComposedURLMatch() {
|
||||
let composedURL = URL(fileURLWithPath: "resource", relativeTo: URL(string: "https://host.com/api/"))
|
||||
let simpleURL = URL(string: "https://host.com/api/resource")
|
||||
let mock = Mock(url: composedURL, contentType: .json, statusCode: 200, data: [.get: MockedData.exampleJSON.data])
|
||||
let urlRequest = URLRequest(url: simpleURL!)
|
||||
XCTAssertEqual(composedURL.absoluteString, simpleURL?.absoluteString)
|
||||
XCTAssert(mock == urlRequest)
|
||||
}
|
||||
|
||||
/// It should call the onRequest and completion callbacks when a `Mock` is used and completed in the right order.
|
||||
func testMockCallbacks() {
|
||||
let onRequestExpectation = expectation(description: "Data request should start")
|
||||
let completionExpectation = expectation(description: "Data request should succeed")
|
||||
var mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
|
||||
mock.onRequest = { _, _ in
|
||||
onRequestExpectation.fulfill()
|
||||
}
|
||||
mock.completion = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: mock.request).resume()
|
||||
|
||||
wait(for: [onRequestExpectation, completionExpectation], timeout: 2.0, enforceOrder: true)
|
||||
}
|
||||
|
||||
/// It should report post body arguments if they exist.
|
||||
func testOnRequestLegacyPostBodyParameters() throws {
|
||||
let onRequestExpectation = expectation(description: "Data request should start")
|
||||
|
||||
let expectedParameters = ["test": "value"]
|
||||
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
|
||||
request.httpMethod = Mock.HTTPMethod.post.rawValue
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
|
||||
|
||||
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
|
||||
mock.onRequest = { request, postBodyArguments in
|
||||
XCTAssertEqual(request.url, mock.request.url)
|
||||
XCTAssertEqual(expectedParameters, postBodyArguments as? [String: String])
|
||||
onRequestExpectation.fulfill()
|
||||
}
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: request).resume()
|
||||
|
||||
wait(for: [onRequestExpectation], timeout: 2.0)
|
||||
}
|
||||
|
||||
func testOnRequestDecodablePostBodyParameters() throws {
|
||||
struct RequestParameters: Codable, Equatable {
|
||||
let name: String
|
||||
}
|
||||
|
||||
let onRequestExpectation = expectation(description: "Data request should start")
|
||||
|
||||
let expectedParameters = RequestParameters(name: UUID().uuidString)
|
||||
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
|
||||
request.httpMethod = Mock.HTTPMethod.post.rawValue
|
||||
request.httpBody = try JSONEncoder().encode(expectedParameters)
|
||||
|
||||
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
|
||||
mock.onRequestHandler = .init(httpBodyType: RequestParameters.self, callback: { request, postBodyDecodable in
|
||||
XCTAssertEqual(request.url, mock.request.url)
|
||||
XCTAssertEqual(expectedParameters, postBodyDecodable)
|
||||
onRequestExpectation.fulfill()
|
||||
})
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: request).resume()
|
||||
|
||||
wait(for: [onRequestExpectation], timeout: 2.0)
|
||||
}
|
||||
|
||||
func testOnRequestJSONDictionaryPostBodyParameters() throws {
|
||||
let onRequestExpectation = expectation(description: "Data request should start")
|
||||
|
||||
let expectedParameters = ["test": "value"]
|
||||
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
|
||||
request.httpMethod = Mock.HTTPMethod.post.rawValue
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
|
||||
|
||||
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
|
||||
mock.onRequestHandler = .init(jsonDictionaryCallback: { request, postBodyArguments in
|
||||
XCTAssertEqual(request.url, mock.request.url)
|
||||
XCTAssertEqual(expectedParameters, postBodyArguments as? [String: String])
|
||||
onRequestExpectation.fulfill()
|
||||
})
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: request).resume()
|
||||
|
||||
wait(for: [onRequestExpectation], timeout: 2.0)
|
||||
}
|
||||
|
||||
func testOnRequestCallbackWithoutRequestAndParameters() throws {
|
||||
let onRequestExpectation = expectation(description: "Data request should start")
|
||||
|
||||
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
|
||||
request.httpMethod = Mock.HTTPMethod.post.rawValue
|
||||
|
||||
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
|
||||
mock.onRequestHandler = .init(callback: {
|
||||
onRequestExpectation.fulfill()
|
||||
})
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: request).resume()
|
||||
|
||||
wait(for: [onRequestExpectation], timeout: 2.0)
|
||||
}
|
||||
|
||||
/// It should report post body arguments with top level collection type if they exist.
|
||||
func testOnRequestPostBodyParametersWithTopLevelCollectionType() throws {
|
||||
let onRequestExpectation = expectation(description: "Data request should start")
|
||||
|
||||
let expectedParameters = [["test": "value"], ["test": "value"]]
|
||||
var request = URLRequest(url: URL(string: "https://www.fakeurl.com")!)
|
||||
request.httpMethod = Mock.HTTPMethod.post.rawValue
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: expectedParameters, options: .prettyPrinted)
|
||||
|
||||
var mock = Mock(url: request.url!, contentType: .json, statusCode: 200, data: [.post: Data()])
|
||||
mock.onRequestHandler = OnRequestHandler(jsonArrayCallback: { request, postBodyArguments in
|
||||
XCTAssertEqual(request.url, mock.request.url)
|
||||
XCTAssertEqual(expectedParameters, postBodyArguments as? [[String: String]])
|
||||
onRequestExpectation.fulfill()
|
||||
})
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: request).resume()
|
||||
|
||||
wait(for: [onRequestExpectation], timeout: 2.0)
|
||||
}
|
||||
|
||||
/// It should call the mock after a delay.
|
||||
func testDelayedMock() {
|
||||
let nonDelayExpectation = expectation(description: "Data request should succeed")
|
||||
let delayedExpectation = expectation(description: "Data request should succeed")
|
||||
var delayedMock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
|
||||
delayedMock.delay = DispatchTimeInterval.seconds(1)
|
||||
delayedMock.completion = {
|
||||
delayedExpectation.fulfill()
|
||||
}
|
||||
delayedMock.register()
|
||||
var nonDelayMock = Mock(contentType: .json, statusCode: 200, data: [.post: Data()])
|
||||
nonDelayMock.completion = {
|
||||
nonDelayExpectation.fulfill()
|
||||
}
|
||||
nonDelayMock.register()
|
||||
|
||||
XCTAssertNotEqual(delayedMock.request.url, nonDelayMock.request.url)
|
||||
|
||||
URLSession.shared.dataTask(with: delayedMock.request).resume()
|
||||
URLSession.shared.dataTask(with: nonDelayMock.request).resume()
|
||||
|
||||
wait(for: [nonDelayExpectation, delayedExpectation], timeout: 2.0, enforceOrder: true)
|
||||
}
|
||||
|
||||
/// It should remove all registered mocks correctly.
|
||||
func testRemoveAll() {
|
||||
let mock = Mock(contentType: .json, statusCode: 200, data: [.get: Data()])
|
||||
mock.register()
|
||||
Mocker.removeAll()
|
||||
XCTAssertTrue(Mocker.shared.mocks.isEmpty)
|
||||
}
|
||||
|
||||
/// It should correctly add two mocks for the same URL if the HTTP method is different.
|
||||
func testDifferentHTTPMethodSameURL() {
|
||||
let url = URL(string: "https://www.fakeurl.com/\(UUID().uuidString)")!
|
||||
Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()]).register()
|
||||
Mock(url: url, contentType: .json, statusCode: 200, data: [.put: Data()]).register()
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = Mock.HTTPMethod.get.rawValue
|
||||
XCTAssertNotNil(Mocker.mock(for: request))
|
||||
request.httpMethod = Mock.HTTPMethod.put.rawValue
|
||||
XCTAssertNotNil(Mocker.mock(for: request))
|
||||
}
|
||||
|
||||
/// It should call the on request expectation.
|
||||
func testOnRequestExpectation() {
|
||||
let url = URL(string: "https://www.fakeurl.com")!
|
||||
|
||||
var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
|
||||
let expectation = expectationForRequestingMock(&mock)
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
|
||||
|
||||
wait(for: [expectation], timeout: 2.0)
|
||||
}
|
||||
|
||||
/// It should call the on completion expectation.
|
||||
func testOnCompletionExpectation() {
|
||||
let url = URL(string: "https://www.fakeurl.com")!
|
||||
|
||||
var mock = Mock(url: url, contentType: .json, statusCode: 200, data: [.get: Data()])
|
||||
let expectation = expectationForCompletingMock(&mock)
|
||||
mock.register()
|
||||
|
||||
URLSession.shared.dataTask(with: URLRequest(url: url)).resume()
|
||||
|
||||
wait(for: [expectation], timeout: 2.0)
|
||||
}
|
||||
|
||||
/// it should return the error we requested from the mock when we pass in an Error.
|
||||
func testMockReturningError() {
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
|
||||
|
||||
enum TestExampleError: Error, LocalizedError {
|
||||
case example
|
||||
|
||||
var errorDescription: String { "example" }
|
||||
}
|
||||
|
||||
Mock(url: originalURL, contentType: .json, statusCode: 500, data: [.get: Data()], requestError: TestExampleError.example).register()
|
||||
|
||||
URLSession.shared.dataTask(with: originalURL) { (data, urlresponse, error) in
|
||||
|
||||
XCTAssertNil(data)
|
||||
XCTAssertNil(urlresponse)
|
||||
XCTAssertNotNil(error)
|
||||
if let error = error {
|
||||
#if os(Linux)
|
||||
XCTAssertEqual(error as? TestExampleError, .example)
|
||||
#else
|
||||
// there's not a particularly elegant way to verify an instance
|
||||
// of an error, but this is a convenient workaround for testing
|
||||
// purposes
|
||||
XCTAssertTrue(String(describing: error).contains("TestExampleError"))
|
||||
#endif
|
||||
}
|
||||
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should cache response
|
||||
func testMockCachePolicy() throws {
|
||||
#if os(Linux)
|
||||
throw XCTSkip("URLSessionTask in swift-corelibs-foundation doesn't cache response for custom protocols")
|
||||
#endif
|
||||
let expectation = self.expectation(description: "Data request should succeed")
|
||||
let originalURL = URL(string: "https://www.wetransfer.com/example.json")!
|
||||
|
||||
Mock(url: originalURL, cacheStoragePolicy: .allowed,
|
||||
contentType: .json, statusCode: 200,
|
||||
data: [.get: MockedData.exampleJSON.data],
|
||||
additionalHeaders: ["Cache-Control": "public, max-age=31557600, immutable"]
|
||||
).register()
|
||||
|
||||
let configuration = URLSessionConfiguration.default
|
||||
#if !os(Linux)
|
||||
configuration.urlCache = URLCache()
|
||||
#endif
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
let urlSession = URLSession(configuration: configuration)
|
||||
|
||||
urlSession.dataTask(with: originalURL) { (_, _, error) in
|
||||
XCTAssertNil(error)
|
||||
|
||||
let cachedResponse = configuration.urlCache?.cachedResponse(for: URLRequest(url: originalURL))
|
||||
XCTAssertNotNil(cachedResponse)
|
||||
XCTAssertEqual(cachedResponse!.data, MockedData.exampleJSON.data)
|
||||
|
||||
expectation.fulfill()
|
||||
}.resume()
|
||||
|
||||
waitForExpectations(timeout: 10.0, handler: nil)
|
||||
}
|
||||
|
||||
/// It should process unknown URL
|
||||
func testMockerOptoutMode() {
|
||||
Mocker.mode = .optout
|
||||
|
||||
let mockedURL = URL(string: "www.google.com")!
|
||||
let ignoredURL = URL(string: "www.wetransfer.com")!
|
||||
let unknownURL = URL(string: "www.netflix.com")!
|
||||
|
||||
// Mocking
|
||||
Mock(url: mockedURL, contentType: .json, statusCode: 200, data: [.get: Data()])
|
||||
.register()
|
||||
|
||||
// Ignoring
|
||||
Mocker.ignore(ignoredURL)
|
||||
|
||||
// Checking mocked URL are processed by Mocker
|
||||
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: mockedURL)))
|
||||
// Checking ignored URL are not processed by Mocker
|
||||
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
|
||||
|
||||
// Checking unknown URL are processed by Mocker (.optout mode)
|
||||
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: unknownURL)))
|
||||
}
|
||||
|
||||
/// It should not process unknown URL
|
||||
func testMockerOptinMode() {
|
||||
Mocker.mode = .optin
|
||||
|
||||
let mockedURL = URL(string: "www.google.com")!
|
||||
let ignoredURL = URL(string: "www.wetransfer.com")!
|
||||
let unknownURL = URL(string: "www.netflix.com")!
|
||||
|
||||
// Mocking
|
||||
Mock(url: mockedURL, contentType: .json, statusCode: 200, data: [.get: Data()])
|
||||
.register()
|
||||
|
||||
// Ignoring
|
||||
Mocker.ignore(ignoredURL)
|
||||
|
||||
// Checking mocked URL are processed by Mocker
|
||||
XCTAssertTrue(MockingURLProtocol.canInit(with: URLRequest(url: mockedURL)))
|
||||
// Checking ignored URL are not processed by Mocker
|
||||
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: ignoredURL)))
|
||||
|
||||
// Checking unknown URL are not processed by Mocker (.optin mode)
|
||||
XCTAssertFalse(MockingURLProtocol.canInit(with: URLRequest(url: unknownURL)))
|
||||
}
|
||||
|
||||
/// Default mode should be .optout
|
||||
func testDefaultMode() {
|
||||
/// Checking default mode
|
||||
XCTAssertEqual(.optout, Mocker.mode)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Mocker",
|
||||
"owner": "WeTransfer"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
HTTP/1.1 302 Moved Temporarily
|
||||
Content-Type: text/html;charset=utf-8
|
||||
Content-Length: 0
|
||||
Cache-Control: public
|
||||
Date: Tue, 10 Oct 2017 07:28:33 GMT
|
||||
Location: https://wetransfer.com/redirect
|
||||
Server: nginx/1.12.0
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Request-Id: 8c43587ec891b2f1f72c61ecec2e96db
|
||||
X-XSS-Protection: 1; mode=block
|
||||
X-Cache: Miss from cloudfront
|
||||
Via: 1.1 72f202fb973968c0cfdb028ab6f36fac.cloudfront.net (CloudFront)
|
||||
X-Amz-Cf-Id: tU8eVZ9jWBJzd3aEB-4gyym_VxcPKskWFByEvXapy5WrdDkV-35-KA==
|
||||
Connection: Keep-alive
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user