From 0b2e8e25167f5521bb3c11a63b4c175bfa051fd3 Mon Sep 17 00:00:00 2001 From: Flynn Date: Sat, 20 Jan 2024 01:38:10 -0500 Subject: [PATCH] Added test implementation and helpers. Added initial workflow. Lock --- .github/workflows/test.yml | 22 ++++ deno.lock | 208 +++++++++++++++++++++++++++++ tests/basic.test.ts | 176 +++++++++++++++++++++++++ tests/internal/common.ts | 117 ++++++++++++++++ tests/internal/deps.ts | 4 + tests/internal/stubs.ts | 264 +++++++++++++++++++++++++++++++++++++ tests/internal/time.ts | 50 +++++++ tests/slack.ts | 96 ++++++++++++++ 8 files changed, 937 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 deno.lock create mode 100644 tests/basic.test.ts create mode 100644 tests/internal/common.ts create mode 100644 tests/internal/deps.ts create mode 100644 tests/internal/stubs.ts create mode 100644 tests/internal/time.ts create mode 100644 tests/slack.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4779870 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Run tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: denoland/setup-deno@v1 + with: + deno-version: "1.39.4" + + - name: Test + run: deno test --allow-env --allow-read --trace-ops diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..324663a --- /dev/null +++ b/deno.lock @@ -0,0 +1,208 @@ +{ + "version": "3", + "remote": { + "https://deno.land/std@0.140.0/_deno_unstable.ts": "be3276fd42cffb49f51b705c4b0aa8656aaf2a34be22d769455c8e50ea38e51a", + "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", + "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", + "https://deno.land/std@0.140.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06", + "https://deno.land/std@0.140.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0", + "https://deno.land/std@0.140.0/async/debounce.ts": "564273ef242bcfcda19a439132f940db8694173abffc159ea34f07d18fc42620", + "https://deno.land/std@0.140.0/async/deferred.ts": "bc18e28108252c9f67dfca2bbc4587c3cbf3aeb6e155f8c864ca8ecff992b98a", + "https://deno.land/std@0.140.0/async/delay.ts": "cbbdf1c87d1aed8edc7bae13592fb3e27e3106e0748f089c263390d4f49e5f6c", + "https://deno.land/std@0.140.0/async/mod.ts": "6e42e275b44367361a81842dd1a789c55ab206d7c8a877d7163ab5c460625be6", + "https://deno.land/std@0.140.0/async/mux_async_iterator.ts": "f4d1d259b0c694d381770ddaaa4b799a94843eba80c17f4a2ec2949168e52d1e", + "https://deno.land/std@0.140.0/async/pool.ts": "97b0dd27c69544e374df857a40902e74e39532f226005543eabacb551e277082", + "https://deno.land/std@0.140.0/async/tee.ts": "1341feb1f5b1a96f8628d0f8fc07d8c43d3813423f18a63bf1b4785568d21b1f", + "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d", + "https://deno.land/std@0.140.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9", + "https://deno.land/std@0.140.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", + "https://deno.land/std@0.140.0/encoding/base64.ts": "c8c16b4adaa60d7a8eee047c73ece26844435e8f7f1328d74593dbb2dd58ea4f", + "https://deno.land/std@0.140.0/encoding/base64url.ts": "55f9d13df02efac10c6f96169daa3e702606a64e8aa27c0295f645f198c27130", + "https://deno.land/std@0.140.0/flags/mod.ts": "019df8a63ed24df2d10be22e8983aa9253623e871228a2f8f328ff2d0404f7ef", + "https://deno.land/std@0.140.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37", + "https://deno.land/std@0.140.0/fmt/printf.ts": "e2c0f72146aed1efecf0c39ab928b26ae493a2278f670a871a0fbdcf36ff3379", + "https://deno.land/std@0.140.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b", + "https://deno.land/std@0.140.0/node/_buffer.mjs": "cf4854f0873fded5bf3efe1e0d6aacb148822604f91e0e3c5a9271b68997f07b", + "https://deno.land/std@0.140.0/node/_core.ts": "568d277be2e086af996cbdd599fec569f5280e9a494335ca23ad392b130d7bb9", + "https://deno.land/std@0.140.0/node/_events.mjs": "a87d6bc0e2a139ce1bb89e56fe36969dc960c1af70b6d5fafaab8782659c57a0", + "https://deno.land/std@0.140.0/node/_next_tick.ts": "3546559be2b353208f8b10df81c6d9c26c045fa4ea811926f6596f2dc6b1b0b1", + "https://deno.land/std@0.140.0/node/_process/exiting.ts": "bc9694769139ffc596f962087155a8bfef10101d03423b9dcbc51ce6e1f88fce", + "https://deno.land/std@0.140.0/node/_process/process.ts": "84644b184053835670f79652d1ce3312c9ad079c211e6207ebefeedf159352a3", + "https://deno.land/std@0.140.0/node/_process/stdio.mjs": "971c3b086040d8521562155db13f22f9971d5c42c852b2081d4d2f0d8b6ab6bd", + "https://deno.land/std@0.140.0/node/_process/streams.mjs": "555062e177ad05f887147651fdda25fa55098475fcf142c8d162b8fe14097bbb", + "https://deno.land/std@0.140.0/node/_stream.mjs": "07f6cbabaad0382fb4b9a25e70ac3093a44022b859247f64726746e6373f1c91", + "https://deno.land/std@0.140.0/node/_util/_util_callbackify.ts": "79928ad80df3e469f7dcdb198118a7436d18a9f6c08bd7a4382332ad25a718cf", + "https://deno.land/std@0.140.0/node/_utils.ts": "ae3ee3999c0b82c3d3d34c2ab5d85ff899f441662a9de05b52b68c39dce8a72c", + "https://deno.land/std@0.140.0/node/buffer.ts": "fbecbf3f237fa49bec96e97ecf56a7b92d48037b3d11219288e68943cc921600", + "https://deno.land/std@0.140.0/node/events.ts": "a1d40fc0dbccc944379ef968b80ea08f9fce579e88b5057fdb64e4f0812476dd", + "https://deno.land/std@0.140.0/node/internal/blob.mjs": "52080b2f40b114203df67f8a6650f9fe3c653912b8b3ef2f31f029853df4db53", + "https://deno.land/std@0.140.0/node/internal/buffer.mjs": "6662fe7fe517329453545be34cea27a24f8ccd6d09afd4f609f11ade2b6dfca7", + "https://deno.land/std@0.140.0/node/internal/crypto/keys.ts": "16ce7b15a9fc5e4e3dee8fde75dae12f3d722558d5a1a6e65a9b4f86d64a21e9", + "https://deno.land/std@0.140.0/node/internal/crypto/util.mjs": "1de55a47fdbed6721b467a77ba48fdd1550c10b5eee77bbdb602eaffee365a5e", + "https://deno.land/std@0.140.0/node/internal/error_codes.ts": "ac03c4eae33de3a69d6c98e8678003207eecf75a6900eb847e3fea3c8c9e6d8f", + "https://deno.land/std@0.140.0/node/internal/errors.ts": "fe669a2482099ddd2d5c9d76a8971b5fb47d7a4c6a98c7f3ef9f366bf10e1998", + "https://deno.land/std@0.140.0/node/internal/fixed_queue.ts": "455b3c484de48e810b13bdf95cd1658ecb1ba6bcb8b9315ffe994efcde3ba5f5", + "https://deno.land/std@0.140.0/node/internal/hide_stack_frames.ts": "a91962ec84610bc7ec86022c4593cdf688156a5910c07b5bcd71994225c13a03", + "https://deno.land/std@0.140.0/node/internal/net.ts": "1239886cd2508a68624c2dae8abf895e8aa3bb15a748955349f9ac5539032238", + "https://deno.land/std@0.140.0/node/internal/normalize_encoding.mjs": "3779ec8a7adf5d963b0224f9b85d1bc974a2ec2db0e858396b5d3c2c92138a0a", + "https://deno.land/std@0.140.0/node/internal/options.ts": "a23c285975e058cb26a19abcb048cd8b46ab12d21cfb028868ac8003fffb43ac", + "https://deno.land/std@0.140.0/node/internal/process/per_thread.mjs": "a42b1dcfb009ad5039144a999a35a429e76112f9322febbe353eda9d1879d936", + "https://deno.land/std@0.140.0/node/internal/streams/_utils.ts": "77fceaa766679847e4d4c3c96b2573c00a790298d90551e8e4df1d5e0fdaad3b", + "https://deno.land/std@0.140.0/node/internal/streams/add-abort-signal.mjs": "5623b83fa64d439cc4a1f09ae47ec1db29512cc03479389614d8f62a37902f5e", + "https://deno.land/std@0.140.0/node/internal/streams/buffer_list.mjs": "c6a7b29204fae025ff5e9383332acaea5d44bc7c522a407a79b8f7a6bc6c312d", + "https://deno.land/std@0.140.0/node/internal/streams/compose.mjs": "b522daab35a80ae62296012a4254fd7edfc0366080ffe63ddda4e38fe6b6803e", + "https://deno.land/std@0.140.0/node/internal/streams/destroy.mjs": "9c9bbeb172a437041d529829f433df72cf0b63ae49f3ee6080a55ffbef7572ad", + "https://deno.land/std@0.140.0/node/internal/streams/duplex.mjs": "95e10e60d31ed3290c33110f8485196bdee19e12550b46e0be9d93b51f8dec23", + "https://deno.land/std@0.140.0/node/internal/streams/end-of-stream.mjs": "38be76eaceac231dfde643e72bc0940625446bf6d1dbd995c91c5ba9fd59b338", + "https://deno.land/std@0.140.0/node/internal/streams/from.mjs": "134255c698ed63b33199911eb8e042f8f67e9682409bb11552e6120041ed1872", + "https://deno.land/std@0.140.0/node/internal/streams/legacy.mjs": "6ea28db95d4503447473e62f0b23ff473bfe1751223c33a3c5816652e93b257a", + "https://deno.land/std@0.140.0/node/internal/streams/passthrough.mjs": "a51074193b959f3103d94de41e23a78dfcff532bdba53af9146b86340d85eded", + "https://deno.land/std@0.140.0/node/internal/streams/pipeline.mjs": "9890b121759ede869174ef70c011fde964ca94d81f2ed97b8622d7cb17b49285", + "https://deno.land/std@0.140.0/node/internal/streams/readable.mjs": "a70c41171ae25c556b52785b0c178328014bd33d8c0e4d229d4adaac7414b6ca", + "https://deno.land/std@0.140.0/node/internal/streams/state.mjs": "9ef917392a9d8005a6e038260c5fd31518d2753aea0bc9e39824c199310434cb", + "https://deno.land/std@0.140.0/node/internal/streams/transform.mjs": "3b361abad2ac78f7ccb6f305012bafdc0e983dfa4bb6ecddb4626e34a781a5f5", + "https://deno.land/std@0.140.0/node/internal/streams/utils.mjs": "06c21d0db0d51f1bf1e3225a661c3c29909be80355d268e64ee5922fc5eb6c5e", + "https://deno.land/std@0.140.0/node/internal/streams/writable.mjs": "ad4e2b176ffdf752c8e678ead3a514679a5a8d652f4acf797115dceb798744d5", + "https://deno.land/std@0.140.0/node/internal/timers.mjs": "b43e24580cec2dd50f795e4342251a79515c0db21630c25b40fdc380a78b74e7", + "https://deno.land/std@0.140.0/node/internal/util.mjs": "2f0c8ff553c175ea6e4ed13d7cd7cd6b86dc093dc2f783c6c3dfc63f60a0943e", + "https://deno.land/std@0.140.0/node/internal/util/comparisons.ts": "680b55fe8bdf1613633bc469fa0440f43162c76dbe36af9aa2966310e1bb9f6e", + "https://deno.land/std@0.140.0/node/internal/util/debuglog.ts": "6f12a764f5379e9d2675395d15d2fb48bd7376921ef64006ffb022fc7f44ab82", + "https://deno.land/std@0.140.0/node/internal/util/inspect.mjs": "d1c2569c66a3dab45eec03208f22ad4351482527859c0011a28a6c797288a0aa", + "https://deno.land/std@0.140.0/node/internal/util/types.ts": "b2dacb8f1f5d28a51c4da5c5b75172b7fcf694073ce95ca141323657e18b0c60", + "https://deno.land/std@0.140.0/node/internal/validators.mjs": "a7e82eafb7deb85c332d5f8d9ffef052f46a42d4a121eada4a54232451acc49a", + "https://deno.land/std@0.140.0/node/internal_binding/_libuv_winerror.ts": "801e05c2742ae6cd42a5f0fd555a255a7308a65732551e962e5345f55eedc519", + "https://deno.land/std@0.140.0/node/internal_binding/_listen.ts": "94ca6d255a4e6718286c65fb30de23ee7038bc9734fe4a2c530c5b93c83c8166", + "https://deno.land/std@0.140.0/node/internal_binding/_node.ts": "e4075ba8a37aef4eb5b592c8e3807c39cb49ca8653faf8e01a43421938076c1b", + "https://deno.land/std@0.140.0/node/internal_binding/_utils.ts": "1c50883b5751a9ea1b38951e62ed63bacfdc9d69ea665292edfa28e1b1c5bd94", + "https://deno.land/std@0.140.0/node/internal_binding/_winerror.ts": "8811d4be66f918c165370b619259c1f35e8c3e458b8539db64c704fbde0a7cd2", + "https://deno.land/std@0.140.0/node/internal_binding/ares.ts": "33ff8275bc11751219af8bd149ea221c442d7e8676e3e9f20ccb0e1f0aac61b8", + "https://deno.land/std@0.140.0/node/internal_binding/async_wrap.ts": "b83e4021a4854b2e13720f96d21edc11f9905251c64c1bc625a361f574400959", + "https://deno.land/std@0.140.0/node/internal_binding/buffer.ts": "781e1d13adc924864e6e37ecb5152e8a4e994cf394695136e451c47f00bda76c", + "https://deno.land/std@0.140.0/node/internal_binding/cares_wrap.ts": "ca96eab31788c84f73f55b015c4d259dbdfa54b525550a20a7a8f9c6788ac208", + "https://deno.land/std@0.140.0/node/internal_binding/config.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/connection_wrap.ts": "0380444ee94d5bd7b0b09921223d16729c9762a94e80b7f51eda49c7f42e6d0a", + "https://deno.land/std@0.140.0/node/internal_binding/constants.ts": "01ab8a464cbee5f351ceba75776ffaa5733c076972f8a2171ddc55f720b6925d", + "https://deno.land/std@0.140.0/node/internal_binding/contextify.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/credentials.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/crypto.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/errors.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/fs.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/fs_dir.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/fs_event_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/handle_wrap.ts": "eadbeea68deee9768b50f8f797738a088e8a7a1c9aa7d092c955faeacac53d58", + "https://deno.land/std@0.140.0/node/internal_binding/heap_utils.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/http_parser.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/icu.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/inspector.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/js_stream.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/messaging.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/mod.ts": "f68e74e8eed84eaa6b0de24f0f4c47735ed46866d7ee1c5a5e7c0667b4f0540f", + "https://deno.land/std@0.140.0/node/internal_binding/module_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/native_module.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/natives.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/node_file.ts": "c96ee0b2af319a3916de950a6c4b0d5fb00d09395c51cd239c54d95d62567aaf", + "https://deno.land/std@0.140.0/node/internal_binding/node_options.ts": "3cd5706153d28a4f5944b8b162c1c61b7b8e368a448fb1a2cff9f7957d3db360", + "https://deno.land/std@0.140.0/node/internal_binding/options.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/os.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/performance.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/pipe_wrap.ts": "792e3bbcbdb7ce3b51a430a85331a90408113160739d72d050ab243714219430", + "https://deno.land/std@0.140.0/node/internal_binding/process_methods.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/report.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/serdes.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/signal_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/spawn_sync.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/stream_wrap.ts": "7a418a7c57bbfb642a111753c98e9cdfcd4aafa5aec4b48be1dbd62e08c2d9a7", + "https://deno.land/std@0.140.0/node/internal_binding/string_decoder.ts": "5cb1863763d1e9b458bc21d6f976f16d9c18b3b3f57eaf0ade120aee38fba227", + "https://deno.land/std@0.140.0/node/internal_binding/symbols.ts": "51cfca9bb6132d42071d4e9e6b68a340a7f274041cfcba3ad02900886e972a6c", + "https://deno.land/std@0.140.0/node/internal_binding/task_queue.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/tcp_wrap.ts": "dc30a903d7589dc82b8056a473b0318ecf3262e5c9e5974375fee8548b847056", + "https://deno.land/std@0.140.0/node/internal_binding/timers.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/tls_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/trace_events.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/tty_wrap.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/types.ts": "4c26fb74ba2e45de553c15014c916df6789529a93171e450d5afb016b4c765e7", + "https://deno.land/std@0.140.0/node/internal_binding/udp_wrap.ts": "74ef0ac9bd1e4ad2f2f470edf25a805f9dc7250cd7aaea43116a959da9134590", + "https://deno.land/std@0.140.0/node/internal_binding/url.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/util.ts": "faf5146c3cc3b2d6c26026a818b4a16e91488ab26e63c069f36ba3c3ae24c97b", + "https://deno.land/std@0.140.0/node/internal_binding/uv.ts": "aa1db842936e77654522d9136bb2ae191bf334423f58962a8a7404b6635b5b49", + "https://deno.land/std@0.140.0/node/internal_binding/v8.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/worker.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/internal_binding/zlib.ts": "e292217d048a33573966b7d25352828d3282921fbcadce8735a20fb3da370cc4", + "https://deno.land/std@0.140.0/node/process.ts": "52e34ac80ce94e157c6df9e25343b3863182d639f0113d0dabb99f7fb59d55ca", + "https://deno.land/std@0.140.0/node/stream.ts": "d127faa074a9e3886e4a01dcfe9f9a6a4b5641f76f6acc356e8ded7da5dc2c81", + "https://deno.land/std@0.140.0/node/stream/promises.mjs": "b263c09f2d6bd715dc514fab3f99cca84f442e2d23e87adbe76e32ea46fc87e6", + "https://deno.land/std@0.140.0/node/string_decoder.ts": "51ce85a173d2e36ac580d418bb48b804adb41732fc8bd85f7d5d27b7accbc61f", + "https://deno.land/std@0.140.0/node/timers.ts": "75956734ab2c69dd4038974d8d33c82a03c3da49568f4004287d87a407912079", + "https://deno.land/std@0.140.0/node/util.ts": "7fd6933b37af89a8e64d73dc6ee1732455a59e7e6d0965311fbd73cd634ea630", + "https://deno.land/std@0.140.0/node/util/types.mjs": "f9288198cacd374b41bae7e92a23179d3160f4c0eaf14e19be3a4e7057219a60", + "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", + "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", + "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", + "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44", + "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757", + "https://deno.land/std@0.140.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21", + "https://deno.land/std@0.140.0/testing/_diff.ts": "029a00560b0d534bc0046f1bce4bd36b3b41ada3f2a3178c85686eb2ff5f1413", + "https://deno.land/std@0.140.0/testing/_format.ts": "0d8dc79eab15b67cdc532826213bbe05bccfd276ca473a50a3fc7bbfb7260642", + "https://deno.land/std@0.140.0/testing/asserts.ts": "3eea146ece74b36d4add6243c19c302e47b2b0201fbeceeeb243b06b6b410af7", + "https://deno.land/std@0.180.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.180.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + "https://deno.land/std@0.180.0/collections/_utils.ts": "5114abc026ddef71207a79609b984614e66a63a4bda17d819d56b0e72c51527e", + "https://deno.land/std@0.180.0/collections/deep_merge.ts": "5a8ed29030f4471a5272785c57c3455fa79697b9a8f306013a8feae12bafc99a", + "https://deno.land/std@0.180.0/encoding/toml.ts": "56e854b1f46e8a60dd3ba9e31d701cd3f9bba121e32609df02fabae17fca8546", + "https://deno.land/std@0.180.0/fmt/colors.ts": "938c5d44d889fb82eff6c358bea8baa7e85950a16c9f6dae3ec3a7a729164471", + "https://deno.land/std@0.180.0/fs/exists.ts": "b8c8a457b71e9d7f29b9d2f87aad8dba2739cbe637e8926d6ba6e92567875f8e", + "https://deno.land/std@0.180.0/io/buf_writer.ts": "2fcaadd9f157970fede6e79c8ea9a58556d8cf3c8a686c3fcaaf3875460092cc", + "https://deno.land/std@0.180.0/log/handlers.ts": "38871ecbfa67b0d39dc3384210439ac9a13cba6118b912236f9011b5989b9a4d", + "https://deno.land/std@0.180.0/log/levels.ts": "6309147664e9e008cd6671610f2505c4c95f181f6bae4816a84b33e0aec66859", + "https://deno.land/std@0.180.0/log/logger.ts": "257ceb47e3f5f872068073de9809b015a7400e8d86dd40563c1d80169e578f71", + "https://deno.land/std@0.180.0/log/mod.ts": "36d156ad18de3f1806c6ddafa4965129be99ccafc27f1813de528d65b6c528bf", + "https://deno.land/std@0.180.0/toml/_parser.ts": "ca98edfbdbc5741734134548db65d4c10ba60abbd739f10f949aa912664fd86e", + "https://deno.land/std@0.180.0/toml/mod.ts": "dbaeb6039241406b369d3edbd59434dc938256e1d8664115277c5cadcbfacd81", + "https://deno.land/std@0.180.0/toml/parse.ts": "627c14a36b1df3f7aaa606833dbf0d001fa9901b77bad3da1a887c812bfad472", + "https://deno.land/std@0.180.0/toml/stringify.ts": "f59c0647ddc6a3a1215294b279c1ea6c8947d262de9d7067b4500f75616cd259", + "https://deno.land/std@0.212.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.212.0/assert/_diff.ts": "dcc63d94ca289aec80644030cf88ccbf7acaa6fbd7b0f22add93616b36593840", + "https://deno.land/std@0.212.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4", + "https://deno.land/std@0.212.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", + "https://deno.land/std@0.212.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f", + "https://deno.land/std@0.212.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1", + "https://deno.land/std@0.212.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e", + "https://deno.land/std@0.212.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9", + "https://deno.land/std@0.212.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769", + "https://deno.land/std@0.212.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c", + "https://deno.land/std@0.212.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219", + "https://deno.land/std@0.212.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444", + "https://deno.land/std@0.212.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2", + "https://deno.land/std@0.212.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005", + "https://deno.land/std@0.212.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0", + "https://deno.land/std@0.212.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1", + "https://deno.land/std@0.212.0/assert/assert_not_equals.ts": "f3edda73043bc2c9fae6cbfaa957d5c69bbe76f5291a5b0466ed132c8789df4c", + "https://deno.land/std@0.212.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931", + "https://deno.land/std@0.212.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f", + "https://deno.land/std@0.212.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be", + "https://deno.land/std@0.212.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49", + "https://deno.land/std@0.212.0/assert/assert_rejects.ts": "e9e0c8d9c3e164c7ac962c37b3be50577c5a2010db107ed272c4c1afb1269f54", + "https://deno.land/std@0.212.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366", + "https://deno.land/std@0.212.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7", + "https://deno.land/std@0.212.0/assert/assert_throws.ts": "edddd86b39606c342164b49ad88dd39a26e72a26655e07545d172f164b617fa7", + "https://deno.land/std@0.212.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", + "https://deno.land/std@0.212.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2", + "https://deno.land/std@0.212.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c", + "https://deno.land/std@0.212.0/assert/mod.ts": "325df8c0683ad83a873b9691aa66b812d6275fc9fec0b2d180ac68a2c5efed3b", + "https://deno.land/std@0.212.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd", + "https://deno.land/std@0.212.0/assert/unreachable.ts": "38cfecb95d8b06906022d2f9474794fca4161a994f83354fd079cac9032b5145", + "https://deno.land/std@0.212.0/async/delay.ts": "eab3187eee39ccc8cc76d411fb21fb1801250ddb1090e486d5aec2ace5403391", + "https://deno.land/std@0.212.0/data_structures/_binary_search_node.ts": "ce1da11601fef0638df4d1e53c377f791f96913383277389286b390685d76c07", + "https://deno.land/std@0.212.0/data_structures/_red_black_node.ts": "c35830a4c60d04fad80e54e991ca6570bbe00a0a7a78d37450efc396fa51eaaf", + "https://deno.land/std@0.212.0/data_structures/binary_search_tree.ts": "2dd43d97ce5f5a4bdba11b075eb458db33e9143f50997b0eebf02912cb44f5d5", + "https://deno.land/std@0.212.0/data_structures/comparators.ts": "74e64752f005f03614d9bd4912ea64c58d2e663b5d8c9dba6e2e2997260f51eb", + "https://deno.land/std@0.212.0/data_structures/red_black_tree.ts": "8f4cbd2c31f7944ac427a1dc9d36d37ca29c02bdfd46722738cabe87c68ebc20", + "https://deno.land/std@0.212.0/fmt/colors.ts": "71e2d7d9911cf3f4f8cceaabe588fd9a4c0228102f4233da62ffd3e421201de7", + "https://deno.land/std@0.212.0/testing/_time.ts": "fefd1ff35b50a410db9b0e7227e05163e1b172c88afd0d2071df0125958c3ff3", + "https://deno.land/std@0.212.0/testing/time.ts": "0d25e0f15eded2d66c9ed37d16c3188b16cc1aefa58be4a4753afb7750e72cb0" + } +} diff --git a/tests/basic.test.ts b/tests/basic.test.ts new file mode 100644 index 0000000..221d83b --- /dev/null +++ b/tests/basic.test.ts @@ -0,0 +1,176 @@ +import { assertArrayIncludes, assertEquals } from './internal/deps.ts'; +import { stubFetch, stubReadTextFile } from './internal/stubs.ts'; +import { createFixedTimepoint, moveTimeToPosition } from './internal/time.ts'; +import createSlackActionsApi from './slack.ts'; + +Deno.test('Simple schedule test', async () => { + const helpers = await basicTestSetup(` +[first-status] +start = 16:00:00 +end = 17:00:00 +icon = ":test:" +message = [ + "This is my test status message", + "This is another possible status message" +] +days = ["Mon", "Tue", "Wed", "Thu", "Fri"] +doNotDisturb = true + +[second-status] +start = 18:00:00 +end = 20:00:00 +icon = ":test2:" +message = [ + "EEEEEE", + "AAAAAAH" +] +days = ["Tue", "Wed", "Thu", "Fri"] + `); + + const { time, runtime } = helpers; + + try { + // set a specific time that we know + moveTimeToPosition(time, '16:03'); + + // Queue the requests we expect to intercept - note that missing requests will fail the test + const userInfoRequest1 = helpers.slackApi.getProfileRequest(); + const dndInfoRequest1 = helpers.slackApi.getDndInfoRequest(); + const userSetRequest1 = helpers.slackApi.updateProfileRequest(); + const dndSetRequest1 = helpers.slackApi.setDndSnoozeRequest(); + + // First cycle iteration + await Promise.all([ + runtime.executeTask(), + userInfoRequest1, + dndInfoRequest1, + userSetRequest1, + dndSetRequest1, + ]); + + // Verify that the expected values were set + helpers.slackApi.assert((state) => { + assertEquals(state.statusEmoji, ':test:'); + assertEquals(state.statusExpiration, 820533600); + assertEquals(state.dndDurationMinutes, 56); + assertArrayIncludes([ + 'This is my test status message', + 'This is another possible status message', + ], [state.statusText]); + }); + + // move time to empty allocation (status should unset) + moveTimeToPosition(helpers.time, '17:30'); + + // Again, queue the requests we expect to intercept + const userInfoRequest2 = helpers.slackApi.getProfileRequest(); + const dndInfoRequest2 = helpers.slackApi.getDndInfoRequest(); + const userSetRequest2 = helpers.slackApi.updateProfileRequest(); + const dndEndRequest2 = helpers.slackApi.endDndSnoozeRequest(); + + // Second iteration (should empty status, and end DnD) + await Promise.all([ + runtime.executeTask(), + userInfoRequest2, + dndInfoRequest2, + userSetRequest2, + dndEndRequest2, + ]); + + // Verify that mocked Slack api has unset state + helpers.slackApi.assert((state) => { + assertEquals(state.statusEmoji, null); + assertEquals(state.statusExpiration, null); + assertEquals(state.dndDurationMinutes, 0); + assertEquals(state.statusText, ''); + }); + + // move time to next allocation (no overlap because it should be monday) + moveTimeToPosition(helpers.time, '18:10'); + + // Queue the requests we expect to intercept + const userInfoRequest3 = helpers.slackApi.getProfileRequest(); + const dndInfoRequest3 = helpers.slackApi.getDndInfoRequest(); + + // Perform work cycle + await Promise.all([ + runtime.executeTask(), + userInfoRequest3, + dndInfoRequest3, + ]); + + // Verify that mocked Slack api has unset state + helpers.slackApi.assert((state) => { + assertEquals(state.statusEmoji, null); + assertEquals(state.statusExpiration, null); + assertEquals(state.dndDurationMinutes, 0); + assertEquals(state.statusText, ''); + }); + + // move time to 19:00 hours on the following day (should be Tuesday) + moveTimeToPosition(helpers.time, '19:00', 1); + + const userInfoRequest4 = helpers.slackApi.getProfileRequest(); + const dndInfoRequest4 = helpers.slackApi.getDndInfoRequest(); + const userSetRequest4 = helpers.slackApi.updateProfileRequest(); + + await Promise.all([ + runtime.executeTask(), + userInfoRequest4, + dndInfoRequest4, + userSetRequest4, + ]); + + helpers.slackApi.assert((state) => { + assertEquals(state.statusEmoji, ':test2:'); + assertEquals(state.statusExpiration, 820630800); + assertEquals(state.dndDurationMinutes, 0); + assertArrayIncludes([ + 'EEEEEE', + 'AAAAAAH', + ], [state.statusText]); + }); + + // await duration(1000); + } catch (error) { + throw error; + } finally { + console.log('cleanup'); + helpers.cleanup(); + } +}); + +async function basicTestSetup( + scheduleTomlFile: string, +) { + // Setup stubs (clean after test) + const time = createFixedTimepoint(2, 1, 1996); + const fetchStub = stubFetch(); + const readFileStub = stubReadTextFile(); + + // Create fake secret and stub + readFileStub.set(`/run/secrets/slack_status_scheduler_user_token`, 'slack_status_scheduler_user_token'); + // Create toml schedule representation and stub + readFileStub.set('/schedule.toml', scheduleTomlFile); + + // create mock Slack api + const slackApi = createSlackActionsApi(fetchStub); + + // load runtime as import module + const runtimeModule = await import('../core/runtime.ts'); + const runtime = runtimeModule.createRuntimeScope({ crashOnException: true }); + + return { + runtime, + time, + fetchStub, + slackApi, + readFileStub, + cleanup() { + // runtime?.stop(); + time.restore(); + fetchStub.cleanup(); + readFileStub.cleanup(); + }, + }; +} diff --git a/tests/internal/common.ts b/tests/internal/common.ts new file mode 100644 index 0000000..5bb3af0 --- /dev/null +++ b/tests/internal/common.ts @@ -0,0 +1,117 @@ +import { setTimeout } from './time.ts'; + +/** + * Creates a simple promise + * @param data + * @returns + */ +export function createSimplePromise( + data: ResolveData, +): Promise { + return new Promise((resolve) => resolve(data)); +} + +/** + * Creates a shallow clone (useful in assertions) using JSON internals + * @param jsonFriendlyData The data to clone + * @returns The replicated object + */ +export function shallowClone(jsonFriendlyData: DataType): DataType { + return JSON.parse(JSON.stringify(jsonFriendlyData)); +} + +type DataMapKeyType = string | RegExp | undefined; + +/** + * Checks a string against a DataMapKey (string or Regexp) + * @param value The value to test + * @param testAgainst The test conditions (string as direct, regexp as well - regexp) + * @returns The test result + */ +export function stringMatchesKey(value: string, testAgainst: DataMapKeyType): boolean { + return (testAgainst instanceof RegExp && testAgainst.test(value)) || + (value === testAgainst); +} + +/** + * Iterate through the data map and check if any keys are matches (processes regular expressions) + * This function, for the most part, only accounts for functions that are string based (no fancy post processing) + * @param keyToCheck + * @returns + */ +export function getDataFromKey( + dataMap: Map, + keyToCheck: string, +): MapDataType | null { + for (const [key, value] of dataMap) { + if (stringMatchesKey(keyToCheck, key)) { + return value; + } + } + + return null; +} + +/** + * Parses JSON if it can, otherwise returns null. Useful for fetch interceptors. + * @param probablyJSONValue + * @returns + */ +export function jsonParseOrNull(probablyJSONValue: string) { + try { + return JSON.parse(probablyJSONValue); + } catch (_e) { + // noop + } + return null; +} + +/** + * Wait a minimum amount of time on the event loop + * @param timeInMs + * @returns + */ +export function duration(timeInMs: number) { + return new Promise((resolve) => setTimeout(() => resolve(null), timeInMs)); +} + +/** + * @unused + * Creates a handler for capturing async errors on the event loop that suddenly + * stop propagating. At the end of tests, this gets executed. + * @returns + */ +export function _captureEventLoopErrors() { + const eventLoopErrors: Error[] = []; + + function onUnhandledRejection(e: PromiseRejectionEvent) { + // Track the error + eventLoopErrors.push(e.reason as Error); + // Don't allow other code to track this exception + e.preventDefault(); + e.stopImmediatePropagation(); + } + + globalThis.addEventListener('unhandledrejection', onUnhandledRejection); + + return { + check() { + if (!eventLoopErrors.length) return; + + console.error(`(${eventLoopErrors.length}) rejections detected`); + + eventLoopErrors.forEach((error, errorIndex) => { + console.error(`(${errorIndex})`); + console.error(error); + }); + + throw new Error(`(${eventLoopErrors.length}) rejections detected`); + }, + cleanup() { + // remove the handler + globalThis.removeEventListener('unhandledrejection', onUnhandledRejection); + // nudge gc + eventLoopErrors.length = 0; + }, + }; +} diff --git a/tests/internal/deps.ts b/tests/internal/deps.ts new file mode 100644 index 0000000..1843406 --- /dev/null +++ b/tests/internal/deps.ts @@ -0,0 +1,4 @@ +export { FakeTime } from 'https://deno.land/std@0.212.0/testing/time.ts'; +export { _internals as FakeTimeInternals } from 'https://deno.land/std@0.212.0/testing/_time.ts'; + +export { assertArrayIncludes, assertEquals } from 'https://deno.land/std@0.212.0/assert/mod.ts'; diff --git a/tests/internal/stubs.ts b/tests/internal/stubs.ts new file mode 100644 index 0000000..5fedbd3 --- /dev/null +++ b/tests/internal/stubs.ts @@ -0,0 +1,264 @@ +// Overrides things like fetch & fs calls + +import { createSimplePromise, getDataFromKey, jsonParseOrNull, stringMatchesKey } from './common.ts'; +import { clearTimeout, setTimeout } from './time.ts'; + +const internalMarker = Symbol('#should_not_be_found#'); + +type FunctionToStubType = (...args: any[]) => unknown; +type StubKeyType = string | RegExp | undefined; +type FetchCallParameters = Parameters; + +interface GlobalStubOptions { + // function that injects/replaces the global/module ref + replacer: (refHelper: FunctionToStub) => void; + // interceptor for synchronous stubs + interceptor?: (interceptorArguments: Parameters) => unknown; + // interceptor for asynchronous stubs + asyncInterceptor?: (interceptorArguments: Parameters) => Promise; +} + +/** + * Low level stubbing helper. The StubDataType is intended to be used when a stubbed function takes strings as the first applicable argument + * for other functions (like fetch, which is more complex), an interceptor implementation should be provided + * @param originalGlobalRef + * @param options + * @returns + */ +function createStubForRef( + originalGlobalRef: FunctionToStub, + options: GlobalStubOptions, +) { + type FunctionToSubArguments = Parameters; + + // @ts-ignore internal check + if (originalGlobalRef[internalMarker] === true) { + throw new Error('An existing stub for this global was not cleared. Please check tests'); + } + + const stubDataMap = new Map(); + + // For the async pathway, we need to await + const stubbedFunction = options.asyncInterceptor + ? async function asyncStubbedFunction(...stubbedFunctionArguments: FunctionToSubArguments) { + const overrideReturn = options.asyncInterceptor && + (await options.asyncInterceptor(stubbedFunctionArguments)); + return overrideReturn || getDataFromKey(stubDataMap, stubbedFunctionArguments[0]) || + originalGlobalRef(stubbedFunctionArguments); + } + : function stubbedFunction(...stubbedFunctionArguments: FunctionToSubArguments) { + const overrideReturn = options.interceptor && options.interceptor(stubbedFunctionArguments); + return overrideReturn || getDataFromKey(stubDataMap, stubbedFunctionArguments[0]) || + originalGlobalRef(stubbedFunctionArguments); + }; + + // replace the global with an assignment to the newly created stub above + // @ts-ignore this is literally magic, calm down typescript + options.replacer(stubbedFunction); + + return { + /** + * Sets the return value of function being stubbed + * @param key + * @param value + */ + set(key: StubKey, value: StubDataType) { + stubDataMap.set(key, value); + }, + /** + * Clears all preset values + */ + clear() { + stubDataMap.clear(); + }, + /** + * Replaces the stubbed function with the cleaned up function + */ + cleanup() { + options.replacer(originalGlobalRef); + }, + }; +} + +/** + * Replaces Deno.readTextFile with basic stub implementation. + * @returns + */ +export function stubReadTextFile() { + return createStubForRef(Deno.readTextFile, { + replacer: (ref) => { + Deno.readTextFile = ref; + }, + }); +} + +interface FetchStubOptions { + throwOnMissingIntercept?: boolean; +} + +/** + * Replaces fetch implementation with full interceptor support, enabling assertions + * TODO: Potentially modularize this for re-use within other Deno projects + * @param fetchStubOptions + * @returns + */ +export function stubFetch(fetchStubOptions: FetchStubOptions = { throwOnMissingIntercept: true }) { + // We get a little bit tricky here - we want to simulate the lease amount of the fetch API we need to + // make tests, which in this case is simply .json(); + type FetchReturnType = { + json: () => Promise; + text?: () => Promise; + }; + + // The interface options + interface InterceptOptions { + match: (fetchArguments: FetchCallParameters, body?: RequestBodyShape) => RequestBodyShape | null; + assertCallWithin?: number; + // solely used to not lose my mind - helps trace if a match has failed assertions + additionalContext?: unknown; + } + + interface StoredInterception extends InterceptOptions { + resolvePromise: (value: FetchCallParameters | PromiseLike) => void; + rejectPromise: (error: Error) => void; + rejectTimeout?: ReturnType; + } + + const pendingInterceptions = new Set(); + + // Create a stub for this handler, but use interception instead of store-based replacement + const fetchBaseStub = createStubForRef(globalThis['fetch'], { + replacer: (ref) => { + globalThis['fetch'] = ref; + }, + // Implement interception core + interceptor(fetchArguments) { + // Attempt to pre-parse the JSON body for use by interceptors + const fetchOptions = fetchArguments[1]; + const requestBody = typeof fetchOptions?.body === 'string' + ? jsonParseOrNull(fetchOptions.body) + : fetchOptions?.body; + + for (const interceptionRef of pendingInterceptions) { + // check the interception options, determine if it's a match (has return value) + const matchedValue = interceptionRef.match(fetchArguments, requestBody); + + if (matchedValue) { + // remove interceptor! + pendingInterceptions.delete(interceptionRef); + // clear reject timeout (if present) + clearTimeout(interceptionRef.rejectTimeout); + // queue resolve on the event loop + setTimeout(() => interceptionRef.resolvePromise(fetchArguments)); + + return createSimplePromise({ + json: () => createSimplePromise(matchedValue), + }); + } + } + + // We should sometimes throw on missing intercepts + if (fetchStubOptions.throwOnMissingIntercept) { + console.error(fetchArguments); + throw new Error(`TEST ERROR - Fetch interception was not detected, aborting request`); + } + }, + }); + + /** + * Intercepts calls to globalThis.fetch and intercepts the arguments. Allows overriding the response + * using the match option callback parameter. + * @param options + * @returns Promise that resolves with the fetch arguments (if called) + */ + function interceptRequest( + options: InterceptOptions, + ): Promise { + const shouldRejectTimeout = Boolean(options.assertCallWithin); + + // Create a basic interceptorRef + const interceptionDataRef = { + ...options, + } as Partial; + + // When the promise resolves, we want to resolve with request params + const promiseToResolve = new Promise((resolve, reject) => { + // Set the interceptor data by reference + interceptionDataRef.resolvePromise = resolve; + interceptionDataRef.rejectPromise = reject; + + // set timeout if rejectTimeout is set + if (shouldRejectTimeout) { + interceptionDataRef.rejectTimeout = setTimeout(() => { + console.error(interceptionDataRef); + reject(new Error('Maximum wait time exceeded for interceptor (see above)')); + }, (options.assertCallWithin || 0) + 100); + } + }); + + // Add interceptor reference to store + pendingInterceptions.add(interceptionDataRef as StoredInterception); + return promiseToResolve; + } + + interface MatchRequestOptions { + method?: 'GET' | 'POST'; + uri: string | RegExp; + responseBody: (requestBody?: RequestBodyShape) => any; + assertFetchArguments?: (fetchArgs: FetchCallParameters, requestBody?: RequestBodyShape) => void; + assertCallWithin?: InterceptOptions['assertCallWithin']; + } + + /** + * A basic request matching helper - takes a uri as a regexp or string. Allows specifying the responseBody + * @param options + * @returns Promise that resolves with the fetch arguments (if called) + */ + function basicMatchRequest( + options: MatchRequestOptions, + ) { + // Create interceptor + return interceptRequest({ + match([uri, requestOptions], requestBody) { + // Validate request method + const impliedRequestMethod = requestOptions?.method || 'GET'; + if (options.method && options.method !== impliedRequestMethod) { + return null; + } + + // Validate uri argument from fetch + const normalizedUri = typeof uri === 'string' ? uri : uri.toString(); + if (!stringMatchesKey(normalizedUri, options.uri)) { + return null; + } + + // If provided, assert the fetch arguments BEFORE the code execution continues. Allows us to validate + // specific settings (such as json body) were provided + if (options.assertFetchArguments) { + options.assertFetchArguments([uri, requestOptions], requestBody); + } + + // Generate a response body depending on what was passed + return options.responseBody(requestBody); + }, + assertCallWithin: options.assertCallWithin, + additionalContext: { + uri: options.uri, + }, + }); + } + + return { + clear() { + fetchBaseStub.clear(); + }, + cleanup() { + // TODO: If interceptors dangle and don't throw for some reason, this would be where to detect them + fetchBaseStub.cleanup(); + }, + interceptRequest, + basicMatchRequest, + }; +} + +export type StubFetchInstanceType = ReturnType; diff --git a/tests/internal/time.ts b/tests/internal/time.ts new file mode 100644 index 0000000..33fc063 --- /dev/null +++ b/tests/internal/time.ts @@ -0,0 +1,50 @@ +import { FakeTime, FakeTimeInternals } from './deps.ts'; + +export type FixedTimepointInstanceType = ReturnType; + +/** + * Creates a FakeTime instance at a given day, month, and year + * @param day Date in calendar index + * @param month Month in calendar index + * @param year + * @returns + */ +export function createFixedTimepoint(day: number, month: number, year: number) { + // create new date with absolute position (monday) + const baseDate = new Date(year, month - 1, day - 1, 0, 1, 0, 1); + const time = new FakeTime(baseDate); + return time; +} + +/** + * On the current day, moves the FakeTime instance to a specific hour and day + * @param time The FakeTime instance + * @param clockTime 24h string representation of where to place the clock + * @param daysToIncrement If required, increment the clock by a certain amount of days + */ +export function moveTimeToPosition(time: FakeTime, clockTime: string, daysToIncrement?: number) { + // determine the desired hour and minute in 24 hour time + const [hour, minute] = clockTime.split(':').map((item) => parseInt(item, 10)); + + // Use the provided FakeTime instance to get the day and month + const today = new Date(); + + // Use the FakeTime setter instance and set the current time + time.now = Number( + new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + (daysToIncrement || 0), + hour, + minute, + 0, + 1, + ), + ); +} + +// Make sure we have stable references to original Deno internal timer stuff +export const setTimeout = FakeTimeInternals.setTimeout; +export const clearTimeout = FakeTimeInternals.clearTimeout; +export const setInterval = FakeTimeInternals.setInterval; +export const clearInterval = FakeTimeInternals.clearInterval; diff --git a/tests/slack.ts b/tests/slack.ts new file mode 100644 index 0000000..d80b234 --- /dev/null +++ b/tests/slack.ts @@ -0,0 +1,96 @@ +import { StubFetchInstanceType } from './internal/stubs.ts'; +import { shallowClone } from './internal/common.ts'; +import { SlackDndDataSend, SlackProfileDataSend } from '../core/slack.ts'; + +export type SlackActionsApiInstanceType = ReturnType; + +/** + * Create mocked representation of Slack service + * @param fetchStub + */ +export default function createSlackActionsApi(fetchStub: StubFetchInstanceType) { + interface SlackState { + statusEmoji: string | null; + statusExpiration: number | null; + statusText: string; + dndDurationMinutes: number; + } + + const slackState: SlackState = { + statusEmoji: null, + statusExpiration: 0, + statusText: '', + dndDurationMinutes: 0, + }; + + const getProfileState = () => ({ + profile: { + status_emoji: slackState.statusEmoji, + status_expiration: slackState.statusExpiration, + status_text: slackState.statusText, + }, + }); + + return { + getProfileRequest() { + return fetchStub.basicMatchRequest({ + uri: 'https://slack.com/api/users.profile.get', + responseBody: getProfileState, + assertCallWithin: 2000, + }); + }, + updateProfileRequest() { + return fetchStub.basicMatchRequest({ + uri: 'https://slack.com/api/users.profile.set', + method: 'POST', + responseBody: (body) => { + slackState.statusEmoji = body?.profile.status_emoji || null; + slackState.statusExpiration = body?.profile.status_expiration || null; + slackState.statusText = body?.profile.status_text || ''; + return getProfileState(); + }, + assertCallWithin: 2000, + }); + }, + getDndInfoRequest() { + return fetchStub.basicMatchRequest({ + uri: 'https://slack.com/api/dnd.info', + responseBody: () => ({ + num_minutes: slackState.dndDurationMinutes, + snooze_enabled: Boolean(slackState.dndDurationMinutes), + }), + assertCallWithin: 2000, + }); + }, + setDndSnoozeRequest() { + return fetchStub.basicMatchRequest({ + uri: 'https://slack.com/api/dnd.setSnooze', + method: 'POST', + responseBody: (body) => { + slackState.dndDurationMinutes = body?.num_minutes || 0; + return { + ok: true, + }; + }, + assertCallWithin: 2000, + }); + }, + endDndSnoozeRequest() { + return fetchStub.basicMatchRequest({ + uri: 'https://slack.com/api/dnd.endSnooze', + method: 'POST', + responseBody: () => { + slackState.dndDurationMinutes = 0; + return { + ok: true, + }; + }, + assertCallWithin: 2000, + }); + }, + assert(assertCallback: (slackState: SlackState) => void) { + // We want to avoid the temptation to mutate the original ref, so return a shallow clone + assertCallback(shallowClone(slackState)); + }, + }; +}