├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ └── build-code.yml ├── .gitignore ├── .nvmrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── LICENSE-CODE ├── README.md ├── SECURITY.md ├── async-iterators ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── package-lock.json ├── package.json ├── sample.env ├── src │ ├── delete-containers.ts │ ├── loop-by-page.ts │ ├── loop-specific-page.ts │ ├── loop.ts │ └── setup-blob-storage.ts └── tsconfig.json ├── install-nvm.sh ├── node-dependencies ├── .editorconfig ├── .eslintrc.json ├── 3-exercise-package-json │ └── src │ │ └── index.js ├── 3-final-solution-exercise-package-json │ ├── package.json │ └── src │ │ └── index.js ├── 5-exercise-dependency-playwright │ ├── __tests__ │ │ └── address-parser.spec.js │ ├── address-parser.js │ ├── package-lock.json │ └── package.json ├── 5-exercise-dependency │ ├── address-parser.js │ └── package.json ├── 5-final-solution-exercise-dependency │ ├── __tests__ │ │ └── address-parser.spec.js │ ├── address-parser.js │ ├── package-lock.json │ └── package.json ├── 7-exercise-dependency-management │ ├── index.js │ └── package.json ├── package-lock.json └── package.json ├── nodejs-debug ├── .editorconfig ├── .eslintrc.json ├── 4-exercise-built-debugger │ └── fibonacci.js ├── 4-final-solution-exercise-built-debugger │ └── fibonnaci.js ├── 6-exercise-debug-with-vscode │ └── currency.js ├── 6-final-solution-exercise-debug-with-vscode copy │ └── currency.js ├── package-lock.json └── package.json ├── nodejs-files ├── .editorconfig ├── .eslintrc.json ├── 3-exercise-work-file-system │ └── index.js ├── 3-final-solution-exercise-work-file-system │ └── index.js ├── 5-exercise-work-with-paths │ └── index.js ├── 5-final-solution-work-with-paths │ └── index.js ├── 7-exercise-create-files-directories │ └── index.js ├── 7-final-solution-exercise-create-files-directories │ └── index.js ├── 9-exercise-read-write-files │ └── index.js ├── 9-final-solution-exercise-read-write-files │ └── index.js ├── package-lock.json ├── package.json └── stores │ ├── 201 │ └── sales.json │ ├── 202 │ └── sales.json │ ├── 203 │ └── sales.json │ └── 204 │ └── sales.json ├── nodejs-http ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── 3-final-solution-exercise-express-web-app │ ├── app.js │ ├── package-lock.json │ └── package.json ├── 5-final-solution-exercise-express-middleware │ ├── app.js │ ├── client.js │ ├── package-lock.json │ └── package.json ├── exercise-express-middleware │ ├── app.js │ ├── client.js │ ├── package-lock.json │ └── package.json ├── package-lock.json └── package.json ├── nodejs-intro-esm ├── .editorconfig ├── .eslintrc.json ├── 5-try │ ├── index-1.js │ └── top-level-async-await.js ├── README.md ├── package-lock.json └── package.json ├── nodejs-intro ├── .editorconfig ├── .eslintrc.json ├── 3-how-nodejs-works │ ├── async-await-top-level.js │ ├── async-await.js │ ├── callback-anonymous.js │ ├── callbacks.js │ ├── file.txt │ ├── nested-callback.js │ ├── promise-call.js │ ├── promise-create-basic.js │ ├── promise-create.js │ ├── promises.js │ └── synchronous-api.js ├── README.md ├── package-lock.json ├── package.json └── resources │ └── nodejs-intro-01.gif ├── nodejs-route ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── 3-final-solution-exercise-express-routing │ └── parameters │ │ ├── app.js │ │ ├── package-lock.json │ │ └── package.json ├── 5-final-solution-exercise-read-write │ └── reading-writing │ │ ├── app.js │ │ ├── client-delete-route.js │ │ ├── client-delete.js │ │ ├── client-get.js │ │ ├── client-post.js │ │ ├── client-put.js │ │ ├── package-lock.json │ │ └── package.json ├── exercise-express-routing │ ├── parameters │ │ ├── app.js │ │ ├── package-lock.json │ │ └── package.json │ └── reading-writing │ │ ├── app.js │ │ ├── client-delete-route.js │ │ ├── client-delete.js │ │ ├── client-get.js │ │ ├── client-post.js │ │ ├── client-put.js │ │ ├── package-lock.json │ │ └── package.json ├── package-lock.json └── package.json ├── package-lock.json ├── package.json ├── resources └── nodejs-learn-path.png ├── scripts └── create-cosmos-db-resources.sh ├── test-with-jest ├── .eslintrc.json ├── .gitignore ├── README.md ├── package.json ├── sample.env ├── src │ ├── fakes │ │ └── fake-in-mem-db.spec.ts │ ├── mock-function │ │ ├── data │ │ │ ├── connect-to-cosmos.ts │ │ │ ├── fake-data.ts │ │ │ ├── model.ts │ │ │ └── verify.ts │ │ ├── index.ts │ │ └── lib │ │ │ ├── insert.spec.ts │ │ │ └── insert.ts │ └── test-boilerplate │ │ ├── boilerplate-with-mock.spec.ts │ │ └── boilerplate.spec.ts └── tsconfig.json ├── test-with-node-testrunner ├── .eslintrc.json ├── README.md ├── package.json ├── src │ ├── data │ │ ├── connect-to-cosmos.ts │ │ ├── fake-data.ts │ │ ├── model.ts │ │ └── verify.ts │ ├── index.ts │ └── lib │ │ └── insert.ts ├── test │ ├── 01-spies.test.ts │ ├── 02-stubs.test.ts │ ├── boilerplate-with-mock.test.ts │ ├── boilerplate.test.ts │ ├── fake-in-mem-db.test.ts │ └── insert.test.ts └── tsconfig.json ├── test-with-vitest ├── .eslintrc.json ├── README.md ├── package.json ├── src │ ├── data │ │ ├── connect-to-cosmos.ts │ │ ├── fake-data.ts │ │ ├── model.ts │ │ └── verify.ts │ ├── index.ts │ └── lib │ │ └── insert.ts ├── tests │ ├── 01-spies.test.ts │ ├── 02-stubs.test.ts │ ├── boilerplate-with-mock.test.ts │ ├── boilerplate.test.ts │ ├── fake-in-mem-db.test.ts │ └── insert.test.ts ├── tsconfig.json └── vitest.config.ts └── unit-testing ├── .eslintrc.json ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── sample.env ├── scripts └── create-resources.sh ├── src ├── fakes │ └── fake-in-mem-db.spec.ts ├── mock-function │ ├── data │ │ ├── connect-to-cosmos.ts │ │ ├── fake-data.ts │ │ ├── model.ts │ │ └── verify.ts │ ├── index.ts │ └── lib │ │ ├── insert.spec.ts │ │ └── insert.ts └── test-boilerplate │ ├── boilerplate-with-mock.spec.ts │ └── boilerplate.spec.ts └── tsconfig.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node 3 | { 4 | "name": "Node.js", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/javascript-node:1-22-bullseye", 7 | "features": { 8 | "ghcr.io/devcontainers-contrib/features/npm-package:1": {}, 9 | "ghcr.io/devcontainers/features/azure-cli:1": {} 10 | }, 11 | "customizations": { 12 | "vscode": { 13 | "extensions": [ 14 | "jasonnutter.search-node-modules", 15 | "dbaeumer.vscode-eslint", 16 | "eg2.vscode-npm-script", 17 | "christian-kohler.npm-intellisense", 18 | "GitHub.copilot", 19 | "GitHub.copilot-chat", 20 | "xabikos.JavaScriptSnippets", 21 | "redhat.vscode-yaml", 22 | "github.vscode-github-actions" 23 | ] 24 | } 25 | }, 26 | 27 | // Features to add to the dev container. More info: https://containers.dev/features. 28 | // "features": {}, 29 | 30 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 31 | // "forwardPorts": [], 32 | 33 | // Use 'postCreateCommand' to run commands after the container is created. 34 | "postCreateCommand": "npm install -g typescript" 35 | 36 | // Configure tool-specific properties. 37 | // "customizations": {}, 38 | 39 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 40 | // "remoteUser": "root" 41 | } 42 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = auto 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "nodejs-intro" 5 | schedule: 6 | interval: "weekly" 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: "*" 10 | update-types: ["version-update:semver-patch"] 11 | 12 | - package-ecosystem: "npm" 13 | directory: "nodejs-dependencies" 14 | schedule: 15 | interval: "weekly" 16 | open-pull-requests-limit: 10 17 | ignore: 18 | - dependency-name: "*" 19 | update-types: ["version-update:semver-patch"] 20 | 21 | - package-ecosystem: "npm" 22 | directory: "nodejs-debug" 23 | schedule: 24 | interval: "weekly" 25 | open-pull-requests-limit: 10 26 | ignore: 27 | - dependency-name: "*" 28 | update-types: ["version-update:semver-patch"] 29 | 30 | - package-ecosystem: "npm" 31 | directory: "nodejs-http/exercise-express-middleware" 32 | schedule: 33 | interval: "weekly" 34 | open-pull-requests-limit: 10 35 | ignore: 36 | - dependency-name: "*" 37 | update-types: ["version-update:semver-patch"] 38 | 39 | - package-ecosystem: "npm" 40 | directory: "nodejs-http/exercise-express-routing/parameters" 41 | schedule: 42 | interval: "weekly" 43 | open-pull-requests-limit: 10 44 | ignore: 45 | - dependency-name: "*" 46 | update-types: ["version-update:semver-patch"] 47 | 48 | - package-ecosystem: "npm" 49 | directory: "nodejs-http/exercise-express-routing/reading-writing" 50 | schedule: 51 | interval: "weekly" 52 | open-pull-requests-limit: 10 53 | ignore: 54 | - dependency-name: "*" 55 | update-types: ["version-update:semver-patch"] 56 | 57 | - package-ecosystem: "npm" 58 | directory: "nodejs-files" 59 | schedule: 60 | interval: "weekly" 61 | open-pull-requests-limit: 10 62 | ignore: 63 | - dependency-name: "*" 64 | update-types: ["version-update:semver-patch"] 65 | 66 | - package-ecosystem: "npm" 67 | directory: "nodejs-http" 68 | schedule: 69 | interval: "weekly" 70 | open-pull-requests-limit: 10 71 | ignore: 72 | - dependency-name: "*" 73 | update-types: ["version-update:semver-patch"] 74 | 75 | - package-ecosystem: "npm" 76 | directory: "nodejs-http/3-final-solution-exercise-express-web-app" 77 | schedule: 78 | interval: "weekly" 79 | open-pull-requests-limit: 10 80 | ignore: 81 | - dependency-name: "*" 82 | update-types: ["version-update:semver-patch"] 83 | 84 | - package-ecosystem: "npm" 85 | directory: "nodejs-http/5-final-solution-exercise-express-middleware" 86 | schedule: 87 | interval: "weekly" 88 | open-pull-requests-limit: 10 89 | ignore: 90 | - dependency-name: "*" 91 | update-types: ["version-update:semver-patch"] 92 | 93 | - package-ecosystem: "npm" 94 | directory: "nodejs-route/exercise-express-routing" 95 | schedule: 96 | interval: "weekly" 97 | open-pull-requests-limit: 10 98 | ignore: 99 | - dependency-name: "*" 100 | update-types: ["version-update:semver-patch"] 101 | 102 | - package-ecosystem: "npm" 103 | directory: "nodejs-route/exercise-express-routing/parameters" 104 | schedule: 105 | interval: "weekly" 106 | open-pull-requests-limit: 10 107 | ignore: 108 | - dependency-name: "*" 109 | update-types: ["version-update:semver-patch"] 110 | 111 | - package-ecosystem: "npm" 112 | directory: "nodejs-route/exercise-express-routing/reading-writing" 113 | schedule: 114 | interval: "weekly" 115 | open-pull-requests-limit: 10 116 | ignore: 117 | - dependency-name: "*" 118 | update-types: ["version-update:semver-patch"] 119 | 120 | - package-ecosystem: "npm" 121 | directory: "nodejs-route/3-final-solution-exercise-express-routing" 122 | schedule: 123 | interval: "weekly" 124 | open-pull-requests-limit: 10 125 | ignore: 126 | - dependency-name: "*" 127 | update-types: ["version-update:semver-patch"] 128 | 129 | - package-ecosystem: "npm" 130 | directory: "nodejs-route/3-final-solution-exercise-express-routing/parameters" 131 | schedule: 132 | interval: "weekly" 133 | open-pull-requests-limit: 10 134 | ignore: 135 | - dependency-name: "*" 136 | update-types: ["version-update:semver-patch"] -------------------------------------------------------------------------------- /.github/workflows/build-code.yml: -------------------------------------------------------------------------------- 1 | name: Format Lint 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | branches: [main] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | permissions: 18 | contents: read # Job-level permissions 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | matrix: 23 | node-version: [18.x] 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | 32 | - name: Install Prettier 33 | run: | 34 | npm install -g prettier 35 | - name: Module 1 - nodejs-intro 36 | run: | 37 | cd nodejs-intro 38 | npm install 39 | npm run format 40 | npm run lint 41 | - name: Module 1 - nodejs-intro-esm 42 | run: | 43 | cd nodejs-intro-esm 44 | npm install 45 | npm run format 46 | npm run lint 47 | 48 | - name: Module 2 - node-dependencies 49 | run: | 50 | cd node-dependencies 51 | npm install 52 | npm run format 53 | npm run lint 54 | 55 | - name: Module 3 - nodejs-debug 56 | run: | 57 | cd node-dependencies 58 | npm install 59 | npm run format 60 | npm run lint 61 | 62 | - name: Module 4 - nodejs-files 63 | run: | 64 | cd nodejs-files 65 | npm install 66 | npm run format 67 | npm run lint 68 | 69 | - name: Module 5 - nodejs-http 70 | run: | 71 | cd nodejs-http 72 | npm install 73 | npm run format 74 | npm run lint 75 | 76 | - name: Module 6 - nodejs-route 77 | run: | 78 | cd nodejs-route 79 | npm install 80 | npm run format 81 | npm run lint 82 | 83 | - name: Docs - async iterators 84 | # In JS Dev Center 85 | run: | 86 | cd async-iterators 87 | npm install 88 | npm run format 89 | npm run lint -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | .env* 4 | dist 5 | **/*.env 6 | **/coverage/ 7 | 8 | 9 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.12.0 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More_considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution 4.0 International Public License 58 | 59 | By exercising the Licensed Rights (defined below), You accept and agree 60 | to be bound by the terms and conditions of this Creative Commons 61 | Attribution 4.0 International Public License ("Public License"). To the 62 | extent this Public License may be interpreted as a contract, You are 63 | granted the Licensed Rights in consideration of Your acceptance of 64 | these terms and conditions, and the Licensor grants You such rights in 65 | consideration of benefits the Licensor receives from making the 66 | Licensed Material available under these terms and conditions. 67 | 68 | 69 | Section 1 -- Definitions. 70 | 71 | a. Adapted Material means material subject to Copyright and Similar 72 | Rights that is derived from or based upon the Licensed Material 73 | and in which the Licensed Material is translated, altered, 74 | arranged, transformed, or otherwise modified in a manner requiring 75 | permission under the Copyright and Similar Rights held by the 76 | Licensor. For purposes of this Public License, where the Licensed 77 | Material is a musical work, performance, or sound recording, 78 | Adapted Material is always produced where the Licensed Material is 79 | synched in timed relation with a moving image. 80 | 81 | b. Adapter's License means the license You apply to Your Copyright 82 | and Similar Rights in Your contributions to Adapted Material in 83 | accordance with the terms and conditions of this Public License. 84 | 85 | c. Copyright and Similar Rights means copyright and/or similar rights 86 | closely related to copyright including, without limitation, 87 | performance, broadcast, sound recording, and Sui Generis Database 88 | Rights, without regard to how the rights are labeled or 89 | categorized. For purposes of this Public License, the rights 90 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 91 | Rights. 92 | 93 | d. Effective Technological Measures means those measures that, in the 94 | absence of proper authority, may not be circumvented under laws 95 | fulfilling obligations under Article 11 of the WIPO Copyright 96 | Treaty adopted on December 20, 1996, and/or similar international 97 | agreements. 98 | 99 | e. Exceptions and Limitations means fair use, fair dealing, and/or 100 | any other exception or limitation to Copyright and Similar Rights 101 | that applies to Your use of the Licensed Material. 102 | 103 | f. Licensed Material means the artistic or literary work, database, 104 | or other material to which the Licensor applied this Public 105 | License. 106 | 107 | g. Licensed Rights means the rights granted to You subject to the 108 | terms and conditions of this Public License, which are limited to 109 | all Copyright and Similar Rights that apply to Your use of the 110 | Licensed Material and that the Licensor has authority to license. 111 | 112 | h. Licensor means the individual(s) or entity(ies) granting rights 113 | under this Public License. 114 | 115 | i. Share means to provide material to the public by any means or 116 | process that requires permission under the Licensed Rights, such 117 | as reproduction, public display, public performance, distribution, 118 | dissemination, communication, or importation, and to make material 119 | available to the public including in ways that members of the 120 | public may access the material from a place and at a time 121 | individually chosen by them. 122 | 123 | j. Sui Generis Database Rights means rights other than copyright 124 | resulting from Directive 96/9/EC of the European Parliament and of 125 | the Council of 11 March 1996 on the legal protection of databases, 126 | as amended and/or succeeded, as well as other essentially 127 | equivalent rights anywhere in the world. 128 | 129 | k. You means the individual or entity exercising the Licensed Rights 130 | under this Public License. Your has a corresponding meaning. 131 | 132 | 133 | Section 2 -- Scope. 134 | 135 | a. License grant. 136 | 137 | 1. Subject to the terms and conditions of this Public License, 138 | the Licensor hereby grants You a worldwide, royalty-free, 139 | non-sublicensable, non-exclusive, irrevocable license to 140 | exercise the Licensed Rights in the Licensed Material to: 141 | 142 | a. reproduce and Share the Licensed Material, in whole or 143 | in part; and 144 | 145 | b. produce, reproduce, and Share Adapted Material. 146 | 147 | 2. Exceptions and Limitations. For the avoidance of doubt, where 148 | Exceptions and Limitations apply to Your use, this Public 149 | License does not apply, and You do not need to comply with 150 | its terms and conditions. 151 | 152 | 3. Term. The term of this Public License is specified in Section 153 | 6(a). 154 | 155 | 4. Media and formats; technical modifications allowed. The 156 | Licensor authorizes You to exercise the Licensed Rights in 157 | all media and formats whether now known or hereafter created, 158 | and to make technical modifications necessary to do so. The 159 | Licensor waives and/or agrees not to assert any right or 160 | authority to forbid You from making technical modifications 161 | necessary to exercise the Licensed Rights, including 162 | technical modifications necessary to circumvent Effective 163 | Technological Measures. For purposes of this Public License, 164 | simply making modifications authorized by this Section 2(a) 165 | (4) never produces Adapted Material. 166 | 167 | 5. Downstream recipients. 168 | 169 | a. Offer from the Licensor -- Licensed Material. Every 170 | recipient of the Licensed Material automatically 171 | receives an offer from the Licensor to exercise the 172 | Licensed Rights under the terms and conditions of this 173 | Public License. 174 | 175 | b. No downstream restrictions. You may not offer or impose 176 | any additional or different terms or conditions on, or 177 | apply any Effective Technological Measures to, the 178 | Licensed Material if doing so restricts exercise of the 179 | Licensed Rights by any recipient of the Licensed 180 | Material. 181 | 182 | 6. No endorsement. Nothing in this Public License constitutes or 183 | may be construed as permission to assert or imply that You 184 | are, or that Your use of the Licensed Material is, connected 185 | with, or sponsored, endorsed, or granted official status by, 186 | the Licensor or others designated to receive attribution as 187 | provided in Section 3(a)(1)(A)(i). 188 | 189 | b. Other rights. 190 | 191 | 1. Moral rights, such as the right of integrity, are not 192 | licensed under this Public License, nor are publicity, 193 | privacy, and/or other similar personality rights; however, to 194 | the extent possible, the Licensor waives and/or agrees not to 195 | assert any such rights held by the Licensor to the limited 196 | extent necessary to allow You to exercise the Licensed 197 | Rights, but not otherwise. 198 | 199 | 2. Patent and trademark rights are not licensed under this 200 | Public License. 201 | 202 | 3. To the extent possible, the Licensor waives any right to 203 | collect royalties from You for the exercise of the Licensed 204 | Rights, whether directly or through a collecting society 205 | under any voluntary or waivable statutory or compulsory 206 | licensing scheme. In all other cases the Licensor expressly 207 | reserves any right to collect such royalties. 208 | 209 | 210 | Section 3 -- License Conditions. 211 | 212 | Your exercise of the Licensed Rights is expressly made subject to the 213 | following conditions. 214 | 215 | a. Attribution. 216 | 217 | 1. If You Share the Licensed Material (including in modified 218 | form), You must: 219 | 220 | a. retain the following if it is supplied by the Licensor 221 | with the Licensed Material: 222 | 223 | i. identification of the creator(s) of the Licensed 224 | Material and any others designated to receive 225 | attribution, in any reasonable manner requested by 226 | the Licensor (including by pseudonym if 227 | designated); 228 | 229 | ii. a copyright notice; 230 | 231 | iii. a notice that refers to this Public License; 232 | 233 | iv. a notice that refers to the disclaimer of 234 | warranties; 235 | 236 | v. a URI or hyperlink to the Licensed Material to the 237 | extent reasonably practicable; 238 | 239 | b. indicate if You modified the Licensed Material and 240 | retain an indication of any previous modifications; and 241 | 242 | c. indicate the Licensed Material is licensed under this 243 | Public License, and include the text of, or the URI or 244 | hyperlink to, this Public License. 245 | 246 | 2. You may satisfy the conditions in Section 3(a)(1) in any 247 | reasonable manner based on the medium, means, and context in 248 | which You Share the Licensed Material. For example, it may be 249 | reasonable to satisfy the conditions by providing a URI or 250 | hyperlink to a resource that includes the required 251 | information. 252 | 253 | 3. If requested by the Licensor, You must remove any of the 254 | information required by Section 3(a)(1)(A) to the extent 255 | reasonably practicable. 256 | 257 | 4. If You Share Adapted Material You produce, the Adapter's 258 | License You apply must not prevent recipients of the Adapted 259 | Material from complying with this Public License. 260 | 261 | 262 | Section 4 -- Sui Generis Database Rights. 263 | 264 | Where the Licensed Rights include Sui Generis Database Rights that 265 | apply to Your use of the Licensed Material: 266 | 267 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 268 | to extract, reuse, reproduce, and Share all or a substantial 269 | portion of the contents of the database; 270 | 271 | b. if You include all or a substantial portion of the database 272 | contents in a database in which You have Sui Generis Database 273 | Rights, then the database in which You have Sui Generis Database 274 | Rights (but not its individual contents) is Adapted Material; and 275 | 276 | c. You must comply with the conditions in Section 3(a) if You Share 277 | all or a substantial portion of the contents of the database. 278 | 279 | For the avoidance of doubt, this Section 4 supplements and does not 280 | replace Your obligations under this Public License where the Licensed 281 | Rights include other Copyright and Similar Rights. 282 | 283 | 284 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 285 | 286 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 287 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 288 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 289 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 290 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 291 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 292 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 293 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 294 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 295 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 296 | 297 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 298 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 299 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 300 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 301 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 302 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 303 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 304 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 305 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 306 | 307 | c. The disclaimer of warranties and limitation of liability provided 308 | above shall be interpreted in a manner that, to the extent 309 | possible, most closely approximates an absolute disclaimer and 310 | waiver of all liability. 311 | 312 | 313 | Section 6 -- Term and Termination. 314 | 315 | a. This Public License applies for the term of the Copyright and 316 | Similar Rights licensed here. However, if You fail to comply with 317 | this Public License, then Your rights under this Public License 318 | terminate automatically. 319 | 320 | b. Where Your right to use the Licensed Material has terminated under 321 | Section 6(a), it reinstates: 322 | 323 | 1. automatically as of the date the violation is cured, provided 324 | it is cured within 30 days of Your discovery of the 325 | violation; or 326 | 327 | 2. upon express reinstatement by the Licensor. 328 | 329 | For the avoidance of doubt, this Section 6(b) does not affect any 330 | right the Licensor may have to seek remedies for Your violations 331 | of this Public License. 332 | 333 | c. For the avoidance of doubt, the Licensor may also offer the 334 | Licensed Material under separate terms or conditions or stop 335 | distributing the Licensed Material at any time; however, doing so 336 | will not terminate this Public License. 337 | 338 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 339 | License. 340 | 341 | 342 | Section 7 -- Other Terms and Conditions. 343 | 344 | a. The Licensor shall not be bound by any additional or different 345 | terms or conditions communicated by You unless expressly agreed. 346 | 347 | b. Any arrangements, understandings, or agreements regarding the 348 | Licensed Material not stated herein are separate from and 349 | independent of the terms and conditions of this Public License. 350 | 351 | 352 | Section 8 -- Interpretation. 353 | 354 | a. For the avoidance of doubt, this Public License does not, and 355 | shall not be interpreted to, reduce, limit, restrict, or impose 356 | conditions on any use of the Licensed Material that could lawfully 357 | be made without permission under this Public License. 358 | 359 | b. To the extent possible, if any provision of this Public License is 360 | deemed unenforceable, it shall be automatically reformed to the 361 | minimum extent necessary to make it enforceable. If the provision 362 | cannot be reformed, it shall be severed from this Public License 363 | without affecting the enforceability of the remaining terms and 364 | conditions. 365 | 366 | c. No term or condition of this Public License will be waived and no 367 | failure to comply consented to unless expressly agreed to by the 368 | Licensor. 369 | 370 | d. Nothing in this Public License constitutes or may be interpreted 371 | as a limitation upon, or waiver of, any privileges and immunities 372 | that apply to the Licensor or You, including from the legal 373 | processes of any jurisdiction or authority. 374 | 375 | 376 | ======================================================================= 377 | 378 | Creative Commons is not a party to its public 379 | licenses. Notwithstanding, Creative Commons may elect to apply one of 380 | its public licenses to material it publishes and in those instances 381 | will be considered the “Licensor.” The text of the Creative Commons 382 | public licenses is dedicated to the public domain under the CC0 Public 383 | Domain Dedication. Except for the limited purpose of indicating that 384 | material is shared under a Creative Commons public license or as 385 | otherwise permitted by the Creative Commons policies published at 386 | creativecommons.org/policies, Creative Commons does not authorize the 387 | use of the trademark "Creative Commons" or any other trademark or logo 388 | of Creative Commons without its prior written consent including, 389 | without limitation, in connection with any unauthorized modifications 390 | to any of its public licenses or any other arrangements, 391 | understandings, or agreements concerning use of licensed material. For 392 | the avoidance of doubt, this paragraph does not form part of the 393 | public licenses. 394 | 395 | Creative Commons may be contacted at creativecommons.org. -------------------------------------------------------------------------------- /LICENSE-CODE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | name: Build JavaScript applications with Node.js 4 | languages: 5 | - javascript 6 | products: 7 | - azure 8 | - vs-code 9 | --- 10 | # Node essentials 11 | 12 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MicrosoftDocs/node-essentials) 13 | 14 | [![Node.Js Learn Path](/resources/nodejs-learn-path.png)](https://learn.microsoft.com/training/paths/build-javascript-applications-nodejs/?WT.mc_id=javascript-111027-gllemos) 15 | 16 | > This repository contains the source code for tutorials proposed in the [Node.js Learning Path](https://docs.microsoft.com/learn/paths/build-javascript-applications-nodejs/?WT.mc_id=nodebeginner-github-cxa) and the [Beginner's video Series to Node.js](https://channel9.msdn.com/Series/Beginners-Series-to-NodeJS?WT.mc_id=javascript-111027-gllemos). 17 | 18 | ## 🎯 Overview 19 | 20 | Learning any new technology is a time-consuming process where it's easy to get lost. This is why we created this series of practical, and focused modules about Node.js for beginners so you can get up to speed. 21 | 22 | You'll find all the source code used in the [Learn modules](https://docs.microsoft.com/learn/paths/build-javascript-applications-nodejs/?WT.mc_id=javascript-111027-gllemos) and [videos](https://channel9.msdn.com/Series/Beginners-Series-to-NodeJS?WT.mc_id=javascript-111027-gllemos) to help you during your learning journey. 23 | 24 | The full banking API source code shown in the Express videos can be found here: [WebDev for Beginners - Bank project](https://github.com/WebDev-Beginners/bank-project/tree/main/api) 25 | 26 | And if you need to learn or improve your JavaScript skills, take a look at the [Beginner's video Series to JavaScript](https://channel9.msdn.com/Shows/Beginners-Series-to-JavaScript?WT.mc_id=javascript-111027-gllemos). 27 | 28 | ## 📚 Next steps 29 | 30 | Because learning is a never-ending journey, we want to help you as much as we can to get you ready for what's coming next. You'll find here a great collection of resources you can use to build your knowledge. 31 | 32 | - ✅ **[Build a Node.js app for Azure Cosmos DB in Visual Studio Code](https://docs.microsoft.com/learn/modules/build-node-cosmos-app-vscode/?WT.mc_id=javascript-111027-gllemos)** 33 | 34 | - ✅ **[Automate Node.js deployments with Azure Pipelines](https://docs.microsoft.com/learn/modules/deploy-nodejs/?WT.mc_id=javascript-111027-gllemos)** 35 | 36 | - ✅ **[Refactor Node.js and Express APIs to serverless APIs with Azure Functions](https://docs.microsoft.com/learn/modules/shift-nodejs-express-apis-serverless/?WT.mc_id=javascript-111027-gllemos)** 37 | 38 | - ✅ **[Build and run a web application with the MEAN stack on an Azure Linux virtual machine](https://docs.microsoft.com/learn/modules/build-a-web-app-with-mean-on-a-linux-vm/?WT.mc_id=javascript-111027-gllemos)** 39 | 40 | - ✅ **[Publish an Angular, React, Svelte, or Vue JavaScript app with Azure Static Web Apps](https://docs.microsoft.com/learn/modules/publish-app-service-static-web-app-api/?WT.mc_id=javascript-111027-gllemos)** 41 | 42 | - ✅ **[Quickstart: Create an image classification project with the Custom Vision client library](https://docs.microsoft.com/azure/cognitive-services/custom-vision-service/quickstarts/image-classification?WT.mc_id=javascript-111027-gllemos)** 43 | 44 | - ✅ **[Create a bot with the Bot Framework SDK for JavaScript](https://docs.microsoft.com/azure/bot-service/javascript/bot-builder-javascript-quickstart?WT.mc_id=javascript-111027-gllemos)** 45 | 46 | ## 💻 Contributing 47 | 48 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 49 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 50 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 51 | 52 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 53 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 54 | provided by the bot. You will only need to do this once across all repos using our CLA. 55 | 56 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 57 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 58 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 59 | 60 | ## ⚖️Legal Notices 61 | 62 | Microsoft and any contributors grant you a license to the Microsoft documentation and other content 63 | in this repository under the [Creative Commons Attribution 4.0 International Public License](https://creativecommons.org/licenses/by/4.0/legalcode), 64 | see the [LICENSE](LICENSE) file, and grant you a license to any code in the repository under the [MIT License](https://opensource.org/licenses/MIT), see the 65 | [LICENSE-CODE](LICENSE-CODE) file. 66 | 67 | Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation 68 | may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. 69 | The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. 70 | Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653. 71 | 72 | Privacy information can be found at https://privacy.microsoft.com/en-us/ 73 | 74 | Microsoft and any contributors reserve all other rights, whether under their respective copyrights, patents, 75 | or trademarks, whether by implication, estoppel or otherwise. 76 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /async-iterators/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = auto 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single -------------------------------------------------------------------------------- /async-iterators/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 2021, 13 | "project": "./tsconfig.json" 14 | }, 15 | "plugins": [ 16 | "jest", 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | "no-console": "off", 21 | "no-noImplicitAny": "off", 22 | "require-await": "error", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/no-unsafe-assignment": "off", 25 | "@typescript-eslint/no-unsafe-argument": "off", 26 | "@typescript-eslint/no-unsafe-member-access": "off", 27 | "@typescript-eslint/unbound-method": "off", 28 | "@typescript-eslint/no-floating-promises": "error" 29 | } 30 | } -------------------------------------------------------------------------------- /async-iterators/.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | dist 4 | .env* -------------------------------------------------------------------------------- /async-iterators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-iterators", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "clear": "rm -rf dist && rm -rf coverage", 7 | "start": "node dist/index.js", 8 | "test:coverage": "jest dist --coverage", 9 | "build": "npm run clear && tsc", 10 | "lint": "eslint \"./**/*.ts\" --fix", 11 | "format": "prettier \"./**/*.ts\" --write", 12 | "precommit": "npm run format && git add . && npm run lint" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "description": "", 17 | "dependencies": { 18 | "@azure/identity": "^4.4.1", 19 | "@azure/storage-blob": "^12.24.0", 20 | "@types/node": "^22.4.1", 21 | "dotenv": "^16.4.5", 22 | "prettier": "^3.3.3", 23 | "uuid": "^10.0.0" 24 | }, 25 | "devDependencies": { 26 | "@types/uuid": "^10.0.0", 27 | "typescript": "^5.5.4", 28 | "@typescript-eslint/eslint-plugin": "^8.2.0", 29 | "eslint": "^8.56.0", 30 | "eslint-config-airbnb-base": "^15.0.0", 31 | "eslint-plugin-import": "^2.29.1", 32 | "eslint-plugin-jest": "^28.8.0", 33 | "husky": "^9.0.10" 34 | }, 35 | "husky": { 36 | "hooks": { 37 | "pre-commit": "npm run precommit" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /async-iterators/sample.env: -------------------------------------------------------------------------------- 1 | AZURE_STORAGE_ACCOUNT_NAME=iteratedfberry 2 | -------------------------------------------------------------------------------- /async-iterators/src/delete-containers.ts: -------------------------------------------------------------------------------- 1 | import { BlobServiceClient } from '@azure/storage-blob'; 2 | 3 | export async function deleteContainers( 4 | blobServiceClient: BlobServiceClient, 5 | ): Promise { 6 | // delete all containers 7 | for await (const container of blobServiceClient.listContainers()) { 8 | const containerClient = blobServiceClient.getContainerClient( 9 | container.name, 10 | ); 11 | await containerClient.delete(); 12 | console.log(`Deleted container: ${container.name}`); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /async-iterators/src/loop-by-page.ts: -------------------------------------------------------------------------------- 1 | import { deleteContainers } from './delete-containers'; 2 | import { setup } from './setup-blob-storage'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import 'dotenv/config'; 5 | 6 | async function main(): Promise { 7 | const blobServiceClient = setup(); 8 | 9 | const containerMax = 10; 10 | const containerNames: string[] = []; 11 | 12 | for (let i = 0; i < containerMax; i++) { 13 | containerNames.push(`container-${uuidv4()}`); 14 | } 15 | 16 | for (const name of containerNames) { 17 | const containerClient = blobServiceClient.getContainerClient(name); 18 | await containerClient.create(); 19 | console.log(`Created container: ${name}`); 20 | } 21 | 22 | const maxPageSize = 3; 23 | // The iterator also supports iteration by page with a configurable (and optional) `maxPageSize` setting. 24 | for await (const response of blobServiceClient.listContainers().byPage({ 25 | maxPageSize, 26 | })) { 27 | if (response.containerItems) { 28 | for (const container of response.containerItems) { 29 | console.log(`Container: ${container.name}`); 30 | } 31 | } 32 | } 33 | 34 | await deleteContainers(blobServiceClient); 35 | } 36 | 37 | main() 38 | .then(() => console.log('done')) 39 | .catch((error) => { 40 | console.error(error); 41 | process.exit(1); 42 | }); 43 | -------------------------------------------------------------------------------- /async-iterators/src/loop-specific-page.ts: -------------------------------------------------------------------------------- 1 | import { BlobServiceClient, ContainerClient } from '@azure/storage-blob'; 2 | import { DefaultAzureCredential } from '@azure/identity'; 3 | import { deleteContainers } from './delete-containers'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | import 'dotenv/config'; 6 | 7 | async function setup(): Promise<{ 8 | blobServiceClient: BlobServiceClient; 9 | containerClient: ContainerClient; 10 | }> { 11 | const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; 12 | if (!accountName) throw Error('Azure Storage accountName not found'); 13 | 14 | const blobServiceClient = new BlobServiceClient( 15 | `https://${accountName}.blob.core.windows.net`, 16 | new DefaultAzureCredential(), 17 | ); 18 | 19 | const containerName = `container-${uuidv4()}`; 20 | 21 | const containerClient = blobServiceClient.getContainerClient(containerName); 22 | await containerClient.create(); 23 | console.log(`Created container: ${containerClient.containerName}`); 24 | 25 | const blobMax = 10; 26 | 27 | for (let i = 0; i < blobMax; i++) { 28 | const blockBlobClient = containerClient.getBlockBlobClient( 29 | `blob-${uuidv4()}`, 30 | ); 31 | await blockBlobClient.upload('Hello world', 11); 32 | console.log(`Uploaded block blob: ${blockBlobClient.name}`); 33 | } 34 | 35 | return { blobServiceClient, containerClient }; 36 | } 37 | 38 | async function main(): Promise { 39 | const { blobServiceClient, containerClient } = await setup(); 40 | 41 | const maxPageSize = 3; 42 | 43 | // Create iterator 44 | const iter = containerClient.listBlobsFlat().byPage({ maxPageSize }); 45 | let pageNumber = 1; 46 | 47 | const result = await iter.next(); 48 | if (result.done) { 49 | throw new Error('Expected at least one page of results.'); 50 | } 51 | 52 | const continuationToken = result.value.continuationToken; 53 | if (!continuationToken) { 54 | throw new Error( 55 | 'Expected a continuation token from the blob service, but one was not returned.', 56 | ); 57 | } 58 | 59 | // Continue with iterator 60 | const resumed = containerClient 61 | .listBlobsFlat() 62 | .byPage({ continuationToken, maxPageSize }); 63 | pageNumber = 2; 64 | for await (const page of resumed) { 65 | console.log(`- Page ${pageNumber++}:`); 66 | for (const blob of page.segment.blobItems) { 67 | console.log(` - ${blob.name}`); 68 | } 69 | } 70 | 71 | await deleteContainers(blobServiceClient); 72 | } 73 | 74 | main() 75 | .then(() => console.log('done')) 76 | .catch((error) => { 77 | console.error(error); 78 | process.exit(1); 79 | }); 80 | -------------------------------------------------------------------------------- /async-iterators/src/loop.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { deleteContainers } from './delete-containers'; 3 | import { setup } from './setup-blob-storage'; 4 | import 'dotenv/config'; 5 | 6 | async function main(): Promise { 7 | const blobServiceClient = setup(); 8 | 9 | const containerMax = 10; 10 | 11 | for (let i = 0; i < containerMax; i++) { 12 | const containerClient = blobServiceClient.getContainerClient( 13 | `container-${uuidv4()}`, 14 | ); 15 | await containerClient.create(); 16 | console.log(`Created container: ${containerClient.containerName}`); 17 | } 18 | 19 | for await (const container of blobServiceClient.listContainers()) { 20 | console.log(`Container: ${container.name}`); 21 | } 22 | 23 | await deleteContainers(blobServiceClient); 24 | } 25 | 26 | main() 27 | .then(() => console.log('done')) 28 | .catch((error) => { 29 | console.error(error); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /async-iterators/src/setup-blob-storage.ts: -------------------------------------------------------------------------------- 1 | import { BlobServiceClient } from '@azure/storage-blob'; 2 | import { DefaultAzureCredential } from '@azure/identity'; 3 | 4 | export function setup(): BlobServiceClient { 5 | const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; 6 | if (!accountName) throw Error('Azure Storage accountName not found'); 7 | 8 | const blobServiceClient = new BlobServiceClient( 9 | `https://${accountName}.blob.core.windows.net`, 10 | new DefaultAzureCredential(), 11 | ); 12 | 13 | return blobServiceClient; 14 | } 15 | -------------------------------------------------------------------------------- /async-iterators/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", // Specify ECMAScript target version 4 | "module": "commonjs", // Specify module code generation 5 | "noImplicitAny": false, 6 | "strict": false, // Enable all strict type-checking options 7 | "esModuleInterop": true, // Enables emit interoperability between CommonJS and ES Modules 8 | "skipLibCheck": true, // Skip type checking of declaration files 9 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file 10 | "types": ["node"], // Specify type package 11 | "outDir": "./dist" 12 | }, 13 | "include": ["src"], // Specify which files to include 14 | "exclude": ["node_modules"], // Specify which files to excludeS 15 | } -------------------------------------------------------------------------------- /install-nvm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | nvm_install_script=https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh 4 | NVM_DIR="$HOME/.nvm" 5 | NVM_NODE_VERSION=18 6 | 7 | if [ ! -d "$HOME/.nvm" ]; then 8 | echo "> Downloading NVM..." 9 | curl -s -o- $nvm_install_script | bash >/dev/null 2>&1 10 | export NVM_DIR=$NVM_DIR 11 | 12 | # loads nvm 13 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 14 | 15 | # loads nvm bash_completion 16 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" 17 | 18 | echo "> Installing Node.js v$NVM_NODE_VERSION+..." 19 | nvm install $NVM_NODE_VERSION >/dev/null 2>&1 20 | fi 21 | 22 | echo >&2 "> Node.js $(node --version) is ready to use." 23 | -------------------------------------------------------------------------------- /node-dependencies/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = auto 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single -------------------------------------------------------------------------------- /node-dependencies/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "requireConfigFile": false, 12 | "babelOptions": { 13 | "parserOpts": { 14 | "plugins": ["topLevelAwait"] 15 | } 16 | } 17 | }, 18 | "rules": { 19 | "no-console": "off", 20 | "no-undef": "off", 21 | "no-unused-vars": "off", 22 | "no-shadow": "off", 23 | "require-await": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /node-dependencies/3-exercise-package-json/src/index.js: -------------------------------------------------------------------------------- 1 | console.log('Welcome to this application'); 2 | -------------------------------------------------------------------------------- /node-dependencies/3-final-solution-exercise-package-json/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tailwind-trader-api", 3 | "version": "1.0.0", 4 | "description": "HTTP API to manage items from the Tailwind Traders database", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./src/index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [ 11 | "javascript", 12 | "nodejs", 13 | "azure", 14 | "mslearning", 15 | "microsoft-learn", 16 | "tutorial" 17 | ], 18 | "author": "", 19 | "license": "MIT" 20 | } 21 | -------------------------------------------------------------------------------- /node-dependencies/3-final-solution-exercise-package-json/src/index.js: -------------------------------------------------------------------------------- 1 | console.log('Welcome to this application!'); 2 | -------------------------------------------------------------------------------- /node-dependencies/5-exercise-dependency-playwright/__tests__/address-parser.spec.js: -------------------------------------------------------------------------------- 1 | const { test, expect } = require('@playwright/test'); 2 | const { addressParser } = require('../address-parser'); 3 | 4 | test('Address Parser', () => { 5 | const result = addressParser( 6 | 'order: 3 books to address: 112 street city here is my payment info: cardnumber', 7 | ); 8 | 9 | expect(result).toEqual({ 10 | order: '3 books', 11 | address: '112 street city', 12 | payment: 'cardnumber', 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /node-dependencies/5-exercise-dependency-playwright/address-parser.js: -------------------------------------------------------------------------------- 1 | exports.addressParser = function parseOrder(order) { 2 | const match = order.match( 3 | /order:\s(?\w+\s\w+).*address:\s(?
\w+\s\w+\s\w+).*payment info:\s(?\w+)/, 4 | ); 5 | return match.groups; 6 | }; 7 | -------------------------------------------------------------------------------- /node-dependencies/5-exercise-dependency-playwright/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5-exercise-dependency-playwright", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "5-exercise-dependency-playwright", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@playwright/test": "^1.40.0" 13 | } 14 | }, 15 | "node_modules/@playwright/test": { 16 | "version": "1.40.0", 17 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz", 18 | "integrity": "sha512-PdW+kn4eV99iP5gxWNSDQCbhMaDVej+RXL5xr6t04nbKLCBwYtA046t7ofoczHOm8u6c+45hpDKQVZqtqwkeQg==", 19 | "dependencies": { 20 | "playwright": "1.40.0" 21 | }, 22 | "bin": { 23 | "playwright": "cli.js" 24 | }, 25 | "engines": { 26 | "node": ">=16" 27 | } 28 | }, 29 | "node_modules/fsevents": { 30 | "version": "2.3.2", 31 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 32 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 33 | "hasInstallScript": true, 34 | "optional": true, 35 | "os": [ 36 | "darwin" 37 | ], 38 | "engines": { 39 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 40 | } 41 | }, 42 | "node_modules/playwright": { 43 | "version": "1.40.0", 44 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.0.tgz", 45 | "integrity": "sha512-gyHAgQjiDf1m34Xpwzaqb76KgfzYrhK7iih+2IzcOCoZWr/8ZqmdBw+t0RU85ZmfJMgtgAiNtBQ/KS2325INXw==", 46 | "dependencies": { 47 | "playwright-core": "1.40.0" 48 | }, 49 | "bin": { 50 | "playwright": "cli.js" 51 | }, 52 | "engines": { 53 | "node": ">=16" 54 | }, 55 | "optionalDependencies": { 56 | "fsevents": "2.3.2" 57 | } 58 | }, 59 | "node_modules/playwright-core": { 60 | "version": "1.40.0", 61 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.0.tgz", 62 | "integrity": "sha512-fvKewVJpGeca8t0ipM56jkVSU6Eo0RmFvQ/MaCQNDYm+sdvKkMBBWTE1FdeMqIdumRaXXjZChWHvIzCGM/tA/Q==", 63 | "bin": { 64 | "playwright-core": "cli.js" 65 | }, 66 | "engines": { 67 | "node": ">=16" 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /node-dependencies/5-exercise-dependency-playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5-exercise-dependency-playwright", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "playwright test" 8 | }, 9 | "keywords": [ 10 | "javascript", 11 | "nodejs", 12 | "azure", 13 | "mslearning", 14 | "microsoft-learn", 15 | "tutorial" 16 | ], 17 | "author": "", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@playwright/test": "^1.40.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /node-dependencies/5-exercise-dependency/address-parser.js: -------------------------------------------------------------------------------- 1 | exports.addressParser = function parseOrder(order) { 2 | const match = order.match( 3 | /order:\s(?\w+\s\w+).*address:\s(?
\w+\s\w+\s\w+).*payment info:\s(?\w+)/, 4 | ); 5 | return match.groups; 6 | }; 7 | -------------------------------------------------------------------------------- /node-dependencies/5-exercise-dependency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5-exercise-dependency", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "address-parser.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /node-dependencies/5-final-solution-exercise-dependency/__tests__/address-parser.spec.js: -------------------------------------------------------------------------------- 1 | const { addressParser } = require('../address-parser'); 2 | 3 | describe('Address Parser', () => { 4 | test('should parse correctly', () => { 5 | expect( 6 | addressParser( 7 | 'I want to to order: 3 books to address: 112 street city here is my payment info: cardnumber', 8 | ), 9 | ).toEqual({ 10 | order: '3 books', 11 | address: '112 street city', 12 | payment: 'cardnumber', 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /node-dependencies/5-final-solution-exercise-dependency/address-parser.js: -------------------------------------------------------------------------------- 1 | exports.addressParser = function parseOrder(order) { 2 | const match = order.match( 3 | /order:\s(?\w+\s\w+).*address:\s(?
\w+\s\w+\s\w+).*payment info:\s(?\w+)/, 4 | ); 5 | return match.groups; 6 | }; 7 | -------------------------------------------------------------------------------- /node-dependencies/5-final-solution-exercise-dependency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5-final-solution-exercise-dependency", 3 | "version": "1.0.0", 4 | "description": "a simple code sample teaching how to use some packages from npm", 5 | "main": "address-parser.js", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "keywords": [ 10 | "javascript", 11 | "nodejs", 12 | "azure", 13 | "mslearning", 14 | "microsoft-learn", 15 | "tutorial" 16 | ], 17 | "author": "", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "jest": "^29.7.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /node-dependencies/7-exercise-dependency-management/index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const _ = require('lodash'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | async function run() { 7 | const response = await fetch('https://dev.to/api/articles?state=rising'); 8 | const json = await response.json(); 9 | const sorted = _.sortBy(json, ['public_reactions_count'], ['desc']); 10 | const top3 = _.take(sorted, 3); 11 | 12 | const filePrefix = new Date().toISOString().split('T')[0]; 13 | fs.writeFileSync( 14 | path.join(__dirname, `${filePrefix}-feed.json`), 15 | JSON.stringify(top3, null, 2), 16 | ); 17 | } 18 | 19 | run(); 20 | -------------------------------------------------------------------------------- /node-dependencies/7-exercise-dependency-management/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "7-exercise-dependency-management", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "lodash": "^1.1.0", 14 | "node-fetch": "^3.3.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /node-dependencies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-dependencies", 3 | "version": "1.0.0", 4 | "description": "a simple code sample for the Introduction to Node.js Course", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest ./5-final-solution-exercise-dependency", 8 | "lint": "eslint \"./**/*.js\" --fix", 9 | "format": "prettier \"./**/*.js\" --write" 10 | }, 11 | "keywords": [ 12 | "javascript", 13 | "nodejs", 14 | "azure", 15 | "mslearning", 16 | "microsoft-learn", 17 | "tutorial" 18 | ], 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@babel/eslint-parser": "^7.23.3", 23 | "eslint": "^8.54.0", 24 | "eslint-config-airbnb-base": "^15.0.0", 25 | "eslint-plugin-import": "^2.29.0", 26 | "prettier": "3.1.0", 27 | "jest": "^29.7.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /nodejs-debug/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = auto 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single -------------------------------------------------------------------------------- /nodejs-debug/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "requireConfigFile": false, 12 | "babelOptions": { 13 | "parserOpts": { 14 | "plugins": ["topLevelAwait"] 15 | } 16 | } 17 | }, 18 | "rules": { 19 | "no-console": "off", 20 | "no-undef": "off", 21 | "no-unused-vars": "off", 22 | "no-shadow": "off", 23 | "require-await": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nodejs-debug/4-exercise-built-debugger/fibonacci.js: -------------------------------------------------------------------------------- 1 | function fibonacci(n) { 2 | let n1 = 0; 3 | let n2 = 1; 4 | let sum = 0; 5 | 6 | for (let i = 2; i < n; i++) { 7 | sum = n1 + n2; 8 | n1 = n2; 9 | n2 = sum; 10 | } 11 | 12 | return n === 0 ? n1 : n2; 13 | } 14 | 15 | const result = fibonacci(5); 16 | console.log(result); 17 | -------------------------------------------------------------------------------- /nodejs-debug/4-final-solution-exercise-built-debugger/fibonnaci.js: -------------------------------------------------------------------------------- 1 | function fibonacci(n) { 2 | let n1 = 0; 3 | let n2 = 1; 4 | let sum = 0; 5 | 6 | for (let i = 2; i <= n; i++) { 7 | sum = n1 + n2; 8 | n1 = n2; 9 | n2 = sum; 10 | } 11 | 12 | return n === 0 ? n1 : n2; 13 | } 14 | 15 | const result = fibonacci(5); 16 | console.log(result); 17 | -------------------------------------------------------------------------------- /nodejs-debug/6-exercise-debug-with-vscode/currency.js: -------------------------------------------------------------------------------- 1 | const rates = {}; 2 | 3 | function setExchangeRate(rate, sourceCurrency, targetCurrency) { 4 | if (rates[sourceCurrency] === undefined) { 5 | rates[sourceCurrency] = {}; 6 | } 7 | 8 | if (rates[targetCurrency] === undefined) { 9 | rates[targetCurrency] = {}; 10 | } 11 | 12 | rates[sourceCurrency][targetCurrency] = rate; 13 | rates[targetCurrency][sourceCurrency] = 1 / rate; 14 | } 15 | 16 | function convertToCurrency(value, sourceCurrency, targetCurrency) { 17 | const exchangeRate = rates[sourceCurrency][targetCurrency]; 18 | return exchangeRate && value * exchangeRate; 19 | } 20 | 21 | function formatValueForDisplay(value) { 22 | return value.toFixed(2); 23 | } 24 | 25 | function printForeignValues(value, sourceCurrency) { 26 | console.info(`The value of ${value} ${sourceCurrency} is:`); 27 | 28 | for (const targetCurrency in rates) { 29 | if (targetCurrency !== sourceCurrency) { 30 | const convertedValue = convertToCurrency( 31 | value, 32 | sourceCurrency, 33 | targetCurrency, 34 | ); 35 | const displayValue = formatValueForDisplay(convertedValue); 36 | console.info(`- ${convertedValue} ${targetCurrency}`); 37 | } 38 | } 39 | } 40 | 41 | setExchangeRate(0.88, 'USD', 'EUR'); 42 | setExchangeRate(107.4, 'USD', 'JPY'); 43 | printForeignValues(10, 'EUR'); 44 | -------------------------------------------------------------------------------- /nodejs-debug/6-final-solution-exercise-debug-with-vscode copy/currency.js: -------------------------------------------------------------------------------- 1 | const rates = {}; 2 | 3 | function setExchangeRate(rate, sourceCurrency, targetCurrency) { 4 | if (rates[sourceCurrency] === undefined) { 5 | rates[sourceCurrency] = {}; 6 | } 7 | 8 | if (rates[targetCurrency] === undefined) { 9 | rates[targetCurrency] = {}; 10 | } 11 | 12 | for (const currency in rates) { 13 | if (currency !== targetCurrency) { 14 | // Use a pivot rate for currencies that don't have the direct conversion rate 15 | const pivotRate = 16 | currency === sourceCurrency ? 1 : rates[currency][sourceCurrency]; 17 | rates[currency][targetCurrency] = rate * pivotRate; 18 | rates[targetCurrency][currency] = 1 / (rate * pivotRate); 19 | } 20 | } 21 | } 22 | 23 | function convertToCurrency(value, sourceCurrency, targetCurrency) { 24 | const exchangeRate = rates[sourceCurrency][targetCurrency]; 25 | return exchangeRate && value * exchangeRate; 26 | } 27 | 28 | function formatValueForDisplay(value) { 29 | return value.toFixed(2); 30 | } 31 | 32 | function printForeignValues(value, sourceCurrency) { 33 | console.info(`The value of ${value} ${sourceCurrency} is:`); 34 | 35 | for (const targetCurrency in rates) { 36 | if (targetCurrency !== sourceCurrency) { 37 | const convertedValue = convertToCurrency( 38 | value, 39 | sourceCurrency, 40 | targetCurrency, 41 | ); 42 | const displayValue = formatValueForDisplay(convertedValue); 43 | console.info(`- ${displayValue} ${targetCurrency}`); 44 | } 45 | } 46 | } 47 | 48 | setExchangeRate(0.88, 'USD', 'EUR'); 49 | setExchangeRate(107.4, 'USD', 'JPY'); 50 | printForeignValues(10, 'EUR'); 51 | -------------------------------------------------------------------------------- /nodejs-debug/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-debug", 3 | "version": "1.0.0", 4 | "description": "a simple code sample to teach how to debug Node.js application", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "eslint \"./**/*.js\" --fix", 9 | "format": "prettier \"./**/*.js\" --write" 10 | }, 11 | "keywords": [ 12 | "javascript", 13 | "nodejs", 14 | "azure", 15 | "mslearning", 16 | "microsoft-learn", 17 | "tutorial" 18 | ], 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@babel/eslint-parser": "^7.24.1", 23 | "eslint": "^9.6.0", 24 | "eslint-config-airbnb-base": "^15.0.0", 25 | "eslint-plugin-import": "^2.29.0", 26 | "prettier": "3.3.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /nodejs-files/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = auto 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single -------------------------------------------------------------------------------- /nodejs-files/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "requireConfigFile": false, 12 | "babelOptions": { 13 | "parserOpts": { 14 | "plugins": ["topLevelAwait"] 15 | } 16 | } 17 | }, 18 | "rules": { 19 | "no-console": "off", 20 | "no-undef": "off", 21 | "no-unused-vars": "off", 22 | "no-shadow": "off", 23 | "require-await": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nodejs-files/3-exercise-work-file-system/index.js: -------------------------------------------------------------------------------- 1 | // To debug in Codespaces or dev container, use 2 | // index-unit-5.js which has the __dirname and path.join needed 3 | // to resolve the path correctly. 4 | 5 | const path = require('path'); 6 | const fs = require('fs').promises; 7 | 8 | async function findSalesFiles(folderName) { 9 | // (1) Add an array at the top, to hold the paths to all the sales files that the program finds. 10 | let results = []; 11 | 12 | try { 13 | // (2) Read the currentFolder with the `readdir` method. 14 | const items = await fs.readdir(folderName, { withFileTypes: true }); 15 | 16 | // (3) Add a block to loop over each item returned from the `readdir` function using the asynchronous `for...of` loop. 17 | for (const item of items) { 18 | // (4) Add an `if` statement to determine if the item is a file or a directory. 19 | if (item.isDirectory()) { 20 | // (5) If the item is a directory, _resursively call the function `findSalesFiles` again, passing in the path to the item. 21 | const resultsReturned = await findSalesFiles( 22 | `${folderName}/${item.name}`, 23 | ); 24 | results = results.concat(resultsReturned); 25 | } else { 26 | // (6) If it's not a directory, add a check to make sure the item name matches *sales.json*. 27 | if (item.name === 'sales.json') { 28 | results.push(`${folderName}/${item.name}`); 29 | } 30 | } 31 | } 32 | } catch (error) { 33 | console.error('Error reading folder:', error.message); 34 | throw error; 35 | } 36 | 37 | return results; 38 | } 39 | 40 | async function main() { 41 | const storesFolderPath = path.join(__dirname, '..', 'stores'); 42 | const results = await findSalesFiles(storesFolderPath); 43 | console.log(results); 44 | } 45 | 46 | main(); 47 | -------------------------------------------------------------------------------- /nodejs-files/3-final-solution-exercise-work-file-system/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs').promises; 3 | 4 | async function findSalesFiles(folderName) { 5 | let results = []; 6 | 7 | try { 8 | const items = await fs.readdir(folderName, { withFileTypes: true }); 9 | 10 | for (const item of items) { 11 | if (item.isDirectory()) { 12 | const resultsReturned = await findSalesFiles( 13 | `${folderName}/${item.name}`, 14 | ); 15 | results = results.concat(resultsReturned); 16 | } else { 17 | if (item.name === 'sales.json') { 18 | results.push(`${folderName}/${item.name}`); 19 | } 20 | } 21 | } 22 | } catch (error) { 23 | console.error('Error reading folder:', error.message); 24 | throw error; 25 | } 26 | 27 | return results; 28 | } 29 | 30 | async function main() { 31 | const storesFolderPath = path.join(__dirname, '..', 'stores'); 32 | const results = await findSalesFiles(storesFolderPath); 33 | console.log(results); 34 | } 35 | 36 | main(); 37 | -------------------------------------------------------------------------------- /nodejs-files/5-exercise-work-with-paths/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | 4 | async function findSalesFiles(folderName) { 5 | // (1) Add an array at the top, to hold the paths to all the sales files that the program finds. 6 | let results = []; 7 | 8 | try { 9 | // (2) Read the currentFolder with the `readdir` method. 10 | const items = await fs.readdir(folderName, { withFileTypes: true }); 11 | 12 | // (3) Add a block to loop over each item returned from the `readdir` function using the asynchronous `for...of` loop. 13 | for (const item of items) { 14 | // (4) Add an `if` statement to determine if the item is a file or a directory. 15 | if (item.isDirectory()) { 16 | // (5) If the item is a directory, _resursively call the function `findSalesFiles` again, passing in the path to the item. 17 | const resultsReturned = await findSalesFiles( 18 | path.join(folderName, item.name), 19 | ); 20 | results = results.concat(resultsReturned); 21 | } else { 22 | if (path.extname(item.name) === '.json') { 23 | results.push(`${folderName}/${item.name}`); 24 | } 25 | } 26 | } 27 | } catch (error) { 28 | // (6) If it's not a directory, add a check to make sure the item name matches *sales.json*. 29 | console.error('Error reading folder:', error.message); 30 | throw error; 31 | } 32 | 33 | return results; 34 | } 35 | 36 | async function main() { 37 | const salesDir = path.join(__dirname, '..', 'stores'); 38 | 39 | // find paths to all the sales files 40 | const salesFiles = await findSalesFiles(salesDir); 41 | console.log(salesFiles); 42 | } 43 | 44 | main(); 45 | -------------------------------------------------------------------------------- /nodejs-files/5-final-solution-work-with-paths/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | 4 | async function findSalesFiles(folderName) { 5 | let results = []; 6 | 7 | try { 8 | const items = await fs.readdir(folderName, { withFileTypes: true }); 9 | 10 | for (const item of items) { 11 | if (item.isDirectory()) { 12 | const resultsReturned = await findSalesFiles( 13 | path.join(folderName, item.name), 14 | ); 15 | results = results.concat(resultsReturned); 16 | } else { 17 | if (path.extname(item.name) === '.json') { 18 | results.push(`${folderName}/${item.name}`); 19 | } 20 | } 21 | } 22 | } catch (error) { 23 | console.error('Error reading folder:', error.message); 24 | throw error; 25 | } 26 | 27 | return results; 28 | } 29 | 30 | async function main() { 31 | const salesDir = path.join(__dirname, '..', 'stores'); 32 | const salesFiles = await findSalesFiles(salesDir); 33 | console.log(salesFiles); 34 | } 35 | 36 | main(); 37 | -------------------------------------------------------------------------------- /nodejs-files/7-exercise-create-files-directories/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | 4 | async function findSalesFiles(folderName) { 5 | // (1) Add an array at the top, to hold the paths to all the sales files that the program finds. 6 | let results = []; 7 | 8 | try { 9 | // (2) Read the currentFolder with the `readdir` method. 10 | const items = await fs.readdir(folderName, { withFileTypes: true }); 11 | 12 | // (3) Add a block to loop over each item returned from the `readdir` function using the asynchronous `for...of` loop. 13 | for (const item of items) { 14 | // (4) Add an `if` statement to determine if the item is a file or a directory. 15 | if (item.isDirectory()) { 16 | // (5) If the item is a directory, _resursively call the function `findSalesFiles` again, passing in the path to the item. 17 | const resultsReturned = await findSalesFiles( 18 | path.join(folderName, item.name), 19 | ); 20 | results = results.concat(resultsReturned); 21 | } else { 22 | // (6) If it's not a directory, add a check to make sure the item name matches *sales.json*. 23 | if (path.extname(item.name) === '.json') { 24 | results.push(`${folderName}/${item.name}`); 25 | } 26 | } 27 | return results; 28 | } 29 | } catch (error) { 30 | console.error('Error reading folder:', error.message); 31 | throw error; 32 | } 33 | } 34 | 35 | async function main() { 36 | const salesDir = path.join(__dirname, '..', 'stores'); 37 | const salesTotalsDir = path.join(__dirname, '..', 'salesTotals'); 38 | 39 | // create the salesTotal directory if it doesn't exist 40 | try { 41 | await fs.mkdir(salesTotalsDir); 42 | } catch { 43 | console.log(`${salesTotalsDir} already exists.`); 44 | } 45 | 46 | // find paths to all the sales files 47 | const salesFiles = await findSalesFiles(salesDir); 48 | 49 | // write the total to the "totals.txt" file 50 | await fs.writeFile(path.join(salesTotalsDir, 'totals.txt'), String()); 51 | console.log(`Wrote sales totals to ${salesTotalsDir}`); 52 | } 53 | 54 | main(); 55 | -------------------------------------------------------------------------------- /nodejs-files/7-final-solution-exercise-create-files-directories/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | 4 | async function findSalesFiles(folderName) { 5 | let results = []; 6 | 7 | try { 8 | const items = await fs.readdir(folderName, { withFileTypes: true }); 9 | for (const item of items) { 10 | if (item.isDirectory()) { 11 | const resultsReturned = await findSalesFiles( 12 | path.join(folderName, item.name), 13 | ); 14 | results = results.concat(resultsReturned); 15 | } else { 16 | if (path.extname(item.name) === '.json') { 17 | results.push(`${folderName}/${item.name}`); 18 | } 19 | } 20 | 21 | return results; 22 | } 23 | } catch (error) { 24 | console.error('Error reading folder:', error.message); 25 | throw error; 26 | } 27 | } 28 | 29 | async function main() { 30 | const salesDir = path.join(__dirname, '..', 'stores'); 31 | 32 | const salesTotalsDir = path.join(__dirname, '..', 'salesTotals'); 33 | 34 | try { 35 | await fs.mkdir(salesTotalsDir); 36 | } catch (error) { 37 | console.log(`${salesTotalsDir} already exists.`); 38 | } 39 | 40 | const salesFiles = await findSalesFiles(salesDir); 41 | 42 | await fs.writeFile(path.join(salesTotalsDir, 'totals.txt'), String()); 43 | console.log(`Wrote sales totals to ${salesTotalsDir}`); 44 | } 45 | 46 | main(); 47 | -------------------------------------------------------------------------------- /nodejs-files/9-exercise-read-write-files/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | 4 | async function calculateSalesTotal(salesFiles) { 5 | // Final sales total 6 | let salesTotal = 0; 7 | 8 | // (1) Tterates over the `salesFiles` array. 9 | for (file of salesFiles) { 10 | // (2) Reads the file. 11 | const fileContents = await fs.readFile(file); 12 | 13 | // (3) Parses the content as JSON. 14 | const data = JSON.parse(fileContents); 15 | 16 | // (4) Increments the `salesTotal` variable with the `total` value from the file. 17 | salesTotal += data.total; 18 | } 19 | return salesTotal; 20 | } 21 | 22 | async function findSalesFiles(folderName) { 23 | // (1) Add an array at the top, to hold the paths to all the sales files that the program finds. 24 | let results = []; 25 | 26 | try { 27 | // (2) Read the currentFolder with the `readdir` method. 28 | const items = await fs.readdir(folderName, { withFileTypes: true }); 29 | 30 | // (3) Add a block to loop over each item returned from the `readdir` function using the asynchronous `for...of` loop. 31 | for (const item of items) { 32 | // (4) Add an `if` statement to determine if the item is a file or a directory. 33 | if (item.isDirectory()) { 34 | // (5) If the item is a directory, recursively call the function `findSalesFiles` again, passing in the path to the item. 35 | const resultsReturned = await findSalesFiles( 36 | path.join(folderName, item.name), 37 | ); 38 | results = results.concat(resultsReturned); 39 | } else { 40 | // (6) If it's not a directory, add a check to make sure the item name matches *sales.json*. 41 | if (path.extname(item.name) === '.json') { 42 | results.push(`${folderName}/${item.name}`); 43 | } 44 | } 45 | 46 | return results; 47 | } 48 | } catch (error) { 49 | console.error('Error reading folder:', error.message); 50 | throw error; 51 | } 52 | } 53 | 54 | async function main() { 55 | const salesDir = path.join(__dirname, '..', 'stores'); 56 | const salesTotalsDir = path.join(__dirname, '..', 'salesTotals'); 57 | 58 | // create the salesTotal directory if it doesn't exist 59 | try { 60 | await fs.mkdir(salesTotalsDir); 61 | } catch { 62 | console.log(`${salesTotalsDir} already exists.`); 63 | } 64 | 65 | // find paths to all the sales files 66 | const salesFiles = await findSalesFiles(salesDir); 67 | 68 | // read through each sales file to calculate the sales total 69 | const salesTotal = await calculateSalesTotal(salesFiles); 70 | 71 | // write the total to the "totals.json" file 72 | await fs.writeFile( 73 | path.join(salesTotalsDir, 'totals.txt'), 74 | `${salesTotal}\r\n`, 75 | { flag: 'a' }, 76 | ); 77 | console.log(`Wrote sales totals to ${salesTotalsDir}`); 78 | } 79 | 80 | main(); 81 | -------------------------------------------------------------------------------- /nodejs-files/9-final-solution-exercise-read-write-files/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | const path = require('path'); 3 | 4 | async function calculateSalesTotal(salesFiles) { 5 | let salesTotal = 0; 6 | 7 | for (file of salesFiles) { 8 | const fileContents = await fs.readFile(file); 9 | const data = JSON.parse(fileContents); 10 | salesTotal += data.total; 11 | } 12 | return salesTotal; 13 | } 14 | 15 | async function findSalesFiles(folderName) { 16 | let results = []; 17 | 18 | try { 19 | const items = await fs.readdir(folderName, { withFileTypes: true }); 20 | for (const item of items) { 21 | if (item.isDirectory()) { 22 | const resultsReturned = await findSalesFiles( 23 | path.join(folderName, item.name), 24 | ); 25 | results = results.concat(resultsReturned); 26 | } else { 27 | if (path.extname(item.name) === '.json') { 28 | results.push(`${folderName}/${item.name}`); 29 | } 30 | } 31 | 32 | return results; 33 | } 34 | } catch (error) { 35 | console.error('Error reading folder:', error.message); 36 | throw error; 37 | } 38 | } 39 | 40 | async function main() { 41 | const salesDir = path.join(__dirname, '..', 'stores'); 42 | const salesTotalsDir = path.join(__dirname, '..', 'salesTotals'); 43 | 44 | try { 45 | await fs.mkdir(salesTotalsDir); 46 | } catch { 47 | console.log(`${salesTotalsDir} already exists.`); 48 | } 49 | 50 | const salesFiles = await findSalesFiles(salesDir); 51 | 52 | const salesTotal = await calculateSalesTotal(salesFiles); 53 | 54 | await fs.writeFile( 55 | path.join(salesTotalsDir, 'totals.txt'), 56 | `${salesTotal}\r\n`, 57 | { flag: 'a' }, 58 | ); 59 | console.log(`Wrote sales totals to ${salesTotalsDir}`); 60 | } 61 | 62 | main(); 63 | -------------------------------------------------------------------------------- /nodejs-files/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-files", 3 | "version": "1.0.0", 4 | "description": "a simple code sample to teach how to read and write files in Node.js applications", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "eslint \"./**/*.js\" --fix", 9 | "format": "prettier \"./**/*.js\" --write", 10 | "precommit": "npm run format && git add . && npm run lint" 11 | }, 12 | "keywords": [ 13 | "javascript", 14 | "nodejs", 15 | "azure", 16 | "mslearning", 17 | "microsoft-learn", 18 | "tutorial" 19 | ], 20 | "author": "", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@babel/eslint-parser": "^7.24.1", 24 | "eslint": "^8.57.0", 25 | "eslint-config-airbnb-base": "^15.0.0", 26 | "eslint-plugin-import": "^2.29.1", 27 | "husky": "^9.0.10", 28 | "prettier": "^3.3.1" 29 | }, 30 | "husky": { 31 | "hooks": { 32 | "pre-commit": "npm run precommit" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /nodejs-files/stores/201/sales.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 22385.32 3 | } -------------------------------------------------------------------------------- /nodejs-files/stores/202/sales.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 3308.21 3 | } -------------------------------------------------------------------------------- /nodejs-files/stores/203/sales.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 101242.09 3 | } -------------------------------------------------------------------------------- /nodejs-files/stores/204/sales.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 58998.14 3 | } -------------------------------------------------------------------------------- /nodejs-http/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = auto 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single -------------------------------------------------------------------------------- /nodejs-http/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "requireConfigFile": false, 12 | "babelOptions": { 13 | "parserOpts": { 14 | "plugins": ["topLevelAwait"] 15 | } 16 | } 17 | }, 18 | "rules": { 19 | "no-console": "off", 20 | "no-undef": "off", 21 | "no-unused-vars": "off", 22 | "no-shadow": "off", 23 | "require-await": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nodejs-http/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /nodejs-http/3-final-solution-exercise-express-web-app/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const port = 3000; 4 | 5 | app.get('/', (req, res) => res.send('Hello World!')); 6 | 7 | app.get('/products', (req, res) => { 8 | const products = [ 9 | { id: 1, name: 'hammer' }, 10 | { id: 2, name: 'screwdriver' }, 11 | { id: 3, name: 'wrench' }, 12 | ]; 13 | 14 | res.json(products); 15 | }); 16 | 17 | app.listen(port, () => { 18 | console.log(`Example app listening on port ${port}! http://localhost:${port}/`); 19 | }); -------------------------------------------------------------------------------- /nodejs-http/3-final-solution-exercise-express-web-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3-final-solution-exercise-express-web-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.19.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nodejs-http/5-final-solution-exercise-express-middleware/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const port = 3000; 4 | 5 | function isAuthorized(req, res, next) { 6 | const authHeader = req.headers.authorization; 7 | 8 | if (!authHeader || authHeader !== 'secretpassword') { 9 | return res.status(401).send('Unauthorized: Access Denied'); 10 | } 11 | 12 | next(); 13 | } 14 | 15 | 16 | app.get('/', isAuthorized, (req, res) => res.send('Hello World!')); 17 | 18 | app.get('/users', (req, res) => { 19 | res.json([ 20 | { 21 | id: 1, 22 | name: 'User Userson' 23 | }, 24 | ]); 25 | }); 26 | 27 | app.get("/products", (req, res) => { 28 | res.json([ 29 | { 30 | id: 1, 31 | name: 'The Bluest Eye' 32 | }, 33 | ]); 34 | }); 35 | 36 | 37 | app.listen(port, () => { 38 | console.log(`Example app listening on port ${port}!`); 39 | }); -------------------------------------------------------------------------------- /nodejs-http/5-final-solution-exercise-express-middleware/client.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const options = { 4 | port: 3000, 5 | hostname: 'localhost', 6 | path: '/users', 7 | headers: { 8 | authorization: 'secretpassword' 9 | }, 10 | }; 11 | 12 | const req = http.get(options, (res) => { 13 | console.log(`Connected - Status Code ${res.statusCode}`); 14 | 15 | res.on('data', (chunk) => { 16 | console.log("Chunk data: ", chunk.toString()); 17 | }); 18 | 19 | res.on('end', () => { 20 | console.log('No more data'); 21 | }); 22 | 23 | res.on('close', () => { 24 | console.log('Connection closed'); 25 | }); 26 | }); 27 | 28 | req.on('error', (error) => { 29 | console.error('An error occurred: ', error); 30 | }); 31 | 32 | req.end(); -------------------------------------------------------------------------------- /nodejs-http/5-final-solution-exercise-express-middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5-final-solution-exercise-express-middleware", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.19.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nodejs-http/exercise-express-middleware/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const port = 3000; 4 | 5 | app.get("/", (req, res) => res.send("Hello World!")); 6 | 7 | app.get("/users", (req, res) => { 8 | res.json([ 9 | { 10 | id: 1, 11 | name: "User Userson", 12 | }, 13 | ]); 14 | }); 15 | 16 | app.get("/products", (req, res) => { 17 | res.json([ 18 | { 19 | id: 1, 20 | name: "The Bluest Eye", 21 | }, 22 | ]); 23 | }); 24 | 25 | app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`)); 26 | -------------------------------------------------------------------------------- /nodejs-http/exercise-express-middleware/client.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const options = { 4 | port: 3000, 5 | hostname: 'localhost', 6 | path: '/users', 7 | headers: {}, 8 | }; 9 | 10 | const req = http.get(options, (res) => { 11 | console.log(`Connected - Status Code ${res.statusCode}`); 12 | 13 | res.on('data', (chunk) => { 14 | console.log("Chunk data: ", chunk.toString()); 15 | }); 16 | 17 | res.on('end', () => { 18 | console.log('No more data'); 19 | }); 20 | 21 | res.on('close', () => { 22 | console.log('Connection closed'); 23 | }); 24 | }); 25 | 26 | req.on('error', (error) => { 27 | console.error('An error occurred: ', error); 28 | }); 29 | 30 | req.end(); -------------------------------------------------------------------------------- /nodejs-http/exercise-express-middleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "exercise-express-middleware", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.19.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nodejs-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-http", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint \"./**/*.js\" --fix", 8 | "format": "prettier \"./**/*.js\" --write", 9 | "precommit": "npm run format && git add . && npm run lint" 10 | }, 11 | "keywords": [ 12 | "javascript", 13 | "nodejs", 14 | "azure", 15 | "mslearning", 16 | "microsoft-learn", 17 | "tutorial" 18 | ], 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@babel/eslint-parser": "^7.24.1", 23 | "eslint": "^8.57.0", 24 | "eslint-config-airbnb-base": "^15.0.0", 25 | "eslint-plugin-import": "^2.29.1", 26 | "husky": "^9.1.5", 27 | "prettier": "^3.3.1" 28 | }, 29 | "husky": { 30 | "hooks": { 31 | "pre-commit": "npm run precommit" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nodejs-intro-esm/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = auto 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single -------------------------------------------------------------------------------- /nodejs-intro-esm/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": false, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "requireConfigFile": false, 12 | "babelOptions": { 13 | "parserOpts": { 14 | "plugins": ["topLevelAwait"] 15 | } 16 | } 17 | }, 18 | "rules": { 19 | "no-console": "off", 20 | "no-undef": "off", 21 | "no-unused-vars": "off", 22 | "no-shadow": "off", 23 | "require-await": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nodejs-intro-esm/5-try/index-1.js: -------------------------------------------------------------------------------- 1 | console.log('Hello World, from a script file.'); 2 | -------------------------------------------------------------------------------- /nodejs-intro-esm/5-try/top-level-async-await.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | console.log(`start`); 4 | try { 5 | const res = await fetch('https://github.com/MicrosoftDocs/node-essentials'); 6 | 7 | console.log('statusCode:', res.status); 8 | } catch (error) { 9 | console.log(`error: ${error}`); 10 | } 11 | console.log(`end`); 12 | -------------------------------------------------------------------------------- /nodejs-intro-esm/README.md: -------------------------------------------------------------------------------- 1 | # Repository for the Course: Introduction to Node.js 2 | 3 | Repository responsible for the code developed during the **[Introduction to Node.js course](https://learn.microsoft.com/training/modules/intro-to-nodejs/?WT.mc_id=javascript-111027-gllemos)** from Microsoft Learn. 4 | 5 | ## How to run the code? 6 | 7 | 1. First fork the repository and then clone it. 8 | 9 | 2. You can use Codespaces to run the code directly in the browser, or you can clone the repository and run it locally. 10 | 11 | 3. Go to the folder: `nodejs-intro/3-how-nodejs-works`. And for example, run the following command: 12 | 13 | ```bash 14 | node 5-try/top-level-async-await.js 15 | ``` 16 | 17 | ![Callback sample](./resources/nodejs-intro-01.gif) 18 | 19 | ## Any issues or doubts? 20 | 21 | If you have any problems or doubts related to the code, feel free to open an **[issue](https://github.com/MicrosoftDocs/node-essentials/issues)** indicating the problem or doubt. -------------------------------------------------------------------------------- /nodejs-intro-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-intro-esm", 3 | "version": "1.0.0", 4 | "description": "a simple code sample for the Introduction to Node.js Course", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "lint": "eslint \"./**/*.js\" --fix", 10 | "format": "prettier \"./**/*.js\" --write" }, 11 | "keywords": [ 12 | "javascript", 13 | "nodejs", 14 | "azure", 15 | "mslearning", 16 | "microsoft-learn", 17 | "tutorial" 18 | ], 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@babel/eslint-parser": "^7.24.1", 23 | "eslint": "^8.57.0", 24 | "eslint-config-airbnb-base": "^15.0.0", 25 | "eslint-plugin-import": "^2.29.0", 26 | "prettier": "3.2.4" 27 | }, 28 | "dependencies": { 29 | "node-fetch": "^3.3.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /nodejs-intro/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = auto 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single -------------------------------------------------------------------------------- /nodejs-intro/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "requireConfigFile": false, 12 | "babelOptions": { 13 | "parserOpts": { 14 | "plugins": ["topLevelAwait"] 15 | } 16 | } 17 | }, 18 | "rules": { 19 | "no-console": "off", 20 | "no-undef": "off", 21 | "no-unused-vars": "off", 22 | "no-shadow": "off", 23 | "require-await": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/async-await-top-level.js: -------------------------------------------------------------------------------- 1 | // top-level async/await asynchronous example 2 | 3 | const fs = require('fs').promises; 4 | 5 | const filePath = './file.txt'; 6 | 7 | // `async` before the parent function 8 | try { 9 | // `await` before the async method 10 | const data = await fs.readFile(filePath, 'utf-8'); 11 | console.log(data); 12 | console.log('Done!'); 13 | } catch (error) { 14 | console.log('An error occurred...: ', error); 15 | } 16 | console.log("I'm the last line of the file!"); 17 | -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/async-await.js: -------------------------------------------------------------------------------- 1 | // async/await asynchronous example 2 | 3 | const fs = require('fs').promises; 4 | 5 | const filePath = './file.txt'; 6 | 7 | // `async` before the parent function 8 | async function readFileAsync() { 9 | try { 10 | // `await` before the async method 11 | const data = await fs.readFile(filePath, 'utf-8'); 12 | console.log(data); 13 | console.log('Done!'); 14 | } catch (error) { 15 | console.log('An error occurred...: ', error); 16 | } 17 | } 18 | 19 | readFileAsync() 20 | .then(() => { 21 | console.log('Success!'); 22 | }) 23 | .catch((error) => { 24 | console.log('An error occurred...: ', error); 25 | }); 26 | -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/callback-anonymous.js: -------------------------------------------------------------------------------- 1 | // callback asynchronous example 2 | 3 | // file system module from Node.js 4 | const fs = require('fs'); 5 | 6 | // relative path to file 7 | const filePath = './file.txt'; 8 | 9 | // async request to read a file 10 | // 11 | // parameter 1: filePath 12 | // parameter 2: encoding of utf-8 13 | // parmeter 3: callback function () => {} 14 | fs.readFile(filePath, 'utf-8', (error, data) => { 15 | if (error) { 16 | console.log('An error occurred...: ', error); 17 | } else { 18 | console.log(data); // Hi, developers! 19 | console.log('Done!'); 20 | } 21 | }); 22 | 23 | console.log("I'm the last line of the file!"); 24 | -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/callbacks.js: -------------------------------------------------------------------------------- 1 | // callback asynchronous example 2 | 3 | // file system module from Node.js 4 | const fs = require('fs'); 5 | 6 | // relative path to file 7 | const filePath = './file.txt'; 8 | 9 | // callback 10 | const callback = (error, data) => { 11 | if (error) { 12 | console.log('An error occurred...: ', error); 13 | } else { 14 | console.log(data); // Hi, developers! 15 | console.log('Done!'); 16 | } 17 | }; 18 | 19 | // async request to read a file 20 | // 21 | // parameter 1: filePath 22 | // parameter 2: encoding of utf-8 23 | // parmeter 3: callback function 24 | fs.readFile(filePath, 'utf-8', callback); 25 | 26 | console.log("I'm the last line of the file!"); 27 | -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/file.txt: -------------------------------------------------------------------------------- 1 | Hi, developers! -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/nested-callback.js: -------------------------------------------------------------------------------- 1 | // nested callback example 2 | 3 | // file system module from Node.js 4 | const fs = require('fs'); 5 | 6 | fs.readFile(param1, param2, (error, data) => { 7 | if (!error) { 8 | fs.writeFile(paramsWrite, (error, data) => { 9 | if (!error) { 10 | fs.readFile(paramsRead, (error, data) => { 11 | if (!error) { 12 | // do something 13 | } 14 | }); 15 | } 16 | }); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/promise-call.js: -------------------------------------------------------------------------------- 1 | // Call a promise function 2 | 3 | promiseRead() 4 | .then((data) => { 5 | // handle success 6 | }) 7 | .catch((error) => { 8 | // handle error 9 | }); 10 | -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/promise-create-basic.js: -------------------------------------------------------------------------------- 1 | // Create a basic promise function 2 | function promiseFunction() { 3 | return new Promise((resolve, reject) => { 4 | // do something 5 | 6 | if (error) { 7 | // indicate success 8 | reject(error); 9 | } else { 10 | // indicate error 11 | resolve(data); 12 | } 13 | }); 14 | } 15 | 16 | // Call a basic promise function 17 | promiseFunction() 18 | .then((data) => { 19 | // handle success 20 | }) 21 | .catch((error) => { 22 | // handle error 23 | }); 24 | -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/promise-create.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | // Promisify a callback function 4 | function promiseRead(fileName) { 5 | // promise 6 | return Promise((resolve, reject) => { 7 | // callback 8 | fs.readFile(fileName, 'utf-8', (error, data) => { 9 | if (error) { 10 | // indicate error 11 | reject(error); 12 | } else { 13 | // indicate success 14 | resolve(data); 15 | } 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/promises.js: -------------------------------------------------------------------------------- 1 | // promises asynchronous example 2 | 3 | const fs = require('fs').promises; 4 | const filePath = './file.txt'; 5 | 6 | // request to read a file 7 | fs.readFile(filePath, 'utf-8') 8 | .then((data) => { 9 | console.log(data); 10 | console.log('Done!'); 11 | }) 12 | .catch((error) => { 13 | console.log('An error occurred...: ', error); 14 | }); 15 | 16 | console.log(`I'm the last line of the file!`); 17 | -------------------------------------------------------------------------------- /nodejs-intro/3-how-nodejs-works/synchronous-api.js: -------------------------------------------------------------------------------- 1 | // synchronous example 2 | 3 | const fs = require('fs'); 4 | 5 | const filePath = './file.txt'; 6 | 7 | try { 8 | // request to read a file 9 | const data = fs.readFileSync(filePath, 'utf-8'); 10 | console.log(data); 11 | console.log('Done!'); 12 | } catch (error) { 13 | console.log('An error occurred...: ', error); 14 | } 15 | -------------------------------------------------------------------------------- /nodejs-intro/README.md: -------------------------------------------------------------------------------- 1 | # Repository for the Course: Introduction to Node.js 2 | 3 | Repository responsible for the code developed during the **[Introduction to Node.js course](https://learn.microsoft.com/training/modules/intro-to-nodejs/?WT.mc_id=javascript-111027-gllemos)** from Microsoft Learn. 4 | 5 | ## How to run the code? 6 | 7 | 1. First fork the repository and then clone it. 8 | 9 | 2. You can use Codespaces to run the code directly in the browser, or you can clone the repository and run it locally. 10 | 11 | 3. Go to the folder: `nodejs-intro/3-how-nodejs-works`. And for example, run the following command: 12 | 13 | ```bash 14 | node callback.js 15 | ``` 16 | 17 | ![Callback sample](./resources/nodejs-intro-01.gif) 18 | 19 | ## Any issues or doubts? 20 | 21 | If you have any problems or doubts related to the code, feel free to open an **[issue](https://github.com/MicrosoftDocs/node-essentials/issues)** indicating the problem or doubt. -------------------------------------------------------------------------------- /nodejs-intro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-intro", 3 | "version": "1.0.0", 4 | "description": "a simple code sample for the Introduction to Node.js Course", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "lint": "eslint \"./**/*.js\" --fix", 10 | "format": "prettier \"./**/*.js\" --write" 11 | }, 12 | "keywords": [ 13 | "javascript", 14 | "nodejs", 15 | "azure", 16 | "mslearning", 17 | "microsoft-learn", 18 | "tutorial" 19 | ], 20 | "author": "", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@babel/eslint-parser": "^7.24.1", 24 | "eslint": "^8.57.0", 25 | "eslint-config-airbnb-base": "^15.0.0", 26 | "eslint-plugin-import": "^2.29.0", 27 | "prettier": "3.3.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /nodejs-intro/resources/nodejs-intro-01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftDocs/node-essentials/0a83f20adffcb21d901f51a0ff7a59b53cbfff8b/nodejs-intro/resources/nodejs-intro-01.gif -------------------------------------------------------------------------------- /nodejs-route/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = auto 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | quote_type = single -------------------------------------------------------------------------------- /nodejs-route/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "requireConfigFile": false, 12 | "babelOptions": { 13 | "parserOpts": { 14 | "plugins": ["topLevelAwait"] 15 | } 16 | } 17 | }, 18 | "rules": { 19 | "no-console": "off", 20 | "no-undef": "off", 21 | "no-unused-vars": "off", 22 | "no-shadow": "off", 23 | "require-await": "error" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /nodejs-route/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /nodejs-route/3-final-solution-exercise-express-routing/parameters/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const port = 3000; 4 | 5 | const products = [ 6 | { 7 | id: 1, 8 | name: 'Ivanhoe', 9 | author: 'Sir Walter Scott', 10 | }, 11 | { 12 | id: 2, 13 | name: 'Colour Magic', 14 | author: 'Terry Pratchett', 15 | }, 16 | { 17 | id: 3, 18 | name: 'The Bluest eye', 19 | author: 'Toni Morrison', 20 | }, 21 | ]; 22 | 23 | app.get('/', (req, res) => res.send('Hello API!')); 24 | 25 | app.get('/products/:id', (req, res) => { 26 | res.json(products.find((p) => p.id === +req.params.id)); 27 | }); 28 | 29 | app.get('/products', (req, res) => { 30 | const page = +req.query.page; 31 | const pageSize = +req.query.pageSize; 32 | 33 | if (page && pageSize) { 34 | const start = (page - 1) * pageSize; 35 | const end = start + pageSize; 36 | res.json(products.slice(start, end)); 37 | } else { 38 | res.json(products); 39 | } 40 | }); 41 | 42 | app.listen(port, () => 43 | console.log(`Example app listening at http://localhost:${port}`), 44 | ); 45 | -------------------------------------------------------------------------------- /nodejs-route/3-final-solution-exercise-express-routing/parameters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parameters", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.17.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nodejs-route/5-final-solution-exercise-read-write/reading-writing/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const port = 3000; 4 | 5 | app.use(express.json()); 6 | 7 | let products = []; 8 | 9 | app.post('/products', function (req, res) { 10 | const newProduct = { 11 | ...req.body, 12 | id: products.length + 1, 13 | }; 14 | 15 | products = [...products, newProduct]; 16 | res.json(newProduct); 17 | }); 18 | 19 | app.put('/products', function (req, res) { 20 | let updatedProduct; 21 | 22 | products = products.map((p) => { 23 | if (p.id === req.body.id) { 24 | updatedProduct = { 25 | ...p, 26 | ...req.body, 27 | }; 28 | return updatedProduct; 29 | } 30 | return p; 31 | }); 32 | res.json(updatedProduct); 33 | }); 34 | 35 | app.delete('/products/:id', function (req, res) { 36 | const deletedProduct = products.find((p) => p.id === +req.params.id); 37 | products = products.filter((p) => p.id !== +req.params.id); 38 | 39 | res.json(deletedProduct); 40 | }); 41 | 42 | app.get('/products', (req, res) => { 43 | res.json(products); 44 | }); 45 | 46 | app.listen(port, () => 47 | console.log(`Example app listening at http://localhost:${port}`), 48 | ); 49 | -------------------------------------------------------------------------------- /nodejs-route/5-final-solution-exercise-read-write/reading-writing/client-delete-route.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const data = JSON.stringify({ 4 | id: 1, 5 | }); 6 | 7 | const options = { 8 | hostname: 'localhost', 9 | port: 3000, 10 | path: '/products', 11 | method: 'DELETE', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | 'Content-Length': data.length, 15 | }, 16 | }; 17 | 18 | const request = http.request(options, (res) => { 19 | let body = ''; 20 | res.on('data', (chunk) => { 21 | body += '' + chunk; 22 | }); 23 | res.on('error', (err) => console.error('err', err)); 24 | res.on('end', () => { 25 | console.log('response', body); 26 | }); 27 | res.on('close', () => { 28 | console.log('Closed connection'); 29 | }); 30 | }); 31 | 32 | request.end(data); 33 | -------------------------------------------------------------------------------- /nodejs-route/5-final-solution-exercise-read-write/reading-writing/client-delete.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const productToDelete = { 4 | id: 1, 5 | }; 6 | const data = JSON.stringify(productToDelete); 7 | 8 | const options = { 9 | hostname: 'localhost', 10 | port: 3000, 11 | path: `/products/${productToDelete.id}`, 12 | method: 'DELETE', 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | 'Content-Length': data.length, 16 | }, 17 | }; 18 | 19 | const request = http.request(options, (res) => { 20 | let body = ''; 21 | res.on('data', (chunk) => { 22 | body += '' + chunk; 23 | }); 24 | res.on('error', (err) => console.error('err', err)); 25 | res.on('end', () => { 26 | console.log('response', body); 27 | }); 28 | res.on('close', () => { 29 | console.log('Closed connection'); 30 | }); 31 | }); 32 | 33 | request.end(data); 34 | -------------------------------------------------------------------------------- /nodejs-route/5-final-solution-exercise-read-write/reading-writing/client-get.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | http.get({ path: '/products', hostname: 'localhost', port: 3000 }, (res) => { 4 | let body = ''; 5 | res.on('data', (chunk) => { 6 | body += '' + chunk; 7 | }); 8 | res.on('end', () => { 9 | console.log('Received data', body); 10 | }); 11 | res.on('close', () => { 12 | console.log('Connection closed'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /nodejs-route/5-final-solution-exercise-read-write/reading-writing/client-post.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const data = JSON.stringify({ 4 | name: 'product', 5 | }); 6 | 7 | const options = { 8 | hostname: 'localhost', 9 | port: 3000, 10 | path: '/products', 11 | method: 'POST', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | 'Content-Length': data.length, 15 | }, 16 | }; 17 | 18 | const request = http.request(options, (res) => { 19 | let body = ''; 20 | res.on('data', (chunk) => { 21 | body += '' + chunk; 22 | }); 23 | res.on('end', () => { 24 | console.log('response', body); 25 | }); 26 | res.on('close', () => { 27 | console.log('Closed connection'); 28 | }); 29 | }); 30 | 31 | request.end(data); 32 | -------------------------------------------------------------------------------- /nodejs-route/5-final-solution-exercise-read-write/reading-writing/client-put.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const data = JSON.stringify({ 4 | name: 'product-updated', 5 | id: 1, 6 | }); 7 | 8 | const options = { 9 | hostname: 'localhost', 10 | port: 3000, 11 | path: '/products', 12 | method: 'PUT', 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | 'Content-Length': data.length, 16 | }, 17 | }; 18 | 19 | const request = http.request(options, (res) => { 20 | let body = ''; 21 | res.on('data', (chunk) => { 22 | body += '' + chunk; 23 | }); 24 | res.on('end', () => { 25 | console.log('response', body); 26 | }); 27 | res.on('close', () => { 28 | console.log('Closed connection'); 29 | }); 30 | }); 31 | 32 | request.end(data); 33 | -------------------------------------------------------------------------------- /nodejs-route/5-final-solution-exercise-read-write/reading-writing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "express": "^4.18.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nodejs-route/exercise-express-routing/parameters/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const port = 3000; 4 | 5 | const products = [ 6 | { 7 | id: 1, 8 | name: 'Ivanhoe', 9 | author: 'Sir Walter Scott', 10 | }, 11 | { 12 | id: 2, 13 | name: 'Colour Magic', 14 | author: 'Terry Pratchett', 15 | }, 16 | { 17 | id: 3, 18 | name: 'The Bluest eye', 19 | author: 'Toni Morrison', 20 | }, 21 | ]; 22 | 23 | app.get('/', (req, res) => res.send('Hello API!')); 24 | 25 | app.get('/products/:id', (req, res) => {}); 26 | 27 | app.get('/products', (req, res) => {}); 28 | 29 | app.listen(port, () => 30 | console.log(`Example app listening at http://localhost:${port}`), 31 | ); 32 | -------------------------------------------------------------------------------- /nodejs-route/exercise-express-routing/parameters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parameters", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.17.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nodejs-route/exercise-express-routing/reading-writing/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const port = 3000; 4 | 5 | app.use(express.json()); 6 | 7 | let products = []; 8 | 9 | app.post('/products', function (req, res) { 10 | // implement 11 | }); 12 | 13 | app.put('/products', function (req, res) { 14 | // implement 15 | }); 16 | 17 | app.delete('/products/:id', function (req, res) { 18 | // implement 19 | }); 20 | 21 | app.get('/products', (req, res) => { 22 | // implement 23 | }); 24 | 25 | app.listen(port, () => 26 | console.log(`Example app listening at http://localhost:${port}`), 27 | ); 28 | -------------------------------------------------------------------------------- /nodejs-route/exercise-express-routing/reading-writing/client-delete-route.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const data = JSON.stringify({ 4 | id: 1, 5 | }); 6 | 7 | const options = { 8 | hostname: 'localhost', 9 | port: 3000, 10 | path: '/products', 11 | method: 'DELETE', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | 'Content-Length': data.length, 15 | }, 16 | }; 17 | 18 | const request = http.request(options, (res) => { 19 | let body = ''; 20 | res.on('data', (chunk) => { 21 | body += '' + chunk; 22 | }); 23 | res.on('error', (err) => console.error('err', err)); 24 | res.on('end', () => { 25 | console.log('response', body); 26 | }); 27 | res.on('close', () => { 28 | console.log('Closed connection'); 29 | }); 30 | }); 31 | 32 | request.end(data); 33 | -------------------------------------------------------------------------------- /nodejs-route/exercise-express-routing/reading-writing/client-delete.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const productToDelete = { 4 | id: 1, 5 | }; 6 | const data = JSON.stringify(productToDelete); 7 | 8 | const options = { 9 | hostname: 'localhost', 10 | port: 3000, 11 | path: `/products/${productToDelete.id}`, 12 | method: 'DELETE', 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | 'Content-Length': data.length, 16 | }, 17 | }; 18 | 19 | const request = http.request(options, (res) => { 20 | let body = ''; 21 | res.on('data', (chunk) => { 22 | body += '' + chunk; 23 | }); 24 | res.on('error', (err) => console.error('err', err)); 25 | res.on('end', () => { 26 | console.log('response', body); 27 | }); 28 | res.on('close', () => { 29 | console.log('Closed connection'); 30 | }); 31 | }); 32 | 33 | request.end(data); 34 | -------------------------------------------------------------------------------- /nodejs-route/exercise-express-routing/reading-writing/client-get.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | http.get({ path: '/products', hostname: 'localhost', port: 3000 }, (res) => { 4 | let body = ''; 5 | res.on('data', (chunk) => { 6 | body += '' + chunk; 7 | }); 8 | res.on('end', () => { 9 | console.log('Received data', body); 10 | }); 11 | res.on('close', () => { 12 | console.log('Connection closed'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /nodejs-route/exercise-express-routing/reading-writing/client-post.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const data = JSON.stringify({ 4 | name: 'product', 5 | }); 6 | 7 | const options = { 8 | hostname: 'localhost', 9 | port: 3000, 10 | path: '/products', 11 | method: 'POST', 12 | headers: { 13 | 'Content-Type': 'application/json', 14 | 'Content-Length': data.length, 15 | }, 16 | }; 17 | 18 | const request = http.request(options, (res) => { 19 | let body = ''; 20 | res.on('data', (chunk) => { 21 | body += '' + chunk; 22 | }); 23 | res.on('end', () => { 24 | console.log('response', body); 25 | }); 26 | res.on('close', () => { 27 | console.log('Closed connection'); 28 | }); 29 | }); 30 | 31 | request.end(data); 32 | -------------------------------------------------------------------------------- /nodejs-route/exercise-express-routing/reading-writing/client-put.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const data = JSON.stringify({ 4 | name: 'product-updated', 5 | id: 1, 6 | }); 7 | 8 | const options = { 9 | hostname: 'localhost', 10 | port: 3000, 11 | path: '/products', 12 | method: 'PUT', 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | 'Content-Length': data.length, 16 | }, 17 | }; 18 | 19 | const request = http.request(options, (res) => { 20 | let body = ''; 21 | res.on('data', (chunk) => { 22 | body += '' + chunk; 23 | }); 24 | res.on('end', () => { 25 | console.log('response', body); 26 | }); 27 | res.on('close', () => { 28 | console.log('Closed connection'); 29 | }); 30 | }); 31 | 32 | request.end(data); 33 | -------------------------------------------------------------------------------- /nodejs-route/exercise-express-routing/reading-writing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.17.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /nodejs-route/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-route", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint \"./**/*.js\" --fix", 8 | "format": "prettier \"./**/*.js\" --write", 9 | "precommit": "npm run format && git add . && npm run lint" 10 | }, 11 | "keywords": [ 12 | "javascript", 13 | "nodejs", 14 | "azure", 15 | "mslearning", 16 | "microsoft-learn", 17 | "tutorial" 18 | ], 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@babel/eslint-parser": "^7.23.3", 23 | "eslint": "^8.56.0", 24 | "eslint-config-airbnb-base": "^15.0.0", 25 | "eslint-plugin-import": "^2.29.1", 26 | "husky": "^9.0.10", 27 | "prettier": "^3.2.4" 28 | }, 29 | "husky": { 30 | "hooks": { 31 | "pre-commit": "npm run precommit" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@babel/eslint-parser": "^7.23.3", 4 | "eslint": "^8.55.0", 5 | "eslint-config-airbnb-base": "^15.0.0", 6 | "eslint-plugin-import": "^2.29.0" 7 | }, 8 | "dependencies": { 9 | "prettier": "^3.1.0" 10 | }, 11 | "workspaces": [ 12 | "node-dependencies", 13 | "nodejs-debug", 14 | "nodejs-files", 15 | "nodejs-http", 16 | "nodejs-intro", 17 | "nodejs-intro-esm", 18 | "nodejs-route", 19 | "test-with-node-testrunner", 20 | "test-with-vitest", 21 | "test-with-jest" 22 | ], 23 | "scripts":{ 24 | "build": "npm run build --workspaces", 25 | "test": "npm run test --workspaces" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/nodejs-learn-path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftDocs/node-essentials/0a83f20adffcb21d901f51a0ff7a59b53cbfff8b/resources/nodejs-learn-path.png -------------------------------------------------------------------------------- /scripts/create-cosmos-db-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # filepath: create-resources.sh 3 | 4 | # Prerequisites: 5 | # - Install the Azure CLI and run: az login 6 | # 7 | # This script now fetches the role definition id for "Cosmos DB Operator" automatically. 8 | 9 | # Exit if any command returns a non-zero status 10 | # set -euo 11 | 12 | # Verify that the user is logged in 13 | if ! az account show > /dev/null 2>&1; then 14 | echo "Error: Not logged in to Azure CLI. Please run 'az login' and try again." 15 | exit 1 16 | fi 17 | 18 | random_string() { 19 | tr -dc 'a-z0-9' > .env 64 | echo "Cosmos DB Endpoint: $COSMOS_DB_ENDPOINT" 65 | 66 | 67 | # -------------------------------------------------------------------------- 68 | # Create a Cosmos DB SQL database and container, and update .env with their values 69 | 70 | # Set database and container names 71 | DB_NAME="db-$RANDOM_STRING" 72 | CONTAINER_NAME="container-$RANDOM_STRING" 73 | 74 | # Create a database (using throughput of 400 RU/s as an example) 75 | az cosmosdb sql database create \ 76 | --account-name "$RESOURCE_NAME" \ 77 | --name "$DB_NAME" \ 78 | --resource-group "$RG" \ 79 | --subscription "$SUBSCRIPTION_ID" \ 80 | --throughput 400 81 | echo "COSMOS_DATABASE_NAME=$DB_NAME" >> .env 82 | printf "Cosmos DB SQL database '%s' created\n" "$DB_NAME" 83 | 84 | # Create a container (using throughput of 400 RU/s as an example) 85 | az cosmosdb sql container create \ 86 | --account-name "$RESOURCE_NAME" \ 87 | --database-name "$DB_NAME" \ 88 | --name "$CONTAINER_NAME" \ 89 | --partition-key-path "/name" \ 90 | --resource-group "$RG" \ 91 | --subscription "$SUBSCRIPTION_ID" \ 92 | --throughput 400 93 | echo "COSMOS_CONTAINER_NAME=$CONTAINER_NAME" >> .env 94 | printf "Cosmos DB SQL container '%s' created\n" "$CONTAINER_NAME" 95 | 96 | printf "Database and container names appended to .env\n" 97 | -------------------------------------------------------------------------- 98 | 99 | # Role definition id for "Cosmos DB Operator" 100 | 101 | # Create a role assignment using the fetched role definition. 102 | az cosmosdb sql role assignment create \ 103 | --resource-group "$RG" \ 104 | --account-name "$RESOURCE_NAME" \ 105 | --role-definition-id "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.DocumentDB/databaseAccounts/$RESOURCE_NAME/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" \ 106 | --principal-id "$PRINCIPAL_ID" \ 107 | --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.DocumentDB/databaseAccounts/$RESOURCE_NAME" -------------------------------------------------------------------------------- /test-with-jest/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 2021, 13 | "project": "./tsconfig.json" 14 | }, 15 | "plugins": [ 16 | "jest", 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | "no-console": "off", 21 | "no-noImplicitAny": "off", 22 | "require-await": "error", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/no-unsafe-assignment": "off", 25 | "@typescript-eslint/no-unsafe-argument": "off", 26 | "@typescript-eslint/no-unsafe-member-access": "off", 27 | "@typescript-eslint/unbound-method": "off" 28 | } 29 | } -------------------------------------------------------------------------------- /test-with-jest/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env* 3 | dist -------------------------------------------------------------------------------- /test-with-jest/README.md: -------------------------------------------------------------------------------- 1 | # Jest for the Azure SDK for JavaScript 2 | 3 | This subfolder is the source code for the [How to Test Azure SDK Integration in JavaScript Applications](https://learn.microsoft.com/azure/developer/javascript/sdk/test-sdk-integration). The purpose is to demonstrate unit test mocks for the Azure SDK for JavaScript. These specific examples use Azure Cosmos DB. 4 | 5 | Use Azure CLI to create Cosmos DB resource if you intend to insert a document with the application code against the cloud resource. [Script](../scripts/create-cosmos-db-resources.sh) 6 | 7 | ## To run the test 8 | 9 | 1. `npm install` 10 | 2. `npm run build` 11 | 3. `npm test` 12 | 13 | ```console 14 | > test-with-jest@1.0.0 test 15 | > jest --detectOpenHandles dist --coverage 16 | 17 | PASS dist/mock-function/lib/insert.spec.js (275.092 s) 18 | PASS dist/fakes/fake-in-mem-db.spec.js 19 | PASS dist/test-boilerplate/boilerplate-with-mock.spec.js 20 | PASS dist/test-boilerplate/boilerplate.spec.js 21 | ---------------|---------|----------|---------|---------|------------------- 22 | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 23 | ---------------|---------|----------|---------|---------|------------------- 24 | All files | 76.47 | 55.55 | 86.66 | 80.43 | 25 | data | 75 | 20 | 83.33 | 75 | 26 | fake-data.js | 100 | 100 | 100 | 100 | 27 | model.js | 50 | 20 | 66.66 | 50 | 13-22 28 | lib | 77.77 | 76.47 | 88.88 | 86.36 | 29 | insert.js | 77.77 | 76.47 | 88.88 | 86.36 | 29-35 30 | ---------------|---------|----------|---------|---------|------------------- 31 | 32 | Test Suites: 4 passed, 4 total 33 | Tests: 6 passed, 6 total 34 | Snapshots: 0 total 35 | Time: 285.194 s 36 | Ran all test suites matching /dist/i. 37 | ``` 38 | 39 | ## Related content 40 | 41 | * [Passwordless connections for Azure services](https://learn.microsoft.com/azure/developer/intro/passwordless-overview) 42 | * [Cosmos DB keyless access to the service](https://learn.microsoft.com/azure/cosmos-db/role-based-access-control) 43 | * [Jest](https://jestjs.io/) -------------------------------------------------------------------------------- /test-with-jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-with-jest", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "clear": "rm -rf dist && rm -rf coverage", 7 | "prestart": "npm run build", 8 | "start": "node dist/mock-function/index.js", 9 | "test": "npm run build && npm run test:jest", 10 | "test:jest": "jest --detectOpenHandles dist --coverage", 11 | "prebuild": "npm run format && npm run lint", 12 | "build": "tsc", 13 | "lint": "eslint \"./src/**/*.ts\" --fix", 14 | "format": "prettier --write './src/**/*.{ts,tsx}" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@azure/cosmos": "^4.1.0", 21 | "@azure/identity": "^4.4.1", 22 | "dotenv": "^16.4.5" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "^29.5.12", 26 | "@types/node": "^22.4.0", 27 | "@typescript-eslint/eslint-plugin": "^8.2.0", 28 | "eslint": "^8.56.0", 29 | "eslint-config-airbnb-base": "^15.0.0", 30 | "eslint-plugin-import": "^2.29.1", 31 | "eslint-plugin-jest": "^28.8.0", 32 | "jest": "^29.7.0", 33 | "prettier": "^3.2.4", 34 | "typescript": "^5.5.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test-with-jest/sample.env: -------------------------------------------------------------------------------- 1 | SUBSCRIPTION_ID= 2 | RESOURCE_GROUP_NAME= 3 | LOCATION= 4 | COSMOS_DATABASE_NAME= 5 | COSMOS_CONTAINER_NAME= 6 | 7 | ## ./scripts/create-resources.sh adds the COSMOS DB endpoint 8 | # The endpoint URL for your Azure Cosmos DB account 9 | #COSMOS_ENDPOINT= 10 | -------------------------------------------------------------------------------- /test-with-jest/src/fakes/fake-in-mem-db.spec.ts: -------------------------------------------------------------------------------- 1 | // fake-in-mem-db.spec.ts 2 | class FakeDatabase { 3 | private data: Record; 4 | 5 | constructor() { 6 | this.data = {}; 7 | } 8 | 9 | save(key: string, value: any): void { 10 | this.data[key] = value; 11 | } 12 | 13 | get(key: string): any { 14 | return this.data[key]; 15 | } 16 | } 17 | 18 | // Function to test 19 | function someTestFunction(db: FakeDatabase, key: string, value: any): any { 20 | db.save(key, value); 21 | return db.get(key); 22 | } 23 | 24 | describe('someTestFunction', () => { 25 | let fakeDb: FakeDatabase; 26 | let testKey: string; 27 | let testValue: any; 28 | 29 | beforeEach(() => { 30 | fakeDb = new FakeDatabase(); 31 | testKey = 'testKey'; 32 | testValue = { 33 | first: 'John', 34 | last: 'Jones', 35 | lastUpdated: new Date().toISOString(), 36 | }; 37 | }); 38 | 39 | afterEach(() => { 40 | // Clear all mocks 41 | jest.resetAllMocks(); 42 | }); 43 | 44 | test('should save and return the correct value', () => { 45 | // Spy on the save method 46 | jest.spyOn(fakeDb, 'save'); 47 | 48 | // Call the function under test. 49 | const result = someTestFunction(fakeDb, testKey, testValue); 50 | 51 | // Verify state 52 | expect(result).toEqual(testValue); 53 | expect(result.first).toBe('John'); 54 | expect(result.last).toBe('Jones'); 55 | expect(result.lastUpdated).toBe(testValue.lastUpdated); 56 | 57 | // Verify behavior 58 | expect(fakeDb.save).toHaveBeenCalledWith(testKey, testValue); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test-with-jest/src/mock-function/data/connect-to-cosmos.ts: -------------------------------------------------------------------------------- 1 | // connect-to-cosmos.ts 2 | 3 | import { Container, CosmosClient } from '@azure/cosmos'; 4 | import { DefaultAzureCredential } from '@azure/identity'; 5 | import 'dotenv/config'; 6 | import { randomUUID } from 'crypto'; 7 | 8 | export { Container }; 9 | 10 | export function connectToCosmosWithoutKey() { 11 | const endpoint = process.env.COSMOS_DB_ENDPOINT!; 12 | const credential = new DefaultAzureCredential(); 13 | 14 | const client = new CosmosClient({ endpoint, aadCredentials: credential }); 15 | return client; 16 | } 17 | export async function connectToContainer(): Promise { 18 | const client = connectToCosmosWithoutKey(); 19 | const databaseName = process.env.COSMOS_DATABASE_NAME; 20 | const containerName = process.env.COSMOS_CONTAINER_NAME; 21 | 22 | // Ensure the database exists 23 | const { database } = await client.databases.createIfNotExists({ 24 | id: databaseName, 25 | }); 26 | 27 | // Ensure the container exists 28 | const { container } = await database.containers.createIfNotExists({ 29 | id: containerName, 30 | }); 31 | 32 | return container; 33 | } 34 | export function getUniqueId(): string { 35 | return randomUUID(); 36 | } 37 | -------------------------------------------------------------------------------- /test-with-jest/src/mock-function/data/fake-data.ts: -------------------------------------------------------------------------------- 1 | import { DbDocument, RawInput } from './model'; 2 | import { randomUUID } from 'crypto'; 3 | 4 | // Predefined arrays of first and last names 5 | const firstNames = ['Alice', 'Bob', 'Charlie', 'Dana', 'Eve', 'Frank']; 6 | const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller']; 7 | 8 | function getRandomElement(arr: T[]): T { 9 | const randomIndex = Math.floor(Math.random() * arr.length); 10 | return arr[randomIndex]; 11 | } 12 | 13 | function createFixture(): T { 14 | const result = { 15 | first: getRandomElement(firstNames), 16 | last: getRandomElement(lastNames), 17 | }; 18 | return result as T; 19 | } 20 | 21 | export function createTestInput(): RawInput { 22 | const { first, last } = createFixture(); 23 | return { id: randomUUID(), first, last }; 24 | } 25 | 26 | export function createTestInputAndResult(): { 27 | input: RawInput; 28 | result: Partial; 29 | } { 30 | const input = createTestInput(); 31 | const result = { 32 | id: input.id, 33 | name: `${input.first} ${input.last}`, 34 | }; 35 | return { input, result }; 36 | } 37 | -------------------------------------------------------------------------------- /test-with-jest/src/mock-function/data/model.ts: -------------------------------------------------------------------------------- 1 | // input-verified.ts 2 | export interface DbDocument { 3 | id: string; 4 | name: string; 5 | } 6 | 7 | export interface DbError { 8 | message: string; 9 | code: number; 10 | } 11 | export function isDbError(error: any): error is DbError { 12 | return 'message' in error && 'code' in error; 13 | } 14 | 15 | export interface VerificationErrors { 16 | message: string; 17 | } 18 | export function isVerificationErrors(error: any): error is VerificationErrors { 19 | return 'message' in error; 20 | } 21 | 22 | export interface RawInput { 23 | id: string; 24 | first: string; 25 | last: string; 26 | } 27 | 28 | export function validateRawInput(input: any): string[] { 29 | const errors: string[] = []; 30 | 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 32 | if (typeof input.first !== 'string' || input.first.trim().length === 0) { 33 | errors.push('First name is required and must be a non-empty string'); 34 | } 35 | 36 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 37 | if (typeof input.last !== 'string' || input.last.trim().length === 0) { 38 | errors.push('Last name is required and must be a non-empty string'); 39 | } 40 | 41 | return errors; 42 | } 43 | -------------------------------------------------------------------------------- /test-with-jest/src/mock-function/data/verify.ts: -------------------------------------------------------------------------------- 1 | // input-verified.ts 2 | import { validateRawInput } from './model'; 3 | 4 | export function inputVerified(doc: any): boolean { 5 | const result = validateRawInput(doc); 6 | return result.length === 0; 7 | } 8 | -------------------------------------------------------------------------------- /test-with-jest/src/mock-function/index.ts: -------------------------------------------------------------------------------- 1 | import { connectToContainer } from './data/connect-to-cosmos'; 2 | import { createTestInput } from './data/fake-data'; 3 | import { insertDocument } from './lib/insert'; 4 | import { DbDocument, DbError, VerificationErrors } from './data/model'; 5 | 6 | async function main(): Promise { 7 | try { 8 | const container = await connectToContainer(); 9 | const input = createTestInput(); 10 | return insertDocument(container, input); 11 | } catch (error) { 12 | console.error(error); 13 | process.exit(1); 14 | } 15 | } 16 | 17 | main() 18 | .then((result: any) => console.log(result)) 19 | .catch(console.error); 20 | -------------------------------------------------------------------------------- /test-with-jest/src/mock-function/lib/insert.spec.ts: -------------------------------------------------------------------------------- 1 | // insertDocument.test.ts 2 | import { Container } from '../data/connect-to-cosmos'; 3 | import { createTestInputAndResult } from '../data/fake-data'; 4 | import type { DbDocument, DbError, RawInput } from '../data/model'; 5 | import { isDbError, isVerificationErrors } from '../data/model'; 6 | import { inputVerified } from '../data/verify'; 7 | import { insertDocument } from './insert'; 8 | 9 | // Mock app dependencies for Cosmos DB setup 10 | jest.mock('../data/connect-to-cosmos', () => ({ 11 | connectToContainer: jest.fn(), 12 | getUniqueId: jest.fn().mockReturnValue('unique-id'), 13 | })); 14 | 15 | // Mock app dependencies for input verification 16 | jest.mock('../data/verify', () => ({ 17 | inputVerified: jest.fn(), 18 | })); 19 | 20 | describe('SDK', () => { 21 | let mockContainer: jest.Mocked; 22 | 23 | beforeEach(() => { 24 | // Clear all mocks before each test 25 | jest.resetAllMocks(); 26 | 27 | // Mock the Cosmos DB Container create method 28 | mockContainer = { 29 | items: { 30 | create: jest.fn(), 31 | }, 32 | } as unknown as jest.Mocked; 33 | }); 34 | 35 | it('should return verification error if input is not verified', async () => { 36 | // Arrange - Mock the input verification function to return false 37 | jest.mocked(inputVerified).mockReturnValue(false); 38 | 39 | // Arrange - wrong shape of doc on purpose 40 | const doc = { name: 'test' }; 41 | 42 | // Act - Call the function to test 43 | const insertDocumentResult = await insertDocument( 44 | mockContainer, 45 | doc as unknown as RawInput, 46 | ); 47 | 48 | // Assert - State verification: Check the result when verification fails 49 | if (isVerificationErrors(insertDocumentResult)) { 50 | expect(insertDocumentResult).toEqual({ 51 | message: 'Verification failed', 52 | }); 53 | } else { 54 | throw new Error('Result is not of type VerificationErrors'); 55 | } 56 | 57 | // Assert - Behavior verification: Ensure create method was not called 58 | expect(mockContainer.items.create).not.toHaveBeenCalled(); 59 | }); 60 | 61 | it('should insert document successfully', async () => { 62 | // Arrange - create input and expected result data 63 | const { input, result }: { input: RawInput; result: Partial } = 64 | createTestInputAndResult(); 65 | 66 | // Arrange - mock the input verification function to return true 67 | (inputVerified as jest.Mock).mockReturnValue(true); 68 | (mockContainer.items.create as jest.Mock).mockResolvedValue({ 69 | resource: result, 70 | }); 71 | 72 | // Act - Call the function to test 73 | const insertDocumentResult = await insertDocument(mockContainer, input); 74 | 75 | // Assert - State verification: Check the result when insertion is successful 76 | expect(insertDocumentResult).toEqual(result); 77 | 78 | // Assert - Behavior verification: Ensure create method was called with correct arguments 79 | expect(mockContainer.items.create).toHaveBeenCalledTimes(1); 80 | expect(mockContainer.items.create).toHaveBeenCalledWith({ 81 | id: input.id, 82 | name: result.name, 83 | }); 84 | }); 85 | 86 | it('should return error if db insert fails', async () => { 87 | // Arrange - create input and expected result data 88 | const { input, result } = createTestInputAndResult(); 89 | 90 | // Arrange - mock the input verification function to return true 91 | jest.mocked(inputVerified).mockReturnValue(true); 92 | 93 | // Arrange - mock the Cosmos DB create method to throw an error 94 | const mockError: DbError = { 95 | message: 'An unknown error occurred', 96 | code: 500, 97 | }; 98 | jest.mocked(mockContainer.items.create).mockRejectedValue(mockError); 99 | 100 | // Act - Call the function to test 101 | const insertDocumentResult = await insertDocument(mockContainer, input); 102 | 103 | // Assert - verify type as DbError 104 | if (isDbError(insertDocumentResult)) { 105 | expect(insertDocumentResult.message).toBe(mockError.message); 106 | } else { 107 | throw new Error('Result is not of type DbError'); 108 | } 109 | 110 | // Assert - Behavior verification: Ensure create method was called with correct arguments 111 | expect(mockContainer.items.create).toHaveBeenCalledTimes(1); 112 | expect(mockContainer.items.create).toHaveBeenCalledWith({ 113 | id: input.id, 114 | name: result.name, 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /test-with-jest/src/mock-function/lib/insert.ts: -------------------------------------------------------------------------------- 1 | // insertDocument.ts 2 | import { Container } from '../data/connect-to-cosmos'; 3 | import { 4 | DbDocument, 5 | DbError, 6 | RawInput, 7 | VerificationErrors, 8 | } from '../data/model'; 9 | import { inputVerified } from '../data/verify'; 10 | 11 | export async function insertDocument( 12 | container: Container, 13 | doc: RawInput, 14 | ): Promise { 15 | const isVerified: boolean = inputVerified(doc); 16 | 17 | if (!isVerified) { 18 | return { message: 'Verification failed' } as VerificationErrors; 19 | } 20 | 21 | try { 22 | const { resource } = await container.items.create({ 23 | id: doc.id, 24 | name: `${doc.first} ${doc.last}`, 25 | }); 26 | 27 | return resource as DbDocument; 28 | } catch (error: any) { 29 | if (error instanceof Error) { 30 | if ((error as any).code === 409) { 31 | return { 32 | message: 'Insertion failed: Duplicate entry', 33 | code: 409, 34 | } as DbError; 35 | } 36 | return { message: error.message, code: (error as any).code } as DbError; 37 | } else { 38 | return { message: 'An unknown error occurred', code: 500 } as DbError; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test-with-jest/src/test-boilerplate/boilerplate-with-mock.spec.ts: -------------------------------------------------------------------------------- 1 | // boilerplate-with-mock.spec.ts 2 | 3 | // Mock the dependencies 4 | jest.mock('../mock-function/data/connect-to-cosmos', () => ({ 5 | myFunctionToMock: jest.fn(), 6 | })); 7 | 8 | describe('nameOfGroupOfTests', () => { 9 | beforeEach(() => { 10 | // Clear all mocks before each test 11 | jest.resetAllMocks(); 12 | 13 | // Other setup required before each test 14 | }); 15 | 16 | it('should if ', async () => { 17 | // Arrange 18 | // - set up the test data and the expected result 19 | // Act 20 | // - call the function to test 21 | // Assert 22 | // - check the state: result returned from function 23 | // - check the behavior: dependency function calls 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test-with-jest/src/test-boilerplate/boilerplate.spec.ts: -------------------------------------------------------------------------------- 1 | // boilerplate.spec.ts 2 | 3 | describe('nameOfGroupOfTests', () => { 4 | beforeEach(() => { 5 | // Setup required before each test 6 | }); 7 | afterEach(() => { 8 | // Cleanup required after each test 9 | }); 10 | 11 | it('should if ', async () => { 12 | // Arrange 13 | // - set up the test data and the expected result 14 | // Act 15 | // - call the function to test 16 | // Assert 17 | // - check the state: result returned from function 18 | // - check the behavior: dependency function calls 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test-with-jest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", // Specify ECMAScript target version 4 | "module": "commonjs", // Specify module code generation 5 | "strict": true, // Enable all strict type-checking options 6 | "esModuleInterop": true, // Enables emit interoperability between CommonJS and ES Modules 7 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file 8 | "types": ["jest", "node"], // Specify type package 9 | "outDir": "dist", 10 | "rootDir": "src", 11 | }, 12 | } -------------------------------------------------------------------------------- /test-with-node-testrunner/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 2021, 13 | "project": "./tsconfig.json" 14 | }, 15 | "plugins": [ 16 | "jest", 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | "no-console": "off", 21 | "no-noImplicitAny": "off", 22 | "require-await": "error", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/no-unsafe-assignment": "off", 25 | "@typescript-eslint/no-unsafe-argument": "off", 26 | "@typescript-eslint/no-unsafe-member-access": "off", 27 | "@typescript-eslint/unbound-method": "off" 28 | } 29 | } -------------------------------------------------------------------------------- /test-with-node-testrunner/README.md: -------------------------------------------------------------------------------- 1 | # Node.js native test runner for the Azure SDK for JavaScript 2 | 3 | This subfolder is the source code for the [How to Test Azure SDK Integration in JavaScript Applications](https://learn.microsoft.com/azure/developer/javascript/sdk/test-sdk-integration). The purpose is to demonstrate unit test mocks for the Azure SDK for JavaScript. These specific examples use Azure Cosmos DB. 4 | 5 | Use Azure CLI to create Cosmos DB resource if you intend to insert a document with the application code against the cloud resource. [Script](../scripts/create-cosmos-db-resources.sh) 6 | 7 | ## To run the test 8 | 9 | 1. `npm install` 10 | 3. `npm test` 11 | 12 | ```console 13 | > test-with-node-testrunner@1.0.0 build 14 | > rm -rf dist && tsc 15 | 16 | ▶ Spies 17 | ✔ should verify calls in a mock (2.466559ms) 18 | ✔ Spies (3.917687ms) 19 | ▶ Stub Test Suite 20 | ✔ should stub APIs (3.878824ms) 21 | ✔ should stub different values for API calls (1.967753ms) 22 | ✔ Stub Test Suite (7.46516ms) 23 | ▶ boilerplate with mock 24 | ✔ should if (2.118992ms) 25 | ✔ boilerplate with mock (3.480984ms) 26 | ▶ boilerplate 27 | ✔ should if (1.587994ms) 28 | ✔ boilerplate (2.865063ms) 29 | ▶ In-Mem DB 30 | ✔ should save and return the correct value (3.486273ms) 31 | ✔ In-Mem DB (4.931667ms) 32 | ▶ SDK 33 | ✔ should insert document successfully (6.777565ms) 34 | ✔ should return verification error if input is not verified (2.334627ms) 35 | ✔ should return error if db insert fails (0.827453ms) 36 | ✔ SDK (11.135955ms) 37 | ℹ tests 9 38 | ℹ suites 6 39 | ℹ pass 9 40 | ℹ fail 0 41 | ℹ cancelled 0 42 | ℹ skipped 0 43 | ℹ todo 0 44 | ℹ duration_ms 281634.635767 45 | ℹ start of coverage report 46 | ℹ ------------------------------------------------------------------------------------------ 47 | ℹ file | line % | branch % | funcs % | uncovered lines 48 | ℹ ------------------------------------------------------------------------------------------ 49 | ℹ dist | | | | 50 | ℹ src | | | | 51 | ℹ data | | | | 52 | ℹ connect-to-cosmos.js | 36.96 | 100.00 | 0.00 | 4-10 21-25 27-41 43-44 53 | ℹ fake-data.js | 100.00 | 100.00 | 100.00 | 54 | ℹ model.js | 47.83 | 100.00 | 66.67 | 12-23 55 | ℹ verify.js | 72.73 | 100.00 | 0.00 | 7-9 56 | ℹ lib | | | | 57 | ℹ insert.js | 82.22 | 75.00 | 90.00 | 32-39 58 | ℹ test | | | | 59 | ℹ 01-spies.test.js | 100.00 | 85.71 | 100.00 | 60 | ℹ 02-stubs.test.js | 92.47 | 88.24 | 89.66 | 19-25 61 | ℹ boilerplate-with-mock.test.js | 90.48 | 84.21 | 81.25 | 21-24 62 | ℹ boilerplate.test.js | 100.00 | 90.00 | 72.73 | 63 | ℹ fake-in-mem-db.test.js | 100.00 | 90.91 | 100.00 | 64 | ℹ insert.test.js | 94.83 | 89.66 | 77.78 | 37 62 79-81 92 65 | ℹ ------------------------------------------------------------------------------------------ 66 | ℹ all files | 86.44 | 87.05 | 80.51 | 67 | ℹ ------------------------------------------------------------------------------------------ 68 | ℹ end of coverage report 69 | ``` 70 | 71 | ## Related content 72 | 73 | * [Passwordless connections for Azure services](https://learn.microsoft.com/azure/developer/intro/passwordless-overview) 74 | * [Cosmos DB keyless access to the service](https://learn.microsoft.com/azure/cosmos-db/role-based-access-control) 75 | * [Node.js Test Runner](https://nodejs.org/docs/latest/api/test.html#test-runner) -------------------------------------------------------------------------------- /test-with-node-testrunner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-with-node-testrunner", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "prebuild": "npm run format && npm run lint", 8 | "build": "tsc", 9 | "lint": "eslint \"./src/**/*.ts\" --fix", 10 | "format": "prettier --write './**/*.{ts,tsx}", 11 | "test": "npm run build && node --test --experimental-test-coverage --experimental-test-module-mocks --trace-exit" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^22.13.10", 15 | "@typescript-eslint/eslint-plugin": "^8.2.0", 16 | "eslint-config-airbnb-base": "^15.0.0", 17 | "eslint-plugin-import": "^2.29.1", 18 | "eslint-plugin-jest": "^28.8.0", 19 | "eslint": "^8.56.0", 20 | "prettier": "^3.2.4", 21 | "typescript": "^5.5.4" 22 | }, 23 | "dependencies": { 24 | "@azure/cosmos": "^4.1.0", 25 | "@azure/identity": "^4.4.1", 26 | "dotenv": "^16.4.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test-with-node-testrunner/src/data/connect-to-cosmos.ts: -------------------------------------------------------------------------------- 1 | // connect-to-cosmos.ts 2 | 3 | import { Container, CosmosClient } from '@azure/cosmos'; 4 | import { DefaultAzureCredential } from '@azure/identity'; 5 | import 'dotenv/config'; 6 | import { randomUUID } from 'crypto'; 7 | 8 | export { Container }; 9 | 10 | export default class CosmosConnector { 11 | static connectToCosmosWithoutKey(): CosmosClient { 12 | const endpoint = process.env.COSMOS_DB_ENDPOINT!; 13 | const credential = new DefaultAzureCredential(); 14 | const client = new CosmosClient({ endpoint, aadCredentials: credential }); 15 | return client; 16 | } 17 | 18 | static async connectToContainer(): Promise { 19 | const client = CosmosConnector.connectToCosmosWithoutKey(); 20 | const databaseName = process.env.COSMOS_DATABASE_NAME!; 21 | const containerName = process.env.COSMOS_CONTAINER_NAME!; 22 | 23 | // Ensure the database exists 24 | const { database } = await client.databases.createIfNotExists({ 25 | id: databaseName, 26 | }); 27 | 28 | // Ensure the container exists 29 | const { container } = await database.containers.createIfNotExists({ 30 | id: containerName, 31 | }); 32 | 33 | return container; 34 | } 35 | 36 | static getUniqueId(): string { 37 | return randomUUID(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test-with-node-testrunner/src/data/fake-data.ts: -------------------------------------------------------------------------------- 1 | import type { DbDocument, RawInput } from './model.js'; 2 | import { randomUUID } from 'crypto'; 3 | 4 | // Predefined arrays of first and last names 5 | const firstNames = ['Alice', 'Bob', 'Charlie', 'Dana', 'Eve', 'Frank']; 6 | const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller']; 7 | 8 | function getRandomElement(arr: T[]): T { 9 | const randomIndex = Math.floor(Math.random() * arr.length); 10 | return arr[randomIndex]; 11 | } 12 | 13 | function createFixture(): T { 14 | const result = { 15 | first: getRandomElement(firstNames), 16 | last: getRandomElement(lastNames), 17 | }; 18 | return result as T; 19 | } 20 | 21 | export function createTestInput(): RawInput { 22 | const { first, last } = createFixture(); 23 | return { id: randomUUID(), first, last }; 24 | } 25 | 26 | export function createTestInputAndResult(): { 27 | input: RawInput; 28 | result: Partial; 29 | } { 30 | const input = createTestInput(); 31 | const result = { 32 | id: input.id, 33 | name: `${input.first} ${input.last}`, 34 | }; 35 | return { input, result }; 36 | } 37 | -------------------------------------------------------------------------------- /test-with-node-testrunner/src/data/model.ts: -------------------------------------------------------------------------------- 1 | // input-verified.ts 2 | export interface DbDocument { 3 | id: string; 4 | name: string; 5 | } 6 | export interface DbError { 7 | message: string; 8 | code: number; 9 | } 10 | export interface VerificationErrors { 11 | message: string; 12 | } 13 | export interface RawInput { 14 | id: string; 15 | first: string; 16 | last: string; 17 | } 18 | 19 | export function isDbError(error: any): error is DbError { 20 | return 'message' in error && 'code' in error; 21 | } 22 | export function isVerificationErrors(error: any): error is VerificationErrors { 23 | return 'message' in error; 24 | } 25 | export function validateRawInput(input: any): string[] { 26 | const errors: string[] = []; 27 | 28 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 29 | if (typeof input.first !== 'string' || input.first.trim().length === 0) { 30 | errors.push('First name is required and must be a non-empty string'); 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 34 | if (typeof input.last !== 'string' || input.last.trim().length === 0) { 35 | errors.push('Last name is required and must be a non-empty string'); 36 | } 37 | 38 | return errors; 39 | } 40 | -------------------------------------------------------------------------------- /test-with-node-testrunner/src/data/verify.ts: -------------------------------------------------------------------------------- 1 | // input-verified.ts 2 | import { validateRawInput } from './model.js'; 3 | 4 | export default class Verfiy { 5 | static inputVerified(doc: any): boolean { 6 | const result = validateRawInput(doc); 7 | return result.length === 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test-with-node-testrunner/src/index.ts: -------------------------------------------------------------------------------- 1 | import CosmosConnector from './data/connect-to-cosmos.js'; 2 | import { createTestInput } from './data/fake-data.js'; 3 | import { insertDocument } from './lib/insert.js'; 4 | 5 | try { 6 | const container = await CosmosConnector.connectToContainer(); 7 | const input = createTestInput(); 8 | const result = await insertDocument(container, input); 9 | console.log(result); 10 | } catch (error) { 11 | console.error(error); 12 | process.exit(1); 13 | } 14 | -------------------------------------------------------------------------------- /test-with-node-testrunner/src/lib/insert.ts: -------------------------------------------------------------------------------- 1 | // insertDocument.ts 2 | import { Container } from '../data/connect-to-cosmos.js'; 3 | import type { 4 | DbDocument, 5 | DbError, 6 | RawInput, 7 | VerificationErrors, 8 | } from '../data/model.js'; 9 | import Verify from '../data/verify.js'; 10 | 11 | export async function insertDocument( 12 | container: Container, 13 | doc: RawInput, 14 | ): Promise { 15 | const isVerified: boolean = Verify.inputVerified(doc); 16 | 17 | if (!isVerified) { 18 | return { message: 'Verification failed' } as VerificationErrors; 19 | } 20 | 21 | try { 22 | const { resource } = await container.items.create({ 23 | id: doc.id, 24 | name: `${doc.first} ${doc.last}`, 25 | }); 26 | 27 | return resource as DbDocument; 28 | } catch (error: any) { 29 | if (error instanceof Error) { 30 | if ((error as any).code === 409) { 31 | return { 32 | message: 'Insertion failed: Duplicate entry', 33 | code: 409, 34 | } as DbError; 35 | } 36 | return { message: error.message, code: (error as any).code } as DbError; 37 | } else { 38 | return { message: 'An unknown error occurred', code: 500 } as DbError; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test-with-node-testrunner/test/01-spies.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, mock } from 'node:test'; 2 | import assert from 'node:assert'; 3 | 4 | function run({ fn, times }) { 5 | for (let i = 0; i < times; i++) { 6 | fn({ current: i * 5 }); 7 | } 8 | } 9 | 10 | describe('Spies', () => { 11 | it('should verify calls in a mock', () => { 12 | const spy = mock.fn(); 13 | spy.mock.mockImplementation((arg) => { 14 | console.log(arg); 15 | }); 16 | 17 | run({ fn: spy, times: 3 }); 18 | 19 | assert.strictEqual(spy.mock.callCount(), 3); 20 | const calls = spy.mock.calls; 21 | 22 | assert.deepStrictEqual(calls[0].arguments[0], { current: 0 }); 23 | assert.deepStrictEqual(calls[1].arguments[0], { current: 5 }); 24 | assert.deepStrictEqual(calls[2].arguments[0], { current: 10 }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test-with-node-testrunner/test/02-stubs.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach, mock } from 'node:test'; 2 | import assert from 'node:assert'; 3 | 4 | class Service { 5 | static async getTalks({ skip, limit }) { 6 | // Use a public API (jsonplaceholder) to retrieve posts with pagination. 7 | const url = `https://jsonplaceholder.typicode.com/posts?_start=${skip}&_limit=${limit}`; 8 | const response = await fetch(url); 9 | return response.json(); 10 | } 11 | } 12 | 13 | function mapResponse(data) { 14 | return data 15 | .map(({ id, title }, index) => `[${index}] id: ${id}, title: ${title}`) 16 | .join('\n'); 17 | } 18 | 19 | async function run({ skip = 0, limit = 10 } = {}) { 20 | const talks = mapResponse(await Service.getTalks({ skip, limit })); 21 | return talks; 22 | } 23 | 24 | describe('Stub Test Suite', () => { 25 | beforeEach(() => mock.restoreAll()); 26 | 27 | it('should stub APIs', async () => { 28 | // Stub Service.getTalks so that it returns one fake post. 29 | const m = mock.method(Service, 'getTalks').mock; 30 | m.mockImplementation(async () => [ 31 | { 32 | id: '1', 33 | title: 'Sample Post', 34 | }, 35 | ]); 36 | 37 | const result = await run({ limit: 1 }); 38 | const expected = `[0] id: 1, title: Sample Post`; 39 | 40 | // Verify that the stubbed method was called once. 41 | assert.deepStrictEqual(m.callCount(), 1); 42 | const calls = m.calls; 43 | assert.deepStrictEqual(calls[0].arguments[0], { skip: 0, limit: 1 }); 44 | assert.strictEqual(result, expected); 45 | }); 46 | 47 | it('should stub different values for API calls', async () => { 48 | // Instead of chaining mockImplementationOnce, build a responses array. 49 | const m = mock.method(Service, 'getTalks').mock; 50 | const responses = [ 51 | async () => [{ id: '1', title: 'Post One' }], 52 | async () => [{ id: '2', title: 'Post Two' }], 53 | async () => [{ id: '3', title: 'Post Three' }], 54 | ]; 55 | 56 | m.mockImplementation(async (_) => { 57 | // Return the next response from the array. 58 | const fn = responses.shift(); 59 | return fn ? fn() : []; 60 | }); 61 | 62 | { 63 | const result = await run({ skip: 0, limit: 1 }); 64 | const expected = `[0] id: 1, title: Post One`; 65 | assert.strictEqual(result, expected); 66 | } 67 | { 68 | const result = await run({ skip: 1, limit: 1 }); 69 | const expected = `[0] id: 2, title: Post Two`; 70 | assert.strictEqual(result, expected); 71 | } 72 | { 73 | const result = await run({ skip: 2, limit: 1 }); 74 | const expected = `[0] id: 3, title: Post Three`; 75 | assert.strictEqual(result, expected); 76 | } 77 | 78 | const calls = m.calls; 79 | assert.strictEqual(m.callCount(), 3); 80 | assert.deepStrictEqual(calls[0].arguments[0], { skip: 0, limit: 1 }); 81 | assert.deepStrictEqual(calls[1].arguments[0], { skip: 1, limit: 1 }); 82 | assert.deepStrictEqual(calls[2].arguments[0], { skip: 2, limit: 1 }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test-with-node-testrunner/test/boilerplate-with-mock.test.ts: -------------------------------------------------------------------------------- 1 | // boilerplate-with-mock.test.ts 2 | import { describe, it, afterEach, beforeEach, mock } from 'node:test'; 3 | import assert from 'node:assert'; 4 | 5 | // original value is 1 6 | const result = 1; 7 | 8 | class MyService { 9 | static async myFunction() { 10 | return Promise.resolve(result); 11 | } 12 | } 13 | 14 | describe('boilerplate with mock', () => { 15 | beforeEach(() => { 16 | // Setup required before each test 17 | mock.restoreAll(); 18 | }); 19 | afterEach(() => { 20 | // Cleanup required after each test 21 | }); 22 | 23 | it('should if ', async () => { 24 | // Arrange: Replace the original implementation with a mock, returning 2 25 | const m = mock.method(MyService, 'myFunction').mock; 26 | m.mockImplementation(async () => Promise.resolve(2)); 27 | 28 | // Act: Test the function, but get the mocked value 29 | const result = await MyService.myFunction(); 30 | 31 | // Assert 32 | assert.strictEqual(result, 2); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test-with-node-testrunner/test/boilerplate.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, afterEach, beforeEach, mock } from 'node:test'; 2 | import assert from 'node:assert'; 3 | 4 | describe('boilerplate', () => { 5 | beforeEach(() => { 6 | // Setup required before each test 7 | }); 8 | afterEach(() => { 9 | // Cleanup required after each test 10 | }); 11 | 12 | it('should if ', async () => { 13 | // Arrange 14 | // - set up the test data and the expected result 15 | // Act 16 | // - call the function to test 17 | // Assert 18 | // - check the state: result returned from function 19 | // - check the behavior: dependency function calls 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test-with-node-testrunner/test/fake-in-mem-db.test.ts: -------------------------------------------------------------------------------- 1 | // fake-in-mem-db.spec.ts 2 | import { describe, it, beforeEach, afterEach, mock } from 'node:test'; 3 | import assert from 'node:assert'; 4 | 5 | class FakeDatabase { 6 | private data: Record; 7 | 8 | constructor() { 9 | this.data = {}; 10 | } 11 | 12 | save(key: string, value: any): void { 13 | this.data[key] = value; 14 | } 15 | 16 | get(key: string): any { 17 | return this.data[key]; 18 | } 19 | } 20 | 21 | // Function to test 22 | function someTestFunction(db: FakeDatabase, key: string, value: any): any { 23 | db.save(key, value); 24 | return db.get(key); 25 | } 26 | 27 | describe('In-Mem DB', () => { 28 | let fakeDb: FakeDatabase; 29 | let testKey: string; 30 | let testValue: any; 31 | 32 | beforeEach(() => { 33 | fakeDb = new FakeDatabase(); 34 | testKey = 'testKey'; 35 | testValue = { 36 | first: 'John', 37 | last: 'Jones', 38 | lastUpdated: new Date().toISOString(), 39 | }; 40 | }); 41 | 42 | afterEach(() => { 43 | // Restore all mocks created by node:test’s mock helper. 44 | mock.restoreAll(); 45 | }); 46 | 47 | it('should save and return the correct value', () => { 48 | // Create a spy on the save method using node:test's mock helper. 49 | const saveSpy = mock.method(fakeDb, 'save').mock; 50 | 51 | // Call the function under test. 52 | const result = someTestFunction(fakeDb, testKey, testValue); 53 | 54 | // Verify state. 55 | assert.deepStrictEqual(result, testValue); 56 | assert.strictEqual(result.first, 'John'); 57 | assert.strictEqual(result.last, 'Jones'); 58 | assert.strictEqual(result.lastUpdated, testValue.lastUpdated); 59 | 60 | // Verify behavior 61 | assert.strictEqual(saveSpy.callCount(), 1); 62 | const calls = saveSpy.calls; 63 | assert.deepStrictEqual(calls[0].arguments, [testKey, testValue]); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test-with-node-testrunner/test/insert.test.ts: -------------------------------------------------------------------------------- 1 | // insertDocument.test.ts 2 | import { describe, it, beforeEach, mock } from 'node:test'; 3 | import assert from 'node:assert'; 4 | 5 | import { Container } from '../src/data/connect-to-cosmos.js'; 6 | import { createTestInputAndResult } from '../src/data/fake-data.js'; 7 | import type { DbDocument, DbError, RawInput } from '../src/data/model.js'; 8 | import { isDbError, isVerificationErrors } from '../src/data/model.js'; 9 | 10 | import Verify from '../src/data/verify.js'; 11 | import CosmosConnector from '../src/data/connect-to-cosmos.js'; 12 | import { insertDocument } from '../src/lib/insert.js'; 13 | 14 | describe('SDK', () => { 15 | beforeEach(() => { 16 | // Clear all mocks before each test 17 | mock.restoreAll(); 18 | }); 19 | 20 | it('should return verification error if input is not verified', async () => { 21 | const fakeContainer = { 22 | items: { 23 | create: async (_: any) => { 24 | throw new Error('Create method not implemented'); 25 | }, 26 | }, 27 | } as unknown as Container; 28 | 29 | const mVerify = mock.method(Verify, 'inputVerified').mock; 30 | mVerify.mockImplementation(() => false); 31 | 32 | const mGetUniqueId = mock.method(CosmosConnector, 'getUniqueId').mock; 33 | mGetUniqueId.mockImplementation(() => 'unique-id'); 34 | 35 | const mContainerCreate = mock.method(fakeContainer.items, 'create').mock; 36 | 37 | // Arrange: wrong shape of document on purpose. 38 | const doc = { name: 'test' } as unknown as RawInput; 39 | 40 | // Act: 41 | const insertDocumentResult = await insertDocument(fakeContainer, doc); 42 | 43 | // Assert - State verification. 44 | if (isVerificationErrors(insertDocumentResult)) { 45 | assert.deepStrictEqual(insertDocumentResult, { 46 | message: 'Verification failed', 47 | }); 48 | } else { 49 | throw new Error('Result is not of type VerificationErrors'); 50 | } 51 | 52 | // Assert - Behavior verification: Verify that create was never called. 53 | assert.strictEqual(mContainerCreate.callCount(), 0); 54 | }); 55 | it('should insert document successfully', async () => { 56 | // Arrange: override inputVerified to return true. 57 | const { input, result }: { input: RawInput; result: Partial } = 58 | createTestInputAndResult(); 59 | 60 | const fakeContainer = { 61 | items: { 62 | create: async (doc: any) => { 63 | return { resource: result }; 64 | }, 65 | }, 66 | } as unknown as Container; 67 | 68 | const mVerify = mock.method(Verify, 'inputVerified').mock; 69 | mVerify.mockImplementation(() => true); 70 | 71 | const mContainerCreate = mock.method( 72 | fakeContainer.items as any, 73 | 'create', 74 | ).mock; 75 | mContainerCreate.mockImplementation(async (doc: any) => { 76 | return { resource: result }; 77 | }); 78 | 79 | // Act: 80 | const receivedResult = await insertDocument(fakeContainer, input); 81 | 82 | // Assert - State verification: Ensure the result is as expected. 83 | assert.deepStrictEqual(receivedResult, result); 84 | 85 | // Assert - Behavior verification: Ensure create was called once with correct arguments. 86 | assert.strictEqual(mContainerCreate.callCount(), 1); 87 | assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], { 88 | id: input.id, 89 | name: result.name, 90 | }); 91 | }); 92 | it('should return error if db insert fails', async () => { 93 | // Arrange: override inputVerified to return true. 94 | const { input, result } = createTestInputAndResult(); 95 | const errorMessage: string = 'An unknown error occurred'; 96 | 97 | const fakeContainer = { 98 | items: { 99 | create: async (doc: any): Promise => { 100 | return Promise.resolve(null); 101 | }, 102 | }, 103 | } as unknown as Container; 104 | 105 | const mVerify = mock.method(Verify, 'inputVerified').mock; 106 | mVerify.mockImplementation(() => true); 107 | 108 | const mContainerCreate = mock.method(fakeContainer.items, 'create').mock; 109 | mContainerCreate.mockImplementation(async (doc: any) => { 110 | const mockError: DbError = { 111 | message: errorMessage, 112 | code: 500, 113 | }; 114 | throw mockError; 115 | }); 116 | 117 | // Act: 118 | const insertDocumentResult = await insertDocument(fakeContainer, input); 119 | 120 | // // Assert - Ensure create method was called once with the correct arguments. 121 | assert.strictEqual(isDbError(insertDocumentResult), true); 122 | assert.strictEqual(mContainerCreate.callCount(), 1); 123 | assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], { 124 | id: input.id, 125 | name: result.name, 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /test-with-node-testrunner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", // Specify ECMAScript target version 4 | "module": "NodeNext", // Specify module code generation 5 | "strict": true, // Enable all strict type-checking options 6 | "esModuleInterop": true, // Enables emit interoperability between CommonJS and ES Modules 7 | "noImplicitAny": false, // Enable error reporting for expressions and declarations with an implied 'any' type 8 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file 9 | "outDir": "./dist", 10 | "rootDir": "./", 11 | }, 12 | "include": [ 13 | "src", 14 | "test" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /test-with-vitest/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 2021, 13 | "project": "./tsconfig.json" 14 | }, 15 | "plugins": [ 16 | "jest", 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | "no-console": "off", 21 | "no-noImplicitAny": "off", 22 | "require-await": "error", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/no-unsafe-assignment": "off", 25 | "@typescript-eslint/no-unsafe-argument": "off", 26 | "@typescript-eslint/no-unsafe-member-access": "off", 27 | "@typescript-eslint/unbound-method": "off" 28 | } 29 | } -------------------------------------------------------------------------------- /test-with-vitest/README.md: -------------------------------------------------------------------------------- 1 | # Vitest for the Azure SDK for JavaScript 2 | 3 | This subfolder is the source code for the [How to Test Azure SDK Integration in JavaScript Applications](https://learn.microsoft.com/azure/developer/javascript/sdk/test-sdk-integration). The purpose is to demonstrate unit test mocks for the Azure SDK for JavaScript. These specific examples use Azure Cosmos DB. 4 | 5 | Use Azure CLI to create Cosmos DB resource if you intend to insert a document with the application code against the cloud resource. [Script](../scripts/create-cosmos-db-resources.sh) 6 | 7 | ## To run the test 8 | 9 | 1. `npm install` 10 | 2. `npm run build` 11 | 3. `npm test` 12 | 13 | ```console 14 | > test-with-vitest@1.0.0 test 15 | > vitest run --coverage 16 | 17 | 18 | RUN v3.0.8 /workspaces/node-essentials/test-with-vitest 19 | Coverage enabled with istanbul 20 | 21 | ✓ tests/02-stubs.test.ts (2 tests) 6ms 22 | ✓ tests/boilerplate.test.ts (1 test) 6ms 23 | ✓ tests/boilerplate-with-mock.test.ts (1 test) 5ms 24 | ✓ tests/fake-in-mem-db.test.ts (1 test) 10ms 25 | ✓ tests/01-spies.test.ts (1 test) 7ms 26 | ✓ tests/insert.test.ts (3 tests) 11ms 27 | 28 | Test Files 6 passed (6) 29 | Tests 9 passed (9) 30 | Start at 18:36:27 31 | Duration 14.01s (transform 5.89s, setup 0ms, collect 12.28s, tests 45ms, environment 2ms, prepare 12.26s) 32 | 33 | % Coverage report from istanbul 34 | -----------------------|---------|----------|---------|---------|------------------- 35 | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 36 | -----------------------|---------|----------|---------|---------|------------------- 37 | All files | 36.95 | 31.25 | 42.85 | 36.95 | 38 | src | 0 | 100 | 0 | 0 | 39 | index.ts | 0 | 100 | 0 | 0 | 7-16 40 | src/data | 32.14 | 20 | 50 | 32.14 | 41 | connect-to-cosmos.ts | 0 | 100 | 0 | 0 | 12-37 42 | fake-data.ts | 100 | 100 | 100 | 100 | 43 | model.ts | 25 | 20 | 66.66 | 25 | 26-38 44 | verify.ts | 0 | 100 | 0 | 0 | 6-7 45 | src/lib | 72.72 | 50 | 100 | 72.72 | 46 | insert.ts | 72.72 | 50 | 100 | 72.72 | 30-36 47 | -----------------------|---------|----------|---------|---------|------------------- 48 | ``` 49 | 50 | ## Related content 51 | 52 | * [Passwordless connections for Azure services](https://learn.microsoft.com/azure/developer/intro/passwordless-overview) 53 | * [Cosmos DB keyless access to the service](https://learn.microsoft.com/azure/cosmos-db/role-based-access-control) 54 | * [Vitest](https://main.vitest.dev/) -------------------------------------------------------------------------------- /test-with-vitest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-with-vitest", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "prebuild": "npm run format && npm run lint", 8 | "build": "rm -rf dist && tsc", 9 | "test": "vitest run --coverage", 10 | "test2": "node --trace-exit ./node_modules/vitest/vitest.js run --coverage", 11 | "lint": "eslint \"./src/**/*.ts\" --fix", 12 | "format": "prettier --write './**/*.{ts,tsx}" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "description": "", 18 | "devDependencies": { 19 | "@types/node": "^22.13.10", 20 | "@typescript-eslint/eslint-plugin": "^8.2.0", 21 | "@vitest/coverage-istanbul": "^3.0.8", 22 | "eslint-config-airbnb-base": "^15.0.0", 23 | "eslint-plugin-import": "^2.29.1", 24 | "eslint-plugin-jest": "^28.8.0", 25 | "eslint": "^8.56.0", 26 | "prettier": "^3.2.4", 27 | "typescript": "^5.5.4", 28 | "vitest": "^3.0.8" 29 | }, 30 | "dependencies": { 31 | "@azure/cosmos": "^4.1.0", 32 | "@azure/identity": "^4.4.1", 33 | "dotenv": "^16.4.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test-with-vitest/src/data/connect-to-cosmos.ts: -------------------------------------------------------------------------------- 1 | // connect-to-cosmos.ts 2 | 3 | import { Container, CosmosClient } from '@azure/cosmos'; 4 | import { DefaultAzureCredential } from '@azure/identity'; 5 | import 'dotenv/config'; 6 | import { randomUUID } from 'crypto'; 7 | 8 | export { Container }; 9 | 10 | export default class CosmosConnector { 11 | static connectToCosmosWithoutKey(): CosmosClient { 12 | const endpoint = process.env.COSMOS_DB_ENDPOINT!; 13 | const credential = new DefaultAzureCredential(); 14 | const client = new CosmosClient({ endpoint, aadCredentials: credential }); 15 | return client; 16 | } 17 | 18 | static async connectToContainer(): Promise { 19 | const client = CosmosConnector.connectToCosmosWithoutKey(); 20 | const databaseName = process.env.COSMOS_DATABASE_NAME!; 21 | const containerName = process.env.COSMOS_CONTAINER_NAME!; 22 | 23 | // Ensure the database exists 24 | const { database } = await client.databases.createIfNotExists({ 25 | id: databaseName, 26 | }); 27 | 28 | // Ensure the container exists 29 | const { container } = await database.containers.createIfNotExists({ 30 | id: containerName, 31 | }); 32 | 33 | return container; 34 | } 35 | 36 | static getUniqueId(): string { 37 | return randomUUID(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test-with-vitest/src/data/fake-data.ts: -------------------------------------------------------------------------------- 1 | import type { DbDocument, RawInput } from './model.js'; 2 | import { randomUUID } from 'crypto'; 3 | 4 | // Predefined arrays of first and last names 5 | const firstNames = ['Alice', 'Bob', 'Charlie', 'Dana', 'Eve', 'Frank']; 6 | const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller']; 7 | 8 | function getRandomElement(arr: T[]): T { 9 | const randomIndex = Math.floor(Math.random() * arr.length); 10 | return arr[randomIndex]; 11 | } 12 | 13 | function createFixture(): T { 14 | const result = { 15 | first: getRandomElement(firstNames), 16 | last: getRandomElement(lastNames), 17 | }; 18 | return result as T; 19 | } 20 | 21 | export function createTestInput(): RawInput { 22 | const { first, last } = createFixture(); 23 | return { id: randomUUID(), first, last }; 24 | } 25 | 26 | export function createTestInputAndResult(): { 27 | input: RawInput; 28 | result: Partial; 29 | } { 30 | const input = createTestInput(); 31 | const result = { 32 | id: input.id, 33 | name: `${input.first} ${input.last}`, 34 | }; 35 | return { input, result }; 36 | } 37 | -------------------------------------------------------------------------------- /test-with-vitest/src/data/model.ts: -------------------------------------------------------------------------------- 1 | // input-verified.ts 2 | export interface DbDocument { 3 | id: string; 4 | name: string; 5 | } 6 | export interface DbError { 7 | message: string; 8 | code: number; 9 | } 10 | export interface VerificationErrors { 11 | message: string; 12 | } 13 | export interface RawInput { 14 | id: string; 15 | first: string; 16 | last: string; 17 | } 18 | 19 | export function isDbError(error: any): error is DbError { 20 | return 'message' in error && 'code' in error; 21 | } 22 | export function isVerificationErrors(error: any): error is VerificationErrors { 23 | return 'message' in error; 24 | } 25 | export function validateRawInput(input: any): string[] { 26 | const errors: string[] = []; 27 | 28 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 29 | if (typeof input.first !== 'string' || input.first.trim().length === 0) { 30 | errors.push('First name is required and must be a non-empty string'); 31 | } 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 34 | if (typeof input.last !== 'string' || input.last.trim().length === 0) { 35 | errors.push('Last name is required and must be a non-empty string'); 36 | } 37 | 38 | return errors; 39 | } 40 | -------------------------------------------------------------------------------- /test-with-vitest/src/data/verify.ts: -------------------------------------------------------------------------------- 1 | // input-verified.ts 2 | import { validateRawInput } from './model.js'; 3 | 4 | export default class Verfiy { 5 | static inputVerified(doc: any): boolean { 6 | const result = validateRawInput(doc); 7 | return result.length === 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test-with-vitest/src/index.ts: -------------------------------------------------------------------------------- 1 | import CosmosConnector from './data/connect-to-cosmos.js'; 2 | import { createTestInput } from './data/fake-data.js'; 3 | import { insertDocument } from './lib/insert.js'; 4 | try { 5 | const container = await CosmosConnector.connectToContainer(); 6 | const input = createTestInput(); 7 | const result = await insertDocument(container, input); 8 | console.log(result); 9 | } catch (error) { 10 | console.error(error); 11 | process.exit(1); 12 | } 13 | -------------------------------------------------------------------------------- /test-with-vitest/src/lib/insert.ts: -------------------------------------------------------------------------------- 1 | // insertDocument.ts 2 | import { Container } from '../data/connect-to-cosmos.js'; 3 | import type { 4 | DbDocument, 5 | DbError, 6 | RawInput, 7 | VerificationErrors, 8 | } from '../data/model.js'; 9 | import Verify from '../data/verify.js'; 10 | 11 | export async function insertDocument( 12 | container: Container, 13 | doc: RawInput, 14 | ): Promise { 15 | const isVerified: boolean = Verify.inputVerified(doc); 16 | 17 | if (!isVerified) { 18 | return { message: 'Verification failed' } as VerificationErrors; 19 | } 20 | 21 | try { 22 | const { resource } = await container.items.create({ 23 | id: doc.id, 24 | name: `${doc.first} ${doc.last}`, 25 | }); 26 | 27 | return resource as DbDocument; 28 | } catch (error: any) { 29 | if (error instanceof Error) { 30 | if ((error as any).code === 409) { 31 | return { 32 | message: 'Insertion failed: Duplicate entry', 33 | code: 409, 34 | } as DbError; 35 | } 36 | return { message: error.message, code: (error as any).code } as DbError; 37 | } else { 38 | return { message: 'An unknown error occurred', code: 500 } as DbError; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test-with-vitest/tests/01-spies.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect, vi } from 'vitest'; 2 | 3 | function run({ 4 | fn, 5 | times, 6 | }: { 7 | fn: (arg: { current: number }) => void; 8 | times: number; 9 | }) { 10 | for (let i = 0; i < times; i++) { 11 | fn({ current: i * 5 }); 12 | } 13 | } 14 | 15 | it('Spies: should verify calls in a mock', () => { 16 | const spy = vi.fn(); 17 | run({ fn: spy, times: 3 }); 18 | 19 | expect(spy).toHaveBeenCalledTimes(3); 20 | expect(spy.mock.calls[0][0]).toEqual({ current: 0 }); 21 | expect(spy.mock.calls[1][0]).toEqual({ current: 5 }); 22 | expect(spy.mock.calls[2][0]).toEqual({ current: 10 }); 23 | }); 24 | -------------------------------------------------------------------------------- /test-with-vitest/tests/02-stubs.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; 2 | 3 | class Service { 4 | static async getTalks({ 5 | skip, 6 | limit, 7 | }: { 8 | skip: number; 9 | limit: number; 10 | }): Promise { 11 | // Use a public API (jsonplaceholder) to retrieve posts with pagination. 12 | const url = `https://jsonplaceholder.typicode.com/posts?_start=${skip}&_limit=${limit}`; 13 | const response = await fetch(url); 14 | return response.json(); 15 | } 16 | } 17 | 18 | function mapResponse(data: any[]): string { 19 | return data 20 | .map(({ id, title }, index) => `[${index}] id: ${id}, title: ${title}`) 21 | .join('\n'); 22 | } 23 | 24 | async function run({ skip = 0, limit = 10 } = {}): Promise { 25 | const talks = mapResponse(await Service.getTalks({ skip, limit })); 26 | return talks; 27 | } 28 | 29 | describe('Stub Test Suite', () => { 30 | beforeEach(() => { 31 | vi.restoreAllMocks(); 32 | }); 33 | 34 | afterEach(() => { 35 | // Optional cleanup if needed. 36 | }); 37 | 38 | it('should stub APIs', async () => { 39 | // Stub Service.getTalks so that it returns one fake post. 40 | const m = vi.spyOn(Service, 'getTalks'); 41 | m.mockImplementation(async () => [ 42 | { 43 | id: '1', 44 | title: 'Sample Post', 45 | }, 46 | ]); 47 | 48 | const result = await run({ limit: 1 }); 49 | const expected = `[0] id: 1, title: Sample Post`; 50 | 51 | // Verify that the stubbed method was called once. 52 | expect(m).toHaveBeenCalledTimes(1); 53 | expect(m.mock.calls[0][0]).toEqual({ skip: 0, limit: 1 }); 54 | expect(result).toBe(expected); 55 | }); 56 | 57 | it('should stub different values for API calls', async () => { 58 | // Instead of chaining multiple mockImplementationOnce calls, build a responses array: 59 | const m = vi.spyOn(Service, 'getTalks'); 60 | const responses = [ 61 | async () => [{ id: '1', title: 'Post One' }], 62 | async () => [{ id: '2', title: 'Post Two' }], 63 | async () => [{ id: '3', title: 'Post Three' }], 64 | ]; 65 | 66 | m.mockImplementation(async (args) => { 67 | // Return the next response from the array. 68 | const fn = responses.shift(); 69 | return fn ? await fn() : []; 70 | }); 71 | 72 | { 73 | const result = await run({ skip: 0, limit: 1 }); 74 | const expected = `[0] id: 1, title: Post One`; 75 | expect(result).toBe(expected); 76 | } 77 | { 78 | const result = await run({ skip: 1, limit: 1 }); 79 | const expected = `[0] id: 2, title: Post Two`; 80 | expect(result).toBe(expected); 81 | } 82 | { 83 | const result = await run({ skip: 2, limit: 1 }); 84 | const expected = `[0] id: 3, title: Post Three`; 85 | expect(result).toBe(expected); 86 | } 87 | 88 | // Verify call count and arguments. 89 | expect(m).toHaveBeenCalledTimes(3); 90 | expect(m.mock.calls[0][0]).toEqual({ skip: 0, limit: 1 }); 91 | expect(m.mock.calls[1][0]).toEqual({ skip: 1, limit: 1 }); 92 | expect(m.mock.calls[2][0]).toEqual({ skip: 2, limit: 1 }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test-with-vitest/tests/boilerplate-with-mock.test.ts: -------------------------------------------------------------------------------- 1 | // boilerplate-with-mock.test.ts 2 | import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; 3 | 4 | const result = 1; 5 | 6 | class MyService { 7 | static async myFunction() { 8 | return Promise.resolve(result); 9 | } 10 | } 11 | 12 | describe('boilerplate with mock', () => { 13 | beforeEach(() => { 14 | // Restore all mocks before each test. 15 | vi.restoreAllMocks(); 16 | }); 17 | 18 | afterEach(() => { 19 | // Cleanup required after each test (if needed). 20 | }); 21 | 22 | it('should if ', async () => { 23 | // Arrange: Replace the original implementation with a mock, returning 2. 24 | vi.spyOn(MyService, 'myFunction').mockResolvedValue(2); 25 | 26 | // Act: Test the function: it should now return the mocked value. 27 | const resultVal = await MyService.myFunction(); 28 | 29 | // Assert 30 | expect(resultVal).toBe(2); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test-with-vitest/tests/boilerplate.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; 2 | 3 | describe('boilerplate', () => { 4 | beforeEach(() => { 5 | // Setup required before each test 6 | }); 7 | 8 | afterEach(() => { 9 | // Cleanup required after each test 10 | }); 11 | 12 | it('should if ', async () => { 13 | // Arrange 14 | // - set up the test data and the expected result 15 | // Act 16 | // - call the function to test 17 | // Assert 18 | // - check the state: result returned from function 19 | // - check the behavior: dependency function calls 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test-with-vitest/tests/fake-in-mem-db.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; 2 | 3 | class FakeDatabase { 4 | private data: Record; 5 | 6 | constructor() { 7 | this.data = {}; 8 | } 9 | 10 | save(key: string, value: any): void { 11 | this.data[key] = value; 12 | } 13 | 14 | get(key: string): any { 15 | return this.data[key]; 16 | } 17 | } 18 | 19 | // Function to test 20 | function someTestFunction(db: FakeDatabase, key: string, value: any): any { 21 | db.save(key, value); 22 | return db.get(key); 23 | } 24 | 25 | describe('In-Mem DB', () => { 26 | let fakeDb: FakeDatabase; 27 | let testKey: string; 28 | let testValue: any; 29 | let saveSpy: ReturnType; 30 | 31 | beforeEach(() => { 32 | fakeDb = new FakeDatabase(); 33 | testKey = 'testKey'; 34 | testValue = { 35 | first: 'John', 36 | last: 'Jones', 37 | lastUpdated: new Date().toISOString(), 38 | }; 39 | 40 | // Create a spy on the save method. 41 | saveSpy = vi.spyOn(fakeDb, 'save'); 42 | }); 43 | 44 | afterEach(() => { 45 | // Restore the spied methods. 46 | vi.restoreAllMocks(); 47 | }); 48 | 49 | it('should save and return the correct value', () => { 50 | // Call the function under test. 51 | const result = someTestFunction(fakeDb, testKey, testValue); 52 | 53 | // Verify state. 54 | expect(result).toEqual(testValue); 55 | expect(result.first).toBe('John'); 56 | expect(result.last).toBe('Jones'); 57 | expect(result.lastUpdated).toBe(testValue.lastUpdated); 58 | 59 | // Verify behavior using vi.spyOn. 60 | expect(saveSpy).toHaveBeenCalledTimes(1); 61 | expect(saveSpy).toHaveBeenCalledWith(testKey, testValue); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test-with-vitest/tests/insert.test.ts: -------------------------------------------------------------------------------- 1 | // insertDocument.test.ts 2 | import { describe, it, beforeEach, expect, vi } from 'vitest'; 3 | import type { Container, ItemResponse } from '@azure/cosmos'; 4 | 5 | import { insertDocument } from '../src/lib/insert.js'; 6 | import { createTestInputAndResult } from '../src/data/fake-data.js'; 7 | import type { DbDocument, DbError, RawInput } from '../src/data/model.js'; 8 | import { isDbError, isVerificationErrors } from '../src/data/model.js'; 9 | import Verify from '../src/data/verify.js'; 10 | 11 | describe('insertDocument', () => { 12 | let fakeContainer: Container; 13 | 14 | beforeEach(() => { 15 | // Clear all mocks before each test 16 | vi.restoreAllMocks(); 17 | 18 | // Create a fake container with a mocked `create` function 19 | fakeContainer = { 20 | items: { 21 | create: vi.fn(), 22 | }, 23 | } as unknown as Container; 24 | }); 25 | 26 | it('should return verification error if input is not verified', async () => { 27 | // Arrange – mock the input verification function to return false. 28 | const inputVerifiedMock = vi.spyOn(Verify, 'inputVerified'); 29 | inputVerifiedMock.mockReturnValue(false); 30 | 31 | const doc = { name: 'test' }; 32 | 33 | // Act – call the function under test. 34 | const insertDocumentResult = await insertDocument( 35 | fakeContainer, 36 | doc as unknown as RawInput, 37 | ); 38 | 39 | // Assert – state verification: result should indicate verification failure. 40 | if (isVerificationErrors(insertDocumentResult)) { 41 | expect(insertDocumentResult).toEqual({ 42 | message: 'Verification failed', 43 | } as unknown as DbError); 44 | } else { 45 | throw new Error('Result is not of type VerificationErrors'); 46 | } 47 | 48 | // Assert – behavior verification: ensure create method was not called. 49 | expect(fakeContainer.items.create).not.toHaveBeenCalled(); 50 | expect(inputVerifiedMock).toHaveBeenCalledTimes(1); 51 | }); 52 | 53 | it('should insert document successfully', async () => { 54 | // Prepare test data 55 | const { input, result }: { input: RawInput; result: Partial } = 56 | createTestInputAndResult(); 57 | const inputVerifiedMock = vi.spyOn(Verify, 'inputVerified'); 58 | inputVerifiedMock.mockReturnValue(true); 59 | 60 | // Set up the mocked return value. 61 | // Here we "cast" our minimal object to satisfy the expected type ItemResponse. 62 | ( 63 | fakeContainer.items.create as unknown as ReturnType 64 | ).mockResolvedValue({ 65 | resource: result, 66 | // Minimal additional properties required by ItemResponse. 67 | item: result, 68 | headers: {}, 69 | statusCode: 201, 70 | diagnostics: {} as any, 71 | requestCharge: 0, 72 | activityId: 'fake-activity-id', 73 | } as unknown as ItemResponse); 74 | 75 | // Call the function under test that internally calls container.items.create. 76 | const insertDocumentResult = await insertDocument(fakeContainer, input); 77 | 78 | // Validate the returned value. 79 | expect(insertDocumentResult).toEqual(result); 80 | 81 | // Validate that create was called once with the proper arguments. 82 | expect(inputVerifiedMock).toHaveBeenCalledTimes(1); 83 | expect(fakeContainer.items.create).toHaveBeenCalledTimes(1); 84 | expect( 85 | (fakeContainer.items.create as unknown as ReturnType).mock 86 | .calls[0][0], 87 | ).toEqual({ 88 | id: input.id, 89 | name: result.name, 90 | }); 91 | }); 92 | 93 | it('should return error if db insert fails', async () => { 94 | // Arrange – create input and expected result data. 95 | const { input, result } = createTestInputAndResult(); 96 | 97 | // Arrange – mock the input verification to return true. 98 | const inputVerifiedMock = vi.spyOn(Verify, 'inputVerified'); 99 | inputVerifiedMock.mockReturnValue(true); 100 | 101 | // Arrange – mock the create method to reject with an error. 102 | const mockError: DbError = { 103 | message: 'An unknown error occurred', 104 | code: 500, 105 | }; 106 | 107 | ( 108 | fakeContainer.items.create as unknown as ReturnType 109 | ).mockRejectedValue(mockError as unknown as DbError); 110 | 111 | // Act – call the function under test. 112 | const insertDocumentResult = await insertDocument(fakeContainer, input); 113 | 114 | // Assert – verify result is of type DbError. 115 | if (isDbError(insertDocumentResult)) { 116 | expect(insertDocumentResult.message).toBe(mockError.message); 117 | } else { 118 | throw new Error('Result is not of type DbError'); 119 | } 120 | 121 | // Assert – behavior verification: ensure create was called once with correct arguments. 122 | expect(inputVerifiedMock).toHaveBeenCalledTimes(1); 123 | expect(fakeContainer.items.create).toHaveBeenCalledTimes(1); 124 | expect( 125 | (fakeContainer.items.create as unknown as ReturnType).mock 126 | .calls[0][0], 127 | ).toEqual({ 128 | id: input.id, 129 | name: result.name, 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /test-with-vitest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", // Specify ECMAScript target version 4 | "module": "NodeNext", // Specify module code generation 5 | "strict": true, // Enable all strict type-checking options 6 | "esModuleInterop": true, // Enables emit interoperability between CommonJS and ES Modules 7 | "noImplicitAny": false, // Enable error reporting for expressions and declarations with an implied 'any' type 8 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file 9 | "outDir": "./dist", 10 | "rootDir": "./", 11 | "erasableSyntaxOnly": true, 12 | }, 13 | "include": ["src", "tests"], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } -------------------------------------------------------------------------------- /test-with-vitest/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'node', 6 | include: ['tests/**/*.test.ts'], 7 | coverage: { 8 | provider: 'istanbul', // Alternatively, you can use "c8" 9 | reporter: ['text', 'lcov'], 10 | // Optionally add thresholds here: 11 | // lines: 80, 12 | // functions: 80, 13 | // branches: 80, 14 | // statements: 80, 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /unit-testing/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 2021, 13 | "project": "./tsconfig.json" 14 | }, 15 | "plugins": [ 16 | "jest", 17 | "@typescript-eslint" 18 | ], 19 | "rules": { 20 | "no-console": "off", 21 | "no-noImplicitAny": "off", 22 | "require-await": "error", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/no-unsafe-assignment": "off", 25 | "@typescript-eslint/no-unsafe-argument": "off", 26 | "@typescript-eslint/no-unsafe-member-access": "off", 27 | "@typescript-eslint/unbound-method": "off" 28 | } 29 | } -------------------------------------------------------------------------------- /unit-testing/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env* 3 | dist -------------------------------------------------------------------------------- /unit-testing/README.md: -------------------------------------------------------------------------------- 1 | # Unit testing for the Azure SDK for JavaScript 2 | 3 | This subfolder is the source code for the [TBD article](). The purpose is to demonstrate unit test mocks for the Azure SDK for JavaScript. These specific examples use Azure Cosmos DB. 4 | 5 | ## To run the test 6 | 7 | 1. `npm install` 8 | 2. `npm run build` 9 | 3. `npm test` 10 | 11 | ```console 12 | > unit-testing@1.0.0 test 13 | > jest dist 14 | 15 | PASS dist/fakes/fake-in-mem-db.spec.js 16 | PASS dist/mock-function/lib/insert.spec.js 17 | 18 | Test Suites: 2 passed, 2 total 19 | Tests: 4 passed, 4 total 20 | Snapshots: 0 total 21 | Time: 4.247 s, estimated 5 s 22 | Ran all test suites matching /dist/i. 23 | ``` 24 | 25 | ## Related content 26 | 27 | * [Passwordless connections for Azure services](https://learn.microsoft.com/azure/developer/intro/passwordless-overview) 28 | * [Cosmos DB keyless access to the service](https://learn.microsoft.com/azure/cosmos-db/role-based-access-control) -------------------------------------------------------------------------------- /unit-testing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unit-testing", 3 | "version": "1.0.0", 4 | "description": "This subfolder is the source code for the [TBD article](). The purpose is to demonstrate unit test mocks for the Azure SDK for JavaScript. These specific examples use Azure Cosmos DB.", 5 | "main": "index.js", 6 | "scripts": { 7 | "clear": "rm -rf dist && rm -rf coverage", 8 | "start": "node dist/mock-function/index.js", 9 | "test": "jest dist", 10 | "test:coverage": "jest dist --coverage", 11 | "build": "npm run clear && tsc", 12 | "lint": "eslint \"./src/**/*.ts\" --fix", 13 | "format": "prettier \"./src/**/*.ts\" --write", 14 | "precommit": "npm run format && git add . && npm run lint" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@azure/cosmos": "^4.1.0", 21 | "@azure/identity": "^4.4.1", 22 | "dotenv": "^16.4.5", 23 | "uuid": "^10.0.0" 24 | }, 25 | "devDependencies": { 26 | "@faker-js/faker": "^8.4.1", 27 | "@types/jest": "^29.5.12", 28 | "@types/node": "^22.4.0", 29 | "@types/uuid": "^10.0.0", 30 | "@typescript-eslint/eslint-plugin": "^8.2.0", 31 | "eslint": "^8.56.0", 32 | "eslint-config-airbnb-base": "^15.0.0", 33 | "eslint-plugin-import": "^2.29.1", 34 | "eslint-plugin-jest": "^28.8.0", 35 | "husky": "^9.0.10", 36 | "jest": "^29.7.0", 37 | "prettier": "^3.2.4", 38 | "typescript": "^5.5.4" 39 | }, 40 | "husky": { 41 | "hooks": { 42 | "pre-commit": "npm run precommit" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /unit-testing/sample.env: -------------------------------------------------------------------------------- 1 | SUBSCRIPTION_ID= 2 | RESOURCE_GROUP_NAME="my-resource-group 3 | LOCATION=eastus2 4 | COSMOS_DATABASE_NAME="my-db" 5 | COSMOS_CONTAINER_NAME="my-container" 6 | 7 | ## ./scripts/create-resources.sh adds the COSMOS DB endpoint 8 | # The endpoint URL for your Azure Cosmos DB account 9 | #COSMOS_ENDPOINT= 10 | -------------------------------------------------------------------------------- /unit-testing/scripts/create-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Prerequisites: 4 | # Azure CLI installed 5 | # `az login` 6 | 7 | # Read .env file in the script 8 | set -a 9 | source ../.env 10 | 11 | random_string() { 12 | cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 10 | head -n 1 13 | } 14 | RANDOM_STRING=$(random_string) 15 | 16 | printf "Subscription ID: $SUBSCRIPTION_ID\n" 17 | printf "Location: $LOCATION\n" 18 | printf "Cosmos DB Database: $COSMOS_DATABASE_NAME\n" 19 | printf "Cosmos DB Container name: $COSMOS_CONTAINER_NAME\n" 20 | 21 | RG="$RESOURCE_GROUP_NAME$RANDOM_STRING" 22 | printf "Resource Group: $RG\n" 23 | 24 | RESOURCE_NAME="$COSMOS_DB_RESOURCE_NAME$RANDOM_STRING" 25 | printf "Resource Name: $RESOURCE_NAME\n" 26 | 27 | # Create a resource group 28 | az group create \ 29 | --subscription $SUBSCRIPTION_ID \ 30 | --name "$RG" \ 31 | --location $LOCATION 32 | printf "Resource Group created\n" 33 | 34 | # Create a Cosmos DB account 35 | az cosmosdb create \ 36 | --subscription $SUBSCRIPTION_ID \ 37 | --resource-group $RG \ 38 | --name $RESOURCE_NAME \ 39 | --kind MongoDB \ 40 | --locations regionName=$LOCATION failoverPriority=0 isZoneRedundant=False 41 | printf "Cosmos DB account created\n" 42 | 43 | # Get the Cosmos DB account endpoint 44 | COSMOS_DB_ENDPOINT=$(az cosmosdb show \ 45 | --subscription $SUBSCRIPTION_ID \ 46 | --resource-group $RG \ 47 | --name $RESOURCE_NAME \ 48 | --query "documentEndpoint" \ 49 | --output tsv) 50 | printf "Cosmos DB Endpoint: $COSMOS_DB_ENDPOINT\n" 51 | 52 | # Append the endpoint to the .env file 53 | echo "COSMOS_DB_ENDPOINT=$COSMOS_DB_ENDPOINT" >> ../.env 54 | 55 | echo "Cosmos DB Endpoint: $COSMOS_DB_ENDPOINT" 56 | -------------------------------------------------------------------------------- /unit-testing/src/fakes/fake-in-mem-db.spec.ts: -------------------------------------------------------------------------------- 1 | // fake-in-mem-db.spec.ts 2 | class FakeDatabase { 3 | private data: Record; 4 | 5 | constructor() { 6 | this.data = {}; 7 | } 8 | 9 | save(key: string, value: any): void { 10 | this.data[key] = value; 11 | } 12 | 13 | get(key: string): any { 14 | return this.data[key]; 15 | } 16 | } 17 | 18 | // Function to test 19 | function someTestFunction(db: FakeDatabase, key: string, value: any): any { 20 | db.save(key, value); 21 | return db.get(key); 22 | } 23 | 24 | // Jest test suite 25 | describe('someTestFunction', () => { 26 | let fakeDb: FakeDatabase; 27 | let testKey: string; 28 | let testValue: any; 29 | 30 | beforeEach(() => { 31 | fakeDb = new FakeDatabase(); 32 | testKey = 'testKey'; 33 | testValue = { 34 | first: 'John', 35 | last: 'Jones', 36 | lastUpdated: new Date().toISOString(), 37 | }; 38 | 39 | // Spy on the save method 40 | jest.spyOn(fakeDb, 'save'); 41 | }); 42 | 43 | afterEach(() => { 44 | // Clear all mocks 45 | jest.clearAllMocks(); 46 | }); 47 | 48 | test('should save and return the correct value', () => { 49 | // Perform test 50 | const result = someTestFunction(fakeDb, testKey, testValue); 51 | 52 | // Verify state 53 | expect(result).toEqual(testValue); 54 | expect(result.first).toBe('John'); 55 | expect(result.last).toBe('Jones'); 56 | expect(result.lastUpdated).toBe(testValue.lastUpdated); 57 | 58 | // Verify behavior 59 | expect(fakeDb.save).toHaveBeenCalledWith(testKey, testValue); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /unit-testing/src/mock-function/data/connect-to-cosmos.ts: -------------------------------------------------------------------------------- 1 | // connect-to-cosmos.ts 2 | 3 | import { Container, CosmosClient } from '@azure/cosmos'; 4 | import { DefaultAzureCredential } from '@azure/identity'; 5 | import 'dotenv/config'; 6 | import { v4 as uuidv4 } from 'uuid'; 7 | 8 | export { Container }; 9 | 10 | export function connectToCosmosWithoutKey() { 11 | const endpoint = process.env.COSMOS_DB_ENDPOINT!; 12 | const credential = new DefaultAzureCredential(); 13 | 14 | const client = new CosmosClient({ endpoint, aadCredentials: credential }); 15 | return client; 16 | } 17 | export async function connectToContainer(): Promise { 18 | const client = connectToCosmosWithoutKey(); 19 | const databaseName = process.env.COSMOS_DATABASE_NAME; 20 | const containerName = process.env.COSMOS_CONTAINER_NAME; 21 | 22 | // Ensure the database exists 23 | const { database } = await client.databases.createIfNotExists({ 24 | id: databaseName, 25 | }); 26 | 27 | // Ensure the container exists 28 | const { container } = await database.containers.createIfNotExists({ 29 | id: containerName, 30 | }); 31 | 32 | return container; 33 | } 34 | export function getUniqueId(): string { 35 | return uuidv4(); 36 | } 37 | -------------------------------------------------------------------------------- /unit-testing/src/mock-function/data/fake-data.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { faker } from '@faker-js/faker'; 3 | import { DbDocument, RawInput } from './model'; 4 | 5 | function createFixture(): T { 6 | const result = { 7 | first: faker.person.firstName(), 8 | last: faker.person.lastName(), 9 | }; 10 | return result as T; 11 | } 12 | 13 | export function createTestInput(): RawInput { 14 | const { first, last } = createFixture(); 15 | return { id: uuidv4(), first, last }; 16 | } 17 | 18 | export function createTestInputAndResult(): { 19 | input: RawInput; 20 | result: Partial; 21 | } { 22 | const input = createTestInput(); 23 | const result = { 24 | id: input.id, 25 | name: `${input.first} ${input.last}`, 26 | }; 27 | return { input, result }; 28 | } 29 | -------------------------------------------------------------------------------- /unit-testing/src/mock-function/data/model.ts: -------------------------------------------------------------------------------- 1 | // input-verified.ts 2 | export interface DbDocument { 3 | id: string; 4 | name: string; 5 | } 6 | 7 | export interface DbError { 8 | message: string; 9 | code: number; 10 | } 11 | export function isDbError(error: any): error is DbError { 12 | return 'message' in error && 'code' in error; 13 | } 14 | 15 | export interface VerificationErrors { 16 | message: string; 17 | } 18 | export function isVerificationErrors(error: any): error is VerificationErrors { 19 | return 'message' in error; 20 | } 21 | 22 | export interface RawInput { 23 | id: string; 24 | first: string; 25 | last: string; 26 | } 27 | 28 | export function validateRawInput(input: any): string[] { 29 | const errors: string[] = []; 30 | 31 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 32 | if (typeof input.first !== 'string' || input.first.trim().length === 0) { 33 | errors.push('First name is required and must be a non-empty string'); 34 | } 35 | 36 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 37 | if (typeof input.last !== 'string' || input.last.trim().length === 0) { 38 | errors.push('Last name is required and must be a non-empty string'); 39 | } 40 | 41 | return errors; 42 | } 43 | -------------------------------------------------------------------------------- /unit-testing/src/mock-function/data/verify.ts: -------------------------------------------------------------------------------- 1 | // input-verified.ts 2 | import { validateRawInput } from './model'; 3 | 4 | export function inputVerified(doc: any): boolean { 5 | const result = validateRawInput(doc); 6 | return result.length === 0 ? true : false; 7 | } 8 | -------------------------------------------------------------------------------- /unit-testing/src/mock-function/index.ts: -------------------------------------------------------------------------------- 1 | import { connectToContainer } from './data/connect-to-cosmos'; 2 | import { createTestInput } from './data/fake-data'; 3 | import { DbDocument, DbError, VerificationErrors } from './data/model'; 4 | import { insertDocument } from './lib/insert'; 5 | 6 | async function main(): Promise { 7 | const container = await connectToContainer(); 8 | const input = createTestInput(); 9 | return await insertDocument(container, input); 10 | } 11 | 12 | main() 13 | .then((doc) => console.log(doc)) 14 | .catch((error) => { 15 | console.error(error); 16 | process.exit(1); 17 | }); 18 | -------------------------------------------------------------------------------- /unit-testing/src/mock-function/lib/insert.spec.ts: -------------------------------------------------------------------------------- 1 | // insertDocument.test.ts 2 | import { Container } from '../data/connect-to-cosmos'; 3 | import { createTestInputAndResult } from '../data/fake-data'; 4 | import { 5 | DbDocument, 6 | DbError, 7 | isDbError, 8 | isVerificationErrors, 9 | RawInput, 10 | } from '../data/model'; 11 | import { inputVerified } from '../data/verify'; 12 | import { insertDocument } from './insert'; 13 | 14 | // Mock app dependencies for Cosmos DB setup 15 | jest.mock('../data/connect-to-cosmos', () => ({ 16 | connectToContainer: jest.fn(), 17 | getUniqueId: jest.fn().mockReturnValue('unique-id'), 18 | })); 19 | 20 | // Mock app dependencies for input verification 21 | jest.mock('../data/verify', () => ({ 22 | inputVerified: jest.fn(), 23 | })); 24 | 25 | describe('insertDocument', () => { 26 | // Mock the Cosmo DB Container object 27 | let mockContainer: jest.Mocked; 28 | 29 | beforeEach(() => { 30 | // Clear all mocks before each test 31 | jest.clearAllMocks(); 32 | 33 | // Mock the Cosmos DB Container create method 34 | mockContainer = { 35 | items: { 36 | create: jest.fn(), 37 | }, 38 | } as unknown as jest.Mocked; 39 | }); 40 | 41 | it('should return verification error if input is not verified', async () => { 42 | // Arrange - Mock the input verification function to return false 43 | jest.mocked(inputVerified).mockReturnValue(false); 44 | 45 | // Arrange - wrong shape of doc on purpose 46 | const doc = { name: 'test' }; 47 | 48 | // Act - Call the function to test 49 | const insertDocumentResult = await insertDocument( 50 | mockContainer, 51 | doc as unknown as RawInput, 52 | ); 53 | 54 | // Assert - State verification: Check the result when verification fails 55 | if (isVerificationErrors(insertDocumentResult)) { 56 | expect(insertDocumentResult).toEqual({ 57 | message: 'Verification failed', 58 | }); 59 | } else { 60 | throw new Error('Result is not of type VerificationErrors'); 61 | } 62 | 63 | // Assert - Behavior verification: Ensure create method was not called 64 | expect(mockContainer.items.create).not.toHaveBeenCalled(); 65 | }); 66 | 67 | it('should insert document successfully', async () => { 68 | // Arrange - create input and expected result data 69 | const { input, result }: { input: RawInput; result: Partial } = 70 | createTestInputAndResult(); 71 | 72 | // Arrange - mock the input verification function to return true 73 | (inputVerified as jest.Mock).mockReturnValue(true); 74 | (mockContainer.items.create as jest.Mock).mockResolvedValue({ 75 | resource: result, 76 | }); 77 | 78 | // Act - Call the function to test 79 | const insertDocumentResult = await insertDocument(mockContainer, input); 80 | 81 | // Assert - State verification: Check the result when insertion is successful 82 | expect(insertDocumentResult).toEqual(result); 83 | 84 | // Assert - Behavior verification: Ensure create method was called with correct arguments 85 | expect(mockContainer.items.create).toHaveBeenCalledTimes(1); 86 | expect(mockContainer.items.create).toHaveBeenCalledWith({ 87 | id: input.id, 88 | name: result.name, 89 | }); 90 | }); 91 | 92 | it('should return error if db insert fails', async () => { 93 | // Arrange - create input and expected result data 94 | const { input, result } = createTestInputAndResult(); 95 | 96 | // Arrange - mock the input verification function to return true 97 | jest.mocked(inputVerified).mockReturnValue(true); 98 | 99 | // Arrange - mock the Cosmos DB create method to throw an error 100 | const mockError: DbError = { 101 | message: 'An unknown error occurred', 102 | code: 500, 103 | }; 104 | jest.mocked(mockContainer.items.create).mockRejectedValue(mockError); 105 | 106 | // Act - Call the function to test 107 | const insertDocumentResult = await insertDocument(mockContainer, input); 108 | 109 | // Assert - verify type as DbError 110 | if (isDbError(insertDocumentResult)) { 111 | expect(insertDocumentResult.message).toBe(mockError.message); 112 | } else { 113 | throw new Error('Result is not of type DbError'); 114 | } 115 | 116 | // Assert - Behavior verification: Ensure create method was called with correct arguments 117 | expect(mockContainer.items.create).toHaveBeenCalledTimes(1); 118 | expect(mockContainer.items.create).toHaveBeenCalledWith({ 119 | id: input.id, 120 | name: result.name, 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /unit-testing/src/mock-function/lib/insert.ts: -------------------------------------------------------------------------------- 1 | // insertDocument.ts 2 | import { Container } from '../data/connect-to-cosmos'; 3 | import { 4 | DbDocument, 5 | DbError, 6 | RawInput, 7 | VerificationErrors, 8 | } from '../data/model'; 9 | import { inputVerified } from '../data/verify'; 10 | 11 | export async function insertDocument( 12 | container: Container, 13 | doc: RawInput, 14 | ): Promise { 15 | const isVerified: boolean = inputVerified(doc); 16 | 17 | if (!isVerified) { 18 | return { message: 'Verification failed' } as VerificationErrors; 19 | } 20 | 21 | try { 22 | const { resource } = await container.items.create({ 23 | id: doc.id, 24 | name: `${doc.first} ${doc.last}`, 25 | }); 26 | 27 | return resource as DbDocument; 28 | } catch (error: any) { 29 | if (error instanceof Error) { 30 | if ((error as any).code === 409) { 31 | return { 32 | message: 'Insertion failed: Duplicate entry', 33 | code: 409, 34 | } as DbError; 35 | } 36 | return { message: error.message, code: (error as any).code } as DbError; 37 | } else { 38 | return { message: 'An unknown error occurred', code: 500 } as DbError; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /unit-testing/src/test-boilerplate/boilerplate-with-mock.spec.ts: -------------------------------------------------------------------------------- 1 | // boilerplate-with-mock.spec.ts 2 | 3 | // Mock the dependencies 4 | jest.mock('../mock-function/data/connect-to-cosmos', () => ({ 5 | myFunctionToMock: jest.fn(), 6 | })); 7 | 8 | describe('nameOfGroupOfTests', () => { 9 | beforeEach(() => { 10 | // Clear all mocks before each test 11 | jest.clearAllMocks(); 12 | 13 | // Other setup required before each test 14 | }); 15 | 16 | it('should if ', async () => { 17 | // Arrange 18 | // - set up the test data and the expected result 19 | // Act 20 | // - call the function to test 21 | // Assert 22 | // - check the state: result returned from function 23 | // - check the behavior: dependency function calls 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /unit-testing/src/test-boilerplate/boilerplate.spec.ts: -------------------------------------------------------------------------------- 1 | // boilerplate.spec.ts 2 | 3 | describe('nameOfGroupOfTests', () => { 4 | beforeEach(() => { 5 | // Setup required before each test 6 | }); 7 | afterEach(() => { 8 | // Cleanup required after each test 9 | }); 10 | 11 | it('should if ', async () => { 12 | // Arrange 13 | // - set up the test data and the expected result 14 | // Act 15 | // - call the function to test 16 | // Assert 17 | // - check the state: result returned from function 18 | // - check the behavior: dependency function calls 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /unit-testing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", // Specify ECMAScript target version 4 | "module": "commonjs", // Specify module code generation 5 | "strict": true, // Enable all strict type-checking options 6 | "esModuleInterop": true, // Enables emit interoperability between CommonJS and ES Modules 7 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file 8 | "types": ["jest", "node"], // Specify type package 9 | "outDir": "./dist", 10 | "rootDir": "./src", 11 | }, 12 | } --------------------------------------------------------------------------------