├── .eslintrc.json ├── .github └── workflows │ ├── publish.yml │ └── semantic-release.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.json ├── .releaserc.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.txt ├── README.md ├── azl.go ├── conf ├── package.cjs.json ├── package.cli.json ├── package.esm.json ├── tsup.cjs.config.ts ├── tsup.common.config.ts ├── tsup.esm.config.ts ├── tsup.scripts.cjs.config.ts ├── tsup.scripts.common.config.ts └── tsup.scripts.esm.ts ├── package-lock.json ├── package.json ├── packages ├── azure-local-cli │ ├── .gitignore │ ├── README.md │ ├── azure_local_cli │ │ ├── __init__.py │ │ └── __main__.py │ ├── package.json │ ├── poetry.lock │ └── pyproject.toml └── csharp-test │ ├── .gitignore │ ├── AzureTests.cs │ ├── GlobalUsings.cs │ ├── QueueAbandon.cs │ ├── QueueBatch.cs │ ├── QueueCancelScheduled.cs │ ├── QueueComplete.cs │ ├── QueueDeadLetter.cs │ ├── QueueDeadLetterMessageExpiration.cs │ ├── QueueDefer.cs │ ├── QueueLargeMessageBatch.cs │ ├── QueueMessageContentProperties.cs │ ├── QueuePeek.cs │ ├── QueueRedeliveryOrder.cs │ ├── QueueRenewLock.cs │ ├── QueueScheduled.cs │ ├── QueueSession.cs │ ├── QueueSubQueue.cs │ ├── SubscriptionComplete.cs │ ├── csharp-test.csproj │ └── package.json ├── patches └── rhea+3.0.2.patch ├── scripts ├── amqp-client.ts ├── amqp-server.ts ├── build-azl-go.ts ├── bundle-cli.ts ├── cli.ts ├── generate-azure-schema.ts └── start-server.ts ├── src ├── cli │ ├── index.ts │ └── start-server.ts ├── generated │ └── azure-rest-api-specs │ │ ├── common-types │ │ └── resource-management │ │ │ ├── v1 │ │ │ └── types.ts │ │ │ └── v6 │ │ │ ├── managedidentity.ts │ │ │ └── types.ts │ │ ├── compute │ │ └── resource-manager │ │ │ └── Microsoft.Compute │ │ │ ├── ComputeRP │ │ │ └── stable │ │ │ │ └── 2024-07-01 │ │ │ │ ├── computeRPCommon.ts │ │ │ │ └── virtualMachine.ts │ │ │ └── common-types │ │ │ └── v1 │ │ │ └── common.ts │ │ ├── resources │ │ └── resource-manager │ │ │ └── Microsoft.Resources │ │ │ └── stable │ │ │ ├── 2016-06-01 │ │ │ └── subscriptions.ts │ │ │ └── 2024-07-01 │ │ │ └── resources.ts │ │ └── servicebus │ │ └── resource-manager │ │ ├── Microsoft.ServiceBus │ │ └── stable │ │ │ └── 2021-11-01 │ │ │ ├── Queue.ts │ │ │ ├── namespace-preview.ts │ │ │ ├── subscriptions.ts │ │ │ └── topics.ts │ │ └── common │ │ ├── v1 │ │ └── definitions.ts │ │ └── v2 │ │ └── definitions.ts ├── index.ts └── lib │ ├── amqp │ └── parse-message.ts │ ├── api │ ├── errors.ts │ ├── index.ts │ └── serve.ts │ ├── broker │ ├── broker-server.ts │ ├── broker.ts │ ├── constants.ts │ ├── consumer-balancer.ts │ ├── errors.ts │ ├── index.ts │ ├── queue.ts │ ├── types.ts │ ├── url.ts │ └── util.ts │ ├── cert │ └── certificate-store.ts │ ├── config │ └── config-store.ts │ ├── configstore │ ├── LICENSE.txt │ ├── dot-prop │ │ ├── index.d.ts │ │ └── index.js │ ├── index.d.ts │ └── index.js │ ├── edgespec-util │ └── route-bundle-from-route-map.ts │ ├── edgespec │ ├── LICENSE.txt │ ├── adapters │ │ └── node.ts │ ├── config │ │ ├── config.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── create-with-edge-spec.ts │ ├── edge │ │ └── transform-to-node.ts │ ├── helpers.ts │ ├── index.ts │ ├── lib │ │ ├── async-work-tracker.ts │ │ ├── format-zod-error.ts │ │ └── normalize-route-map.ts │ ├── middleware │ │ ├── http-exceptions.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── with-default-exception-handling.ts │ │ ├── with-input-validation.ts │ │ ├── with-logger.ts │ │ ├── with-methods.ts │ │ └── with-unhandled-exception-handling.ts │ └── types │ │ ├── context.ts │ │ ├── edge-spec.ts │ │ ├── global-spec.ts │ │ ├── index.ts │ │ ├── route-spec.ts │ │ ├── util.ts │ │ └── web-handler.ts │ ├── index.ts │ ├── integration │ ├── azure │ │ ├── index.ts │ │ ├── resource-groups.ts │ │ ├── routes.ts │ │ ├── service-bus │ │ │ ├── index.ts │ │ │ ├── namespace.ts │ │ │ ├── queue.ts │ │ │ ├── subscription.ts │ │ │ └── topic.ts │ │ └── susbcriptions.ts │ ├── index.ts │ └── integration.ts │ ├── logger │ ├── get-test-logger.ts │ ├── index.ts │ └── with-logger.ts │ ├── openapi │ ├── convert-to-zod.ts │ └── extract-route.ts │ ├── server │ ├── check-config.ts │ ├── env.ts │ └── start-server.ts │ ├── tls │ └── index.ts │ └── util │ ├── bearer-token.ts │ ├── is-errno-exception.ts │ ├── long.ts │ ├── service-bus.ts │ ├── timeout.ts │ └── uuid.ts ├── test ├── azure │ ├── model.test.ts │ ├── namespace.test.ts │ ├── resource_group.test.ts │ ├── service_bus.test.ts │ └── subscription.test.ts ├── fixtured-test.ts ├── lib │ ├── broker │ │ └── url.test.ts │ └── util │ │ └── uuid.test.ts ├── parity │ └── service-bus │ │ ├── queue │ │ ├── abandon.test.ts │ │ ├── accessed-at.test.ts │ │ ├── active-message-count.test.ts │ │ ├── cancel-scheduled.test.ts │ │ ├── complete.test.ts │ │ ├── dead-letter-message-expiration.test.ts │ │ ├── dead-letter.test.ts │ │ ├── default-queue.test.ts │ │ ├── defaults.test.ts │ │ ├── defer.test.ts │ │ ├── duplicate-detection.test.ts │ │ ├── expires-at.test.ts │ │ ├── large-message-batch.test.ts │ │ ├── lock-duration.test.ts │ │ ├── max-delivery-count-dlq.test.ts │ │ ├── max-delivery-count.test.ts │ │ ├── message-batch.test.ts │ │ ├── message-content-properties.test.ts │ │ ├── message-count.test.ts │ │ ├── multi-queue.test.ts │ │ ├── naming-scheme.test.ts │ │ ├── peek.test.ts │ │ ├── receive-and-delete.test.ts │ │ ├── redelivery-order.test.ts │ │ ├── renew-lock.test.ts │ │ ├── scheduled.test.ts │ │ ├── session │ │ │ ├── complete.test.ts │ │ │ ├── deduplication.test.ts │ │ │ ├── locked.test.ts │ │ │ ├── peek-session.test.ts │ │ │ └── requires-session-enabled.test.ts │ │ ├── sub-queue-type.test.ts │ │ └── ttl.test.ts │ │ └── subscription │ │ ├── abandon.test.ts │ │ ├── accessed-at.test.ts │ │ ├── active-message-count.test.ts │ │ ├── cancel-scheduled.test.ts │ │ ├── complete.test.ts │ │ ├── dead-letter-message-expiration.test.ts │ │ ├── dead-letter.test.ts │ │ ├── defaults.test.ts │ │ ├── defer.test.ts │ │ ├── duplicate-detection.test.ts │ │ ├── expires-at.test.ts │ │ ├── lock-duration.test.ts │ │ ├── max-delivery-count-dlq.test.ts │ │ ├── max-delivery-count.test.ts │ │ ├── message-batch.test.ts │ │ ├── message-content-properties.test.ts │ │ ├── message-count.test.ts │ │ ├── naming-scheme.test.ts │ │ ├── peek.test.ts │ │ ├── receive-and-delete.test.ts │ │ ├── receive-from-topic.test.ts │ │ ├── renew-lock.test.ts │ │ ├── scheduled.test.ts │ │ ├── send-to-subscription.test.ts │ │ ├── session │ │ └── complete.test.ts │ │ ├── sub-queue-type.test.ts │ │ └── ttl.test.ts ├── service-bus │ ├── auto-delete-on-idle.test.ts │ └── sequence-number.test.ts └── setup.ts ├── tsconfig.json └── vitest.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "plugins": ["simple-import-sort", "unused-imports"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx"], 7 | "extends": ["standard-with-typescript", "prettier"], 8 | "rules": { 9 | "@typescript-eslint/consistent-type-definitions": "off", 10 | "@typescript-eslint/explicit-function-return-type": "off", 11 | "@typescript-eslint/naming-convention": "off", 12 | "@typescript-eslint/no-import-type-side-effects": "error", 13 | "@typescript-eslint/strict-boolean-expressions": "off", 14 | "@typescript-eslint/no-non-null-assertion": "off", 15 | "@typescript-eslint/ban-types": "off", 16 | "@typescript-eslint/no-unnecessary-type-assertion": "off", 17 | "@typescript-eslint/dot-notation": "off", 18 | "@typescript-eslint/no-unsafe-argument": "off", 19 | "@typescript-eslint/prefer-nullish-coalescing": "off", 20 | "@typescript-eslint/no-dynamic-delete": "off", 21 | "@typescript-eslint/ban-ts-comment": "off", 22 | "@typescript-eslint/no-misused-promises": "off", 23 | "@typescript-eslint/array-type": "off", 24 | "prefer-regex-literals": "off", 25 | "no-lone-blocks": "off" 26 | }, 27 | "overrides": [ 28 | { 29 | "files": ["./vitest.config.ts"], 30 | "rules": { 31 | "@typescript-eslint/triple-slash-reference": "off" 32 | } 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/semantic-release.yml: -------------------------------------------------------------------------------- 1 | name: Semantic Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | version: 10 | runs-on: ubuntu-latest 11 | name: Version 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Semantic release 17 | id: release 18 | uses: cycjimmy/semantic-release-action@v4 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 22 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 23 | 24 | - name: Run publish workflow 25 | if: steps.release.outputs.new_release_published == 'true' 26 | run: gh workflow run publish.yml --ref ${{ steps.release.outputs.new_release_git_tag }} 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .env 4 | .env.local 5 | .env.production 6 | 7 | dist 8 | 9 | **/.DS_Store -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } 4 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "+([0-9])?(.{+([0-9]),x}).x", 4 | "main", 5 | "next", 6 | "next-major", 7 | { "name": "beta", "prerelease": true }, 8 | { "name": "alpha", "prerelease": true } 9 | ], 10 | "plugins": [ 11 | "@semantic-release/commit-analyzer", 12 | "@semantic-release/release-notes-generator", 13 | "@semantic-release/changelog", 14 | "@semantic-release/npm", 15 | [ 16 | "@semantic-release/git", 17 | { 18 | "assets": ["package.json", "CHANGELOG.md"], 19 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", 20 | "gpgSign": true 21 | } 22 | ] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.1.0](https://github.com/mxsdev/LocalSandbox/compare/v1.0.2...v1.1.0) (2024-09-22) 2 | 3 | 4 | ### Features 5 | 6 | * **license:** use apache license ([cae137c](https://github.com/mxsdev/LocalSandbox/commit/cae137c81d1a091e59251bd192ebe1adf6878eb7)) 7 | 8 | ## [1.0.2](https://github.com/mxsdev/LocalSandbox/compare/v1.0.1...v1.0.2) (2024-09-20) 9 | 10 | 11 | ### Performance Improvements 12 | 13 | * disable cert caching for now ([0c87dc5](https://github.com/mxsdev/LocalSandbox/commit/0c87dc5604648b9aa12675c888794d84f7349188)) 14 | * improve UX for default resources config ([12a7e64](https://github.com/mxsdev/LocalSandbox/commit/12a7e64361c0cec2abe96613ab8efa30d3d22aac)) 15 | 16 | ## [1.0.1](https://github.com/mxsdev/LocalSandbox/compare/v1.0.0...v1.0.1) (2024-09-20) 17 | 18 | 19 | ### Performance Improvements 20 | 21 | * smaller multiarch docker build ([18f1352](https://github.com/mxsdev/LocalSandbox/commit/18f13524d2f32349667d4220365863da61bd065b)) 22 | 23 | 24 | ### Reverts 25 | 26 | * Revert "chore(release): 1.0.1 [skip ci]" ([2d60510](https://github.com/mxsdev/LocalSandbox/commit/2d605108a99e9340766bac6e8f258403bfb74692)) 27 | * Revert "chore(release): 1.0.1 [skip ci]" ([97b0eae](https://github.com/mxsdev/LocalSandbox/commit/97b0eae2447dfbfa2c39fa236b035186f4a18d1c)) 28 | 29 | # 1.0.0 (2024-09-20) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * parse out python location from version output ([f37e28b](https://github.com/mxsdev/LocalSandbox/commit/f37e28b12f5956285cd17b4d243e38a6bccd5571)) 35 | 36 | 37 | ### Features 38 | 39 | * initial release ([e79572c](https://github.com/mxsdev/LocalSandbox/commit/e79572c80d87871512b7ad789fb47515c8a59145)) 40 | 41 | 42 | ### Reverts 43 | 44 | * Revert "chore(release): 1.0.0 [skip ci]" ([42fad85](https://github.com/mxsdev/LocalSandbox/commit/42fad85cbc05f9b69540bde5ac701c95dd5a953c)) 45 | * Revert "chore(release): 1.0.0 [skip ci]" ([6ccaf5f](https://github.com/mxsdev/LocalSandbox/commit/6ccaf5fce7d859046b15c57a898c611932198348)) 46 | * Revert "chore(release): 1.0.0 [skip ci]" ([5c23821](https://github.com/mxsdev/LocalSandbox/commit/5c238210dc060af4cae12b9ab94ddc7651578af7)) 47 | * Revert "chore(release): 1.0.0 [skip ci]" ([4d13099](https://github.com/mxsdev/LocalSandbox/commit/4d13099d456ca36573e291ac1bb9071b88eeb5e6)) 48 | * Revert "chore(release): 1.0.0 [skip ci]" ([1d3ba98](https://github.com/mxsdev/LocalSandbox/commit/1d3ba9863b240450e987a3864874491afd1455e5)) 49 | * Revert "chore(release): 1.0.0 [skip ci]" ([6718b90](https://github.com/mxsdev/LocalSandbox/commit/6718b903f31de382132278520a00e80f7673c550)) 50 | * Revert "chore(release): 1.0.0 [skip ci]" ([c3afb05](https://github.com/mxsdev/LocalSandbox/commit/c3afb05d8214680244cd40007008d328e94f36e1)) 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/azure-cli 2 | 3 | ARG TARGETARCH 4 | 5 | WORKDIR /app 6 | 7 | COPY ./dist/binary/localsandbox-linux-${TARGETARCH} /app/localsandbox 8 | 9 | RUN /app/localsandbox --version 10 | 11 | COPY ./packages/azure-local-cli/azure_local_cli/__main__.py /app/__main__.py 12 | 13 | RUN mkdir -p /app/azl 14 | 15 | RUN echo "#!/bin/bash" > /app/azl/azl 16 | RUN echo "PYTHONPATH=/usr/lib64/az/lib/python3.9/site-packages python3 /app/__main__.py \$@" >> /app/azl/azl 17 | RUN chmod +x /app/azl/azl 18 | ENV PATH="/app/azl:${PATH}" 19 | 20 | RUN azl --version 21 | 22 | CMD ["/app/localsandbox", "run"] -------------------------------------------------------------------------------- /azl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | // Embed the Python script at compile time 13 | // 14 | //go:embed packages/azure-local-cli/azure_local_cli/__main__.py 15 | var embeddedScript string 16 | 17 | func runAzureLocalCli(args ...string) error { 18 | azPath, err := exec.LookPath("az") 19 | if err != nil || azPath == "" { 20 | fmt.Fprintln(os.Stderr, "Could not find az cli in PATH") 21 | os.Exit(1) 22 | } 23 | 24 | cmd := exec.Command(azPath, "--version") 25 | var stdout, stderr bytes.Buffer 26 | cmd.Stdout = &stdout 27 | cmd.Stderr = &stderr 28 | 29 | if err := cmd.Run(); err != nil { 30 | fmt.Fprintln(os.Stderr, stderr.String()) 31 | return err 32 | } 33 | 34 | pythonLocationMatch := "" 35 | for _, line := range strings.Split(strings.ReplaceAll(stdout.String(), "\r\n", "\n"), "\n") { 36 | if strings.HasPrefix(line, "Python location '") { 37 | pythonLocationMatch = strings.TrimPrefix(line, "Python location '") 38 | pythonLocationMatch = strings.TrimSuffix(pythonLocationMatch, "'") 39 | break 40 | } 41 | } 42 | 43 | if pythonLocationMatch == "" { 44 | fmt.Fprintln(os.Stderr, "Could not find python location from az cli") 45 | os.Exit(1) 46 | } 47 | 48 | pythonCmd := exec.Command(pythonLocationMatch, append([]string{"-c", embeddedScript}, args...)...) 49 | pythonCmd.Stdout = os.Stdout 50 | pythonCmd.Stderr = os.Stderr 51 | 52 | if err := pythonCmd.Run(); err != nil { 53 | return err 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func main() { 60 | if err := runAzureLocalCli(os.Args[1:]...); err != nil { 61 | fmt.Fprintln(os.Stderr, err.Error()) 62 | os.Exit(1) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /conf/package.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /conf/package.cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "localsandbox", 3 | "bin": "./cli.js", 4 | "pkg": { 5 | "scripts": ["cli.js", "start-server.js"], 6 | "assets": ["../../../packages/azure-local-cli/azure_local_cli/__main__.py"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /conf/package.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /conf/tsup.cjs.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | import { common_tsup_options } from "./tsup.common.config.js" 3 | 4 | export default defineConfig({ 5 | ...common_tsup_options, 6 | 7 | outDir: "./dist/cjs", 8 | format: "cjs", 9 | 10 | legacyOutput: true, 11 | 12 | // banner: { 13 | // js: "import { createRequire } from 'module';const require = createRequire(import.meta.url); const __filename = import.meta.filename; const __dirname = import.meta.dirname;", 14 | // }, 15 | }) 16 | -------------------------------------------------------------------------------- /conf/tsup.common.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from "tsup" 2 | 3 | export const common_tsup_options = { 4 | entry: ["src/**/*.ts"], 5 | dts: { entry: ["src/index.ts"] }, 6 | 7 | platform: "node", 8 | 9 | clean: true, 10 | minify: false, 11 | bundle: false, 12 | 13 | sourcemap: true, 14 | } satisfies Options 15 | -------------------------------------------------------------------------------- /conf/tsup.esm.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | import { common_tsup_options } from "./tsup.common.config.js" 3 | 4 | export default defineConfig({ 5 | ...common_tsup_options, 6 | 7 | outDir: "./dist/esm", 8 | format: "esm", 9 | 10 | // banner: { 11 | // js: "import { createRequire } from 'module';const require = createRequire(import.meta.url); const __filename = import.meta.filename; const __dirname = import.meta.dirname;", 12 | // }, 13 | }) 14 | -------------------------------------------------------------------------------- /conf/tsup.scripts.cjs.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | import { common_scripts_tsup_options } from "./tsup.scripts.common.config.js" 3 | 4 | export default defineConfig({ 5 | ...common_scripts_tsup_options, 6 | 7 | outDir: "./dist/scripts/cjs", 8 | 9 | format: "cjs", 10 | outExtension: () => ({ 11 | js: ".js", 12 | }), 13 | 14 | // banner: { 15 | // js: "import { createRequire } from 'module';const require = createRequire(import.meta.url); const __filename = import.meta.filename; const __dirname = import.meta.dirname;", 16 | // }, 17 | }) 18 | -------------------------------------------------------------------------------- /conf/tsup.scripts.common.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from "tsup" 2 | import fs from "fs" 3 | 4 | export const common_scripts_tsup_options = { 5 | entry: ["./scripts/cli.ts", "./scripts/start-server.ts"], 6 | env: { 7 | __AZL_PYTHON_MAIN: fs.readFileSync( 8 | "./packages/azure-local-cli/azure_local_cli/__main__.py", 9 | "utf-8", 10 | ), 11 | }, 12 | noExternal: [/(.*)/], 13 | platform: "node", 14 | clean: true, 15 | minify: true, 16 | bundle: true, 17 | } satisfies Options 18 | -------------------------------------------------------------------------------- /conf/tsup.scripts.esm.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup" 2 | import { common_scripts_tsup_options } from "./tsup.scripts.common.config.js" 3 | 4 | export default defineConfig({ 5 | ...common_scripts_tsup_options, 6 | 7 | outDir: "./dist/scripts/esm", 8 | 9 | format: "esm", 10 | legacyOutput: true, 11 | 12 | banner: { 13 | js: "import { createRequire } from 'module';const require = createRequire(import.meta.url); const __filename = import.meta.filename; const __dirname = import.meta.dirname;", 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /packages/azure-local-cli/README.md: -------------------------------------------------------------------------------- 1 | # azure-local-cli 2 | -------------------------------------------------------------------------------- /packages/azure-local-cli/azure_local_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxsdev/LocalSandbox/4899dc13f423847e3f0991f09486e0f70df69ffe/packages/azure-local-cli/azure_local_cli/__init__.py -------------------------------------------------------------------------------- /packages/azure-local-cli/azure_local_cli/__main__.py: -------------------------------------------------------------------------------- 1 | from collections import UserDict 2 | 3 | class AttrDict(UserDict): 4 | def __getattr__(self, key): 5 | return self.__getitem__(key) 6 | def __setattr__(self, key, value): 7 | if key == "data": 8 | return super().__setattr__(key, value) 9 | return self.__setitem__(key, value) 10 | 11 | import os 12 | 13 | port = int(os.environ.get("LOCALSANDBOX_PORT") or 7329) 14 | hostname = f"localhost.localsandbox.sh" 15 | endpoint = f"https://{hostname}:{port}/azure" 16 | 17 | import azure.cli.core as azure_cli_core 18 | 19 | get_default_cli = azure_cli_core.get_default_cli 20 | 21 | 22 | def get_cli(): 23 | cli = get_default_cli() 24 | getboolean_real = cli.config.getboolean 25 | 26 | def getboolean(section, option, fallback=None): 27 | if section == "core" and option == "instance_discovery": 28 | return False 29 | 30 | return getboolean_real(section, option, fallback=fallback) 31 | 32 | cli.config.getboolean = getboolean 33 | 34 | return cli 35 | 36 | 37 | from msal import PublicClientApplication 38 | 39 | 40 | class LocalSandboxUserCredential(PublicClientApplication): 41 | def __init__(self, client_id, username, **kwargs): 42 | self.username = username 43 | super().__init__(client_id, **kwargs) 44 | 45 | def get_token(self, *args, **kwargs): 46 | return AttrDict( 47 | {"token": self.username, "expires_in": float("inf"), "id": "id"} 48 | ) 49 | 50 | 51 | import azure.cli.core.auth.msal_authentication as msal_authentication 52 | 53 | msal_authentication.UserCredential = LocalSandboxUserCredential 54 | 55 | import azure.cli.core._profile as _profile 56 | 57 | default_subscription_id = os.environ.get("AZURE_SUBSCRIPTION_ID") or "default" 58 | 59 | _profile.Profile.get_subscription = lambda _, b: AttrDict( 60 | { 61 | "user": {"type": "user", "name": b or default_subscription_id}, 62 | "id": b or default_subscription_id, 63 | "tenantId": b or default_subscription_id, 64 | } 65 | ) 66 | 67 | azure_cli_core.get_default_cli = get_cli 68 | 69 | import warnings 70 | 71 | warnings.filterwarnings("ignore") 72 | 73 | from azure.cli.core import cloud 74 | 75 | from adal.constants import AADConstants 76 | 77 | AADConstants.WORLD_WIDE_AUTHORITY = hostname 78 | AADConstants.WELL_KNOWN_AUTHORITY_HOSTS.append(hostname) 79 | 80 | cloud.AZURE_PUBLIC_CLOUD.endpoints.management = endpoint 81 | cloud.AZURE_PUBLIC_CLOUD.endpoints.resource_manager = endpoint 82 | cloud.AZURE_PUBLIC_CLOUD.endpoints.active_directory = endpoint 83 | 84 | from azure.cli import __main__ 85 | 86 | def main(): 87 | pass -------------------------------------------------------------------------------- /packages/azure-local-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure-local-cli", 3 | "scripts": { 4 | "build": "npm run install && mkdir -p dist && poetry run python -m nuitka ./azure_local_cli --output-filename=./dist/azl --remove-output --assume-yes-for-downloads", 5 | "build:poetry": "poetry build", 6 | "generate:requirements": "poetry export -f requirements.txt --without-hashes --output requirements.txt" 7 | }, 8 | "dependencies": { 9 | "which": "^4.0.0" 10 | }, 11 | "devDependencies": { 12 | "@types/which": "^3.0.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/azure-local-cli/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "azure-local-cli" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Max Stoumen "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.12" 10 | azure-cli = "^2.64.0" 11 | 12 | # [build-system] 13 | # requires = ["setuptools>=42", "wheel", "nuitka", "toml"] 14 | # build-backend = "nuitka.distutils.Build" 15 | 16 | [tool.poetry-pyinstaller-plugin.scripts] 17 | azl = { source = "azure_local_cli/__main__.py", type = "onefile", bundle = false } 18 | 19 | [tool.poetry.group.dev.dependencies] 20 | nuitka = "^2.4.8" 21 | 22 | [tool.poetry.scripts] 23 | azl = "azure_local_cli.__main__:main" 24 | 25 | # [tool.poetry-pyinstaller-plugin.collect] 26 | # all = ['azure'] 27 | -------------------------------------------------------------------------------- /packages/csharp-test/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /packages/csharp-test/QueueAbandon.cs: -------------------------------------------------------------------------------- 1 | namespace csharp_test; 2 | 3 | public class QueueAbandon : AzureTests 4 | { 5 | [Fact] 6 | public async void Abandon() 7 | { 8 | var queueName = await CreateQueue(maxDeliveryCount: 1); 9 | 10 | var sender = CreateSender(queueName); 11 | var recevier = CreateReceiver(queueName); 12 | 13 | await sender.SendMessageAsync(new("1")); 14 | 15 | { 16 | var message = await recevier.ReceiveMessageAsync(); 17 | Assert.Equal("1", message.Body.ToString()); 18 | await recevier.AbandonMessageAsync(message); 19 | } 20 | 21 | { 22 | var message = await recevier.ReceiveMessageAsync(TimeSpan.FromMilliseconds(100)); 23 | Assert.Null(message); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueBatch.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | 3 | namespace csharp_test; 4 | 5 | public class QueueBatch : AzureTests 6 | { 7 | [Fact] 8 | public async void Batch() 9 | { 10 | var queueName = await CreateQueue(); 11 | 12 | var sender = CreateSender(queueName); 13 | 14 | var batch = await sender.CreateMessageBatchAsync(); 15 | 16 | batch.TryAddMessage(new ServiceBusMessage("1")); 17 | batch.TryAddMessage(new ServiceBusMessage("2")); 18 | batch.TryAddMessage(new ServiceBusMessage("3")); 19 | 20 | await sender.SendMessagesAsync(batch); 21 | 22 | var recevier = CreateReceiver(queueName); 23 | 24 | var msg = await recevier.ReceiveMessagesAsync(3); 25 | 26 | Assert.Equal(3, msg.Count); 27 | 28 | foreach (var m in msg) 29 | { 30 | await recevier.CompleteMessageAsync(m); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueCancelScheduled.cs: -------------------------------------------------------------------------------- 1 | namespace csharp_test; 2 | 3 | public class QueueCancelScheduled : AzureTests 4 | { 5 | [Fact] 6 | public async void CancelScheduled() 7 | { 8 | var queueName = await CreateQueue(); 9 | 10 | var sender = CreateSender(queueName); 11 | 12 | var scheduleTimespan = e2eMode ? TimeSpan.FromSeconds(5) : TimeSpan.FromMilliseconds(1000); 13 | 14 | var ScheduledEnqueuedTime = DateTimeOffset.UtcNow.Add(scheduleTimespan); 15 | 16 | var msgId = await sender.ScheduleMessageAsync(new("1"), ScheduledEnqueuedTime); 17 | 18 | var receiver = CreateReceiver(queueName); 19 | await sender.CancelScheduledMessageAsync(msgId); 20 | 21 | { 22 | var message = await receiver.PeekMessageAsync(); 23 | Assert.Null(message); 24 | } 25 | 26 | Assert.True(DateTimeOffset.UtcNow < ScheduledEnqueuedTime); 27 | 28 | { 29 | var message = await receiver.ReceiveMessageAsync(maxWaitTime: scheduleTimespan * 1.5); 30 | Assert.Null(message); 31 | } 32 | 33 | Assert.True(DateTimeOffset.UtcNow >= ScheduledEnqueuedTime); 34 | } 35 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueComplete.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | 3 | namespace csharp_test; 4 | 5 | class JsonMessage 6 | { 7 | public required string Message { get; set; } 8 | public required int Id { get; set; } 9 | } 10 | 11 | public class QueueComplete : AzureTests 12 | { 13 | [Fact] 14 | public async void Complete() 15 | { 16 | var queueName = await CreateQueue(); 17 | 18 | var sender = CreateSender(queueName); 19 | 20 | await sender.SendMessageAsync(new ServiceBusMessage(new BinaryData(new JsonMessage() 21 | { 22 | Id = 1, 23 | Message = "Hello, World!" 24 | }))); 25 | 26 | 27 | var recevier = CreateReceiver(queueName); 28 | 29 | var msg = await recevier.ReceiveMessageAsync(); 30 | 31 | JsonMessage jsonMessage = msg.Body.ToObjectFromJson(); 32 | 33 | Assert.Equal("Hello, World!", jsonMessage.Message); 34 | Assert.Equal(1, jsonMessage.Id); 35 | 36 | await recevier.CompleteMessageAsync(msg); 37 | } 38 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueDeadLetter.cs: -------------------------------------------------------------------------------- 1 | namespace csharp_test; 2 | 3 | public class QueueDeadLetter : AzureTests 4 | { 5 | [Fact] 6 | public async void DeadLetter() 7 | { 8 | var dlqName = await CreateQueue(); 9 | var queueName = await CreateQueue(forwardDeadLetteredMessagesTo: dlqName); 10 | 11 | var sender = CreateSender(queueName); 12 | 13 | await sender.SendMessageAsync(new("1")); 14 | var receiver = CreateReceiver(queueName); 15 | 16 | { 17 | var message = await receiver.ReceiveMessageAsync(); 18 | Assert.NotNull(message); 19 | Assert.Equal("1", message.Body.ToString()); 20 | Assert.Equal(1, message.SequenceNumber); 21 | 22 | await receiver.DeadLetterMessageAsync(message, deadLetterReason: "reason", deadLetterErrorDescription: "description"); 23 | } 24 | 25 | var receiver_dlq = CreateReceiver(dlqName); 26 | 27 | { 28 | var message = await receiver_dlq.ReceiveMessageAsync(); 29 | Assert.NotNull(message); 30 | Assert.Equal("1", message.Body.ToString()); 31 | Assert.Equal(1, message.SequenceNumber); 32 | Assert.Equal(queueName, message.DeadLetterSource); 33 | Assert.Equal("reason", message.DeadLetterReason); 34 | Assert.Equal("description", message.DeadLetterErrorDescription); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueDeadLetterMessageExpiration.cs: -------------------------------------------------------------------------------- 1 | namespace csharp_test; 2 | 3 | public class QueueDeadLetterMessageExpiration : AzureTests 4 | { 5 | [Fact] 6 | public async void DeadLetterMessageExpiration() 7 | { 8 | var dlqName = await CreateQueue(); 9 | var queueName = await CreateQueue( 10 | deadLetteringOnMessageExpiration: true, 11 | forwardDeadLetteredMessagesTo: dlqName 12 | ); 13 | 14 | var ttl = TimeSpan.FromMilliseconds(200); 15 | 16 | var sender = CreateSender(queueName); 17 | 18 | await sender.SendMessageAsync(new("1") 19 | { 20 | TimeToLive = ttl 21 | }); 22 | await Task.Delay(ttl); 23 | 24 | 25 | { 26 | var receiver = CreateReceiver(queueName); 27 | var message = await receiver.ReceiveMessageAsync(maxWaitTime: TimeSpan.FromMilliseconds(10)); 28 | Assert.Null(message); 29 | } 30 | 31 | { 32 | var receiver = CreateReceiver(dlqName); 33 | var message = await receiver.ReceiveMessageAsync(); 34 | Assert.NotNull(message); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueDefer.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | 3 | namespace csharp_test; 4 | 5 | public class QueueDefer : AzureTests 6 | { 7 | [Fact] 8 | public async void Defer() 9 | { 10 | var queueName = await CreateQueue(); 11 | 12 | var sender = CreateSender(queueName); 13 | await sender.SendMessageAsync(new ServiceBusMessage("1")); 14 | 15 | var recevier = CreateReceiver(queueName); 16 | 17 | { 18 | var msg = await recevier.ReceiveMessageAsync(); 19 | await recevier.DeferMessageAsync(msg); 20 | 21 | var deferred_msg = await recevier.ReceiveDeferredMessageAsync(msg.SequenceNumber); 22 | Assert.NotNull(deferred_msg); 23 | Assert.Equal("1", deferred_msg.Body.ToString()); 24 | Assert.Equal(ServiceBusMessageState.Deferred, deferred_msg.State); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueLargeMessageBatch.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | 3 | namespace csharp_test; 4 | 5 | public class QueueLargeMessageBatch : AzureTests 6 | { 7 | [Fact] 8 | public async void LargeMessageBatch() 9 | { 10 | var queueName = await CreateQueue(); 11 | 12 | var sender = CreateSender(queueName); 13 | 14 | var batch = await sender.CreateMessageBatchAsync(new CreateMessageBatchOptions() 15 | { 16 | MaxSizeInBytes = 256_000 17 | }); 18 | 19 | var numMessages = 1000; 20 | for (int i = 0; i < numMessages; i++) 21 | { 22 | Assert.True(batch.TryAddMessage(new ServiceBusMessage("hello world!"))); 23 | } 24 | 25 | await sender.SendMessagesAsync(batch); 26 | 27 | var recevier = CreateReceiver(queueName); 28 | 29 | { 30 | var messages = await recevier.ReceiveMessagesAsync(maxMessages: numMessages); 31 | Assert.Equal(numMessages, messages.Count); 32 | 33 | foreach (var message in messages) 34 | { 35 | Assert.Equal("hello world!", message.Body.ToString()); 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueMessageContentProperties.cs: -------------------------------------------------------------------------------- 1 | namespace csharp_test; 2 | 3 | public class QueueMessageProperties : AzureTests 4 | { 5 | [Fact] 6 | public async void Subject() 7 | { 8 | var queueName = await CreateQueue(); 9 | 10 | var sender = CreateSender(queueName); 11 | 12 | await sender.SendMessageAsync(new("hello world!") 13 | { 14 | Subject = "greeting" 15 | }); 16 | 17 | var recevier = CreateReceiver(queueName); 18 | 19 | var msg = await recevier.ReceiveMessageAsync(); 20 | Assert.Equal("greeting", msg.Subject); 21 | } 22 | 23 | [Fact] 24 | public async void CorrelationId() 25 | { 26 | var queueName = await CreateQueue(); 27 | 28 | var sender = CreateSender(queueName); 29 | 30 | await sender.SendMessageAsync(new("hello world!") 31 | { 32 | CorrelationId = "123" 33 | }); 34 | 35 | var recevier = CreateReceiver(queueName); 36 | 37 | var msg = await recevier.ReceiveMessageAsync(); 38 | Assert.Equal("123", msg.CorrelationId); 39 | } 40 | 41 | [Fact] 42 | public async void MessageId() 43 | { 44 | var queueName = await CreateQueue(); 45 | 46 | var sender = CreateSender(queueName); 47 | 48 | await sender.SendMessageAsync(new("hello world!") 49 | { 50 | MessageId = "123" 51 | }); 52 | 53 | var recevier = CreateReceiver(queueName); 54 | 55 | var msg = await recevier.ReceiveMessageAsync(); 56 | Assert.Equal("123", msg.MessageId); 57 | } 58 | 59 | [Fact] 60 | public async void ContentType() 61 | { 62 | var queueName = await CreateQueue(); 63 | 64 | var sender = CreateSender(queueName); 65 | 66 | await sender.SendMessageAsync(new("hello world!") 67 | { 68 | ContentType = "application/json" 69 | }); 70 | 71 | var recevier = CreateReceiver(queueName); 72 | 73 | var msg = await recevier.ReceiveMessageAsync(); 74 | Assert.Equal("application/json", msg.ContentType); 75 | } 76 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueuePeek.cs: -------------------------------------------------------------------------------- 1 | namespace csharp_test; 2 | 3 | public class QueuePeek : AzureTests 4 | { 5 | [Fact] 6 | public async void Peek() 7 | { 8 | var queueName = await CreateQueue(); 9 | 10 | var sender = CreateSender(queueName); 11 | var recevier = CreateReceiver(queueName); 12 | 13 | { 14 | var message = await recevier.PeekMessageAsync(); 15 | Assert.Null(message); 16 | } 17 | 18 | await sender.SendMessageAsync(new("1")); 19 | 20 | { 21 | var message = await recevier.PeekMessageAsync(); 22 | Assert.NotNull(message); 23 | Assert.Equal("1", message.Body.ToString()); 24 | } 25 | } 26 | 27 | [Fact] 28 | public async void PeekMultiple() 29 | { 30 | var queueName = await CreateQueue(); 31 | 32 | var sender = CreateSender(queueName); 33 | var recevier = CreateReceiver(queueName); 34 | 35 | { 36 | var messages = await recevier.PeekMessagesAsync(2); 37 | Assert.Empty(messages); 38 | } 39 | 40 | await sender.SendMessageAsync(new("1")); 41 | await sender.SendMessageAsync(new("2")); 42 | await sender.SendMessageAsync(new("3")); 43 | 44 | { 45 | var messages = await recevier.PeekMessagesAsync(2); 46 | Assert.Equal(2, messages.Count); 47 | 48 | Assert.Equal("1", messages[0].Body.ToString()); 49 | Assert.Equal("2", messages[1].Body.ToString()); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueRedeliveryOrder.cs: -------------------------------------------------------------------------------- 1 | namespace csharp_test; 2 | 3 | public class QueueRedeliveryOrder : AzureTests 4 | { 5 | [Fact] 6 | public async void RedeliveryOrder() 7 | { 8 | var queueName = await CreateQueue(); 9 | 10 | var sender = CreateSender(queueName); 11 | 12 | await sender.SendMessageAsync(new("1")); 13 | await sender.SendMessageAsync(new("2")); 14 | 15 | { 16 | var receiver = CreateReceiver(queueName); 17 | 18 | var msg1 = await receiver.ReceiveMessageAsync(); 19 | Assert.Equal("1", msg1.Body.ToString()); 20 | 21 | var msg2 = await receiver.ReceiveMessageAsync(); 22 | Assert.Equal("2", msg2.Body.ToString()); 23 | 24 | await receiver.AbandonMessageAsync(msg1); 25 | await receiver.AbandonMessageAsync(msg2); 26 | } 27 | 28 | { 29 | var receiver = CreateReceiver(queueName); 30 | 31 | var messages = await receiver.ReceiveMessagesAsync(2); 32 | Assert.Equal("1", messages[0].Body.ToString()); 33 | Assert.Equal("1", messages[1].Body.ToString()); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueRenewLock.cs: -------------------------------------------------------------------------------- 1 | namespace csharp_test; 2 | 3 | public class QueueRenewLock : AzureTests 4 | { 5 | [Fact] 6 | public async void RenewLock() 7 | { 8 | var lockDuration = e2eMode ? TimeSpan.FromSeconds(5) : TimeSpan.FromMilliseconds(500); 9 | 10 | var queueName = await CreateQueue(lockDuration: lockDuration); 11 | 12 | var sender = CreateSender(queueName); 13 | 14 | await sender.SendMessageAsync(new("hello world!")); 15 | 16 | var recevier = CreateReceiver(queueName); 17 | 18 | var msg = await recevier.ReceiveMessageAsync(); 19 | Assert.NotNull(msg); 20 | 21 | { 22 | await Task.Delay(lockDuration / 2); 23 | 24 | { 25 | var messages = await recevier.ReceiveMessagesAsync(1, maxWaitTime: TimeSpan.FromMilliseconds(10)); 26 | Assert.Empty(messages); 27 | } 28 | 29 | await recevier.RenewMessageLockAsync(msg); 30 | 31 | await Task.Delay(lockDuration); 32 | 33 | { 34 | var messages = await recevier.ReceiveMessagesAsync(1, maxWaitTime: TimeSpan.FromMilliseconds(10)); 35 | 36 | Assert.Single(messages); 37 | Assert.Equal(msg.SequenceNumber, messages[0].SequenceNumber); 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueScheduled.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | 3 | namespace csharp_test; 4 | 5 | public class QueueScheduled : AzureTests 6 | { 7 | [Fact] 8 | public async void Scheduled() 9 | { 10 | var queueName = await CreateQueue(); 11 | 12 | var sender = CreateSender(queueName); 13 | 14 | var scheduleTimespan = e2eMode ? TimeSpan.FromSeconds(5) : TimeSpan.FromMilliseconds(1000); 15 | 16 | var ScheduledEnqueuedTime = DateTimeOffset.UtcNow.Add(scheduleTimespan); 17 | 18 | await sender.ScheduleMessageAsync(new("1"), ScheduledEnqueuedTime); 19 | 20 | var receiver = CreateReceiver(queueName); 21 | 22 | { 23 | var message = await receiver.ReceiveMessageAsync(maxWaitTime: TimeSpan.FromMilliseconds(100)); 24 | Assert.Null(message); 25 | } 26 | 27 | { 28 | var message = await receiver.PeekMessageAsync(); 29 | Assert.Equal(ServiceBusMessageState.Scheduled, message.State); 30 | } 31 | 32 | Assert.True(DateTimeOffset.UtcNow < ScheduledEnqueuedTime); 33 | 34 | { 35 | var message = await CreateReceiver(queueName).ReceiveMessageAsync(); 36 | Assert.Equal("1", message.Body.ToString()); 37 | Assert.Equal(ServiceBusMessageState.Scheduled, message.State); 38 | Assert.Equal( 39 | ScheduledEnqueuedTime.ToUnixTimeMilliseconds(), 40 | message.ScheduledEnqueueTime.ToUnixTimeMilliseconds() 41 | ); 42 | } 43 | 44 | Assert.True(DateTimeOffset.UtcNow >= ScheduledEnqueuedTime); 45 | } 46 | } -------------------------------------------------------------------------------- /packages/csharp-test/QueueSubQueue.cs: -------------------------------------------------------------------------------- 1 | using Azure.Messaging.ServiceBus; 2 | 3 | namespace csharp_test; 4 | 5 | public class QueueSubQueue : AzureTests 6 | { 7 | [Fact] 8 | public async void SubQueue() 9 | { 10 | var queueName = await CreateQueue(); 11 | 12 | var sender = CreateSender(queueName); 13 | await sender.SendMessageAsync(new("hello world!")); 14 | 15 | var recevier = CreateReceiver(queueName); 16 | 17 | var msg = await recevier.ReceiveMessageAsync(); 18 | Assert.NotNull(msg); 19 | 20 | await recevier.DeadLetterMessageAsync(msg, deadLetterReason: "reason", deadLetterErrorDescription: "description"); 21 | 22 | var receiverDlq = CreateReceiver(queueName, new ServiceBusReceiverOptions() 23 | { 24 | SubQueue = Azure.Messaging.ServiceBus.SubQueue.DeadLetter 25 | }); 26 | 27 | var msgDlq = await receiverDlq.ReceiveMessageAsync(); 28 | Assert.NotNull(msgDlq); 29 | Assert.Equal("reason", msgDlq.DeadLetterReason); 30 | Assert.Equal("description", msgDlq.DeadLetterErrorDescription); 31 | Assert.Equal(msg.SequenceNumber, msgDlq.SequenceNumber); 32 | await receiverDlq.CompleteMessageAsync(msgDlq); 33 | } 34 | 35 | [Fact] 36 | public async void SubQueueFail() 37 | { 38 | var dlqName = await CreateQueue(); 39 | var queueName = await CreateQueue(forwardDeadLetteredMessagesTo: dlqName); 40 | 41 | var receiverDlq = CreateReceiver(queueName, new ServiceBusReceiverOptions() 42 | { 43 | SubQueue = Azure.Messaging.ServiceBus.SubQueue.DeadLetter 44 | }); 45 | 46 | try 47 | { 48 | await receiverDlq.ReceiveMessageAsync(maxWaitTime: TimeSpan.FromMilliseconds(100)); 49 | Assert.Fail("Expected exception to be thrown"); 50 | } 51 | catch (InvalidOperationException e) 52 | { 53 | Assert.Matches("Cannot create a message receiver on an entity with auto-forwarding enabled", e.Message); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /packages/csharp-test/SubscriptionComplete.cs: -------------------------------------------------------------------------------- 1 | namespace csharp_test; 2 | 3 | public class SubscriptionComplete : AzureTests 4 | { 5 | [Fact] 6 | public async void Complete() 7 | { 8 | var topicName = await CreateTopic(); 9 | var subscriptionName = await CreateSubscription(topicName); 10 | 11 | var sender = CreateSender(topicName); 12 | 13 | await sender.SendMessageAsync(new("hello world!")); 14 | 15 | var receiver = CreateReceiver(topicName, subscriptionName); 16 | 17 | var msg = await receiver.ReceiveMessageAsync(); 18 | 19 | Assert.NotNull(msg); 20 | Assert.Equal("hello world!", msg.Body.ToString()); 21 | 22 | await receiver.CompleteMessageAsync(msg); 23 | } 24 | } -------------------------------------------------------------------------------- /packages/csharp-test/csharp-test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | csharp_test 6 | enable 7 | enable 8 | 9 | false 10 | true 11 | 12 | false 13 | NU1903;NU1902 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | all 25 | 26 | 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | all 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /packages/csharp-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@localsandbox/csharp-test", 3 | "scripts": { 4 | "test": "dotnet test" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /patches/rhea+3.0.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/rhea/lib/container.js b/node_modules/rhea/lib/container.js 2 | index ac9a5ab..6f3a0b0 100644 3 | --- a/node_modules/rhea/lib/container.js 4 | +++ b/node_modules/rhea/lib/container.js 5 | @@ -103,5 +103,6 @@ Container.prototype.ReceiverEvents = eventTypes.ReceiverEvents; 6 | Container.prototype.SenderEvents = eventTypes.SenderEvents; 7 | Container.prototype.SessionEvents = eventTypes.SessionEvents; 8 | Container.prototype.ConnectionEvents = eventTypes.ConnectionEvents; 9 | +Container.prototype.Typed = Container.prototype.types.Typed 10 | 11 | module.exports = new Container(); 12 | diff --git a/node_modules/rhea/lib/link.js b/node_modules/rhea/lib/link.js 13 | index 9d1ed84..c9b46aa 100644 14 | --- a/node_modules/rhea/lib/link.js 15 | +++ b/node_modules/rhea/lib/link.js 16 | @@ -288,7 +288,7 @@ var Sender = function (session, name, local_handle, opts) { 17 | Sender.prototype = Object.create(link); 18 | Sender.prototype.constructor = Sender; 19 | Sender.prototype._get_drain = function () { 20 | - if (this._draining && this._drained && this.credit) { 21 | + if (this._draining && this._drained) { 22 | while (this.credit) { 23 | ++this.delivery_count; 24 | --this.credit; 25 | diff --git a/node_modules/rhea/lib/types.js b/node_modules/rhea/lib/types.js 26 | index d5a27b9..a1ed067 100644 27 | --- a/node_modules/rhea/lib/types.js 28 | +++ b/node_modules/rhea/lib/types.js 29 | @@ -149,10 +149,11 @@ function hex(i) { 30 | return Number(i).toString(16); 31 | } 32 | 33 | -var types = {'by_code':{}}; 34 | +var types = {'by_code':{}, Typed}; 35 | Object.defineProperty(types, 'MAX_UINT', {value: 4294967295, writable: false, configurable: false}); 36 | Object.defineProperty(types, 'MAX_USHORT', {value: 65535, writable: false, configurable: false}); 37 | 38 | + 39 | function define_type(name, typecode, annotations, empty_value) { 40 | var t = new TypeDesc(name, typecode, annotations, empty_value); 41 | t.create.typecode = t.typecode;//hack 42 | -------------------------------------------------------------------------------- /scripts/amqp-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | ConnectionEvents, 4 | type ConnectionOptions, 5 | SenderEvents, 6 | SessionEvents, 7 | } from "rhea-promise" 8 | 9 | const port = 5671 10 | process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0" 11 | 12 | const connectionOptions: ConnectionOptions = { 13 | port, 14 | transport: "tls", 15 | host: "127.0.0.1", 16 | properties: { 17 | queue: "queue", 18 | }, 19 | } 20 | 21 | const connection = new Connection(connectionOptions) 22 | 23 | process.on("SIGINT", async () => { 24 | console.log("exiting gracefully...") 25 | await connection.close() 26 | process.exit(1) 27 | }) 28 | 29 | connection.on(ConnectionEvents.connectionClose, () => { 30 | console.log("connection closed!") 31 | }) 32 | 33 | await connection.open() 34 | 35 | { 36 | const sender = await connection.createAwaitableSender({ 37 | name: "beatrice", 38 | }) 39 | 40 | sender.addListener(SenderEvents.senderError, () => { 41 | console.log("sender error!") 42 | }) 43 | 44 | sender.session.on(SessionEvents.sessionClose, () => { 45 | console.log("session closed") 46 | }) 47 | 48 | sender.addListener(SenderEvents.senderClose, () => { 49 | console.log("sender closed!") 50 | }) 51 | 52 | sender.addListener(SenderEvents.senderOpen, () => {}) 53 | 54 | connection.on(ConnectionEvents.connectionError, () => { 55 | console.log("connection error") 56 | }) 57 | 58 | console.log("ok...") 59 | 60 | process.on("unhandledRejection", () => { 61 | console.log("unhandled rejection!!") 62 | process.exit(1) 63 | }) 64 | 65 | process.on("uncaughtException", () => { 66 | console.log("uncaught exception!!") 67 | process.exit(1) 68 | }) 69 | 70 | console.log("sending...") 71 | await sender.send({ 72 | body: "hi", 73 | }) 74 | console.log("sent!") 75 | 76 | await sender.close() 77 | } 78 | 79 | await connection.close() 80 | -------------------------------------------------------------------------------- /scripts/build-azl-go.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process" 2 | import os from "node:os" 3 | 4 | const cmd = `go build -o ./dist/binary/azl${os.platform() === "win32" ? ".exe" : ""} ./azl.go` 5 | 6 | execSync(cmd, { stdio: "inherit" }) 7 | -------------------------------------------------------------------------------- /scripts/bundle-cli.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora" 2 | import { create } from "tar" 3 | 4 | const bundle_filename = process.env["BUNDLE_FILENAME"] 5 | 6 | if (!bundle_filename) { 7 | console.error("BUNDLE_FILENAME is not set") 8 | process.exit(1) 9 | } 10 | 11 | const spinner = ora("Bundling CLI").start() 12 | 13 | await create( 14 | { 15 | file: `${bundle_filename}`, 16 | gzip: true, 17 | C: "./dist/binary", 18 | }, 19 | ["."], 20 | ) 21 | .then(() => spinner.succeed("Bundling CLI complete")) 22 | .catch((e) => { 23 | spinner.fail("Bundling CLI failed") 24 | console.error(e) 25 | process.exit(1) 26 | }) 27 | -------------------------------------------------------------------------------- /scripts/cli.ts: -------------------------------------------------------------------------------- 1 | import { program } from "@commander-js/extra-typings" 2 | import { runCli } from "../src/cli/index.js" 3 | 4 | runCli(program) 5 | -------------------------------------------------------------------------------- /scripts/start-server.ts: -------------------------------------------------------------------------------- 1 | import { startLocalSandboxServer } from "lib/server/start-server.js" 2 | 3 | void startLocalSandboxServer() 4 | -------------------------------------------------------------------------------- /src/cli/start-server.ts: -------------------------------------------------------------------------------- 1 | import "../../scripts/start-server.js" 2 | -------------------------------------------------------------------------------- /src/generated/azure-rest-api-specs/common-types/resource-management/v6/managedidentity.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | export const systemAssignedServiceIdentityType = z 4 | .enum(["None", "SystemAssigned"]) 5 | .describe( 6 | "Type of managed service identity (either system assigned, or none).", 7 | ) 8 | 9 | export const userAssignedIdentity = z 10 | .object({ 11 | principalId: z 12 | .string() 13 | .uuid() 14 | .describe("The principal ID of the assigned identity.") 15 | .readonly() 16 | .optional(), 17 | clientId: z 18 | .string() 19 | .uuid() 20 | .describe("The client ID of the assigned identity.") 21 | .readonly() 22 | .optional(), 23 | }) 24 | .describe("User assigned identity properties") 25 | 26 | export const managedServiceIdentityType = z 27 | .enum([ 28 | "None", 29 | "SystemAssigned", 30 | "UserAssigned", 31 | "SystemAssigned,UserAssigned", 32 | ]) 33 | .describe( 34 | "Type of managed service identity (where both SystemAssigned and UserAssigned types are allowed).", 35 | ) 36 | 37 | export const systemAssignedServiceIdentity = z 38 | .object({ 39 | principalId: z 40 | .string() 41 | .uuid() 42 | .describe( 43 | "The service principal ID of the system assigned identity. This property will only be provided for a system assigned identity.", 44 | ) 45 | .readonly() 46 | .optional(), 47 | tenantId: z 48 | .string() 49 | .uuid() 50 | .describe( 51 | "The tenant ID of the system assigned identity. This property will only be provided for a system assigned identity.", 52 | ) 53 | .readonly() 54 | .optional(), 55 | type: systemAssignedServiceIdentityType, 56 | }) 57 | .describe("Managed service identity (either system assigned, or none)") 58 | 59 | export const managedServiceIdentity = z 60 | .object({ 61 | principalId: z 62 | .string() 63 | .uuid() 64 | .describe( 65 | "The service principal ID of the system assigned identity. This property will only be provided for a system assigned identity.", 66 | ) 67 | .readonly() 68 | .optional(), 69 | tenantId: z 70 | .string() 71 | .uuid() 72 | .describe( 73 | "The tenant ID of the system assigned identity. This property will only be provided for a system assigned identity.", 74 | ) 75 | .readonly() 76 | .optional(), 77 | type: managedServiceIdentityType, 78 | userAssignedIdentities: z 79 | .record(userAssignedIdentity) 80 | .describe( 81 | "The set of user assigned identities associated with the resource. The userAssignedIdentities dictionary keys will be ARM resource ids in the form: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}. The dictionary values can be empty objects ({}) in requests.", 82 | ) 83 | .optional(), 84 | }) 85 | .describe( 86 | "Managed service identity (system assigned and/or user assigned identities)", 87 | ) 88 | 89 | export default {} as const 90 | -------------------------------------------------------------------------------- /src/generated/azure-rest-api-specs/servicebus/resource-manager/common/v2/definitions.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | export const proxyResource = z 4 | .object({ 5 | id: z 6 | .string() 7 | .describe( 8 | "Fully qualified resource ID for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}", 9 | ) 10 | .readonly() 11 | .optional(), 12 | name: z.string().describe("The name of the resource").readonly().optional(), 13 | type: z 14 | .string() 15 | .describe( 16 | 'The type of the resource. E.g. "Microsoft.EventHub/Namespaces" or "Microsoft.EventHub/Namespaces/EventHubs"', 17 | ) 18 | .readonly() 19 | .optional(), 20 | location: z 21 | .string() 22 | .describe("The geo-location where the resource lives") 23 | .readonly() 24 | .optional(), 25 | }) 26 | .describe( 27 | "Common fields that are returned in the response for all Azure Resource Manager resources", 28 | ) 29 | 30 | export default {} as const 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/index.js" 2 | -------------------------------------------------------------------------------- /src/lib/api/errors.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxsdev/LocalSandbox/4899dc13f423847e3f0991f09486e0f70df69ffe/src/lib/api/errors.ts -------------------------------------------------------------------------------- /src/lib/api/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createWithEdgeSpec, 3 | type EdgeSpecRouteMap, 4 | type Middleware, 5 | } from "edgespec" 6 | import { z } from "zod" 7 | import { routeBundleFromRouteMap } from "../edgespec-util/route-bundle-from-route-map.js" 8 | import { createWithDefaultExceptionHandling } from "edgespec/middleware/index.js" 9 | import { createAzureIntegration } from "../integration/index.js" 10 | import type { Logger } from "pino" 11 | import { AzureServiceBusBroker } from "../broker/broker.js" 12 | import { azure_routes } from "../integration/azure/routes.js" 13 | import { withLogger } from "../logger/with-logger.js" 14 | import { getTestLogger } from "lib/logger/get-test-logger.js" 15 | import type { BrokerServer } from "lib/broker/broker-server.js" 16 | 17 | const routeLoggingMiddleware: Middleware<{}, { logger: Logger }> = async ( 18 | req, 19 | ctx, 20 | next, 21 | ) => { 22 | // TODO: inject logger from context 23 | const { logger, cleanup } = getTestLogger("api") 24 | 25 | logger.debug( 26 | { url: req.url, method: req.method, body: await req.clone().text() }, 27 | "Incoming API Request", 28 | ) 29 | 30 | ctx.logger = logger 31 | 32 | const res = await next(req, ctx) 33 | 34 | await cleanup() 35 | 36 | return res 37 | } 38 | 39 | const withRouteSpec = createWithEdgeSpec({ 40 | authMiddleware: {}, 41 | beforeAuthMiddleware: [ 42 | routeLoggingMiddleware, 43 | createWithDefaultExceptionHandling(), 44 | ], 45 | }) 46 | 47 | export const createApiBundle = ( 48 | settings: { 49 | amqp_logger?: { logger: Logger; cleanup?: () => Promise } 50 | logger?: Logger 51 | } = {}, 52 | ) => { 53 | let { amqp_logger, logger } = settings 54 | amqp_logger ??= getTestLogger("amqp") 55 | 56 | const store_bundle = azure_routes.createStoreBundle() 57 | 58 | const azure_service_bus_broker = new AzureServiceBusBroker( 59 | store_bundle.store, 60 | { 61 | logger: amqp_logger.logger, 62 | cleanup: amqp_logger.cleanup, 63 | tls: false, 64 | }, 65 | ) 66 | 67 | const azure_integration = createAzureIntegration({ 68 | logger, 69 | broker: azure_service_bus_broker, 70 | store_bundle, 71 | }) 72 | 73 | const routes: EdgeSpecRouteMap = { 74 | "/azure/[...params]": withRouteSpec({ 75 | methods: ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"], 76 | routeParams: z.object({ 77 | params: z.string().array(), 78 | }), 79 | })(async (req, ctx) => { 80 | return await azure_integration(req, { 81 | middleware: [withLogger(ctx.logger.child({}))], 82 | }) 83 | }), 84 | // "/[...params]": withRouteSpec({ 85 | // methods: ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"], 86 | // routeParams: z.object({ 87 | // params: z.string().array(), 88 | // }), 89 | // })(async () => { 90 | // return new Response("Not Found", { status: 404 }) 91 | // }), 92 | } 93 | 94 | const amqp_server: BrokerServer = azure_service_bus_broker 95 | 96 | return { 97 | bundle: routeBundleFromRouteMap(routes), 98 | amqp_server, 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/api/serve.ts: -------------------------------------------------------------------------------- 1 | import { startServer } from "edgespec/adapters/node.js" 2 | import type { EdgeSpecRouteBundle } from "edgespec" 3 | import type { CertificateStore } from "lib/cert/certificate-store.js" 4 | 5 | export const serve = async ( 6 | bundle: EdgeSpecRouteBundle, 7 | port: number, 8 | cert?: CertificateStore, 9 | ) => { 10 | return await startServer(bundle, { port, tls: await cert?.get() }) 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/broker/constants.ts: -------------------------------------------------------------------------------- 1 | import rhea from "rhea" 2 | 3 | export const BrokerConstants = { 4 | debug: { 5 | operations: { 6 | setSequenceNumber: "x-localsandbox-set-sequence-number", 7 | }, 8 | }, 9 | messageState: { 10 | active: rhea.types.wrap_int(0), 11 | deferred: rhea.types.wrap_int(1), 12 | scheduled: rhea.types.wrap_int(2), 13 | }, 14 | deadLetterReason: "DeadLetterReason", 15 | deadLetterDescription: "DeadLetterErrorDescription", 16 | deadLetterSubqueue: "$DeadLetterQueue", 17 | subscriptionsSubqueue: "Subscriptions", 18 | errors: { 19 | messageExpired: { 20 | reason: "TTLExpiredException", 21 | description: "The message expired and was dead lettered.", 22 | }, 23 | maxDeliveryCountExceeded: { 24 | reason: "MaxDeliveryCountExceeded", 25 | description: "Message could not be consumed after 2 delivery attempts.", 26 | }, 27 | }, 28 | } as const 29 | -------------------------------------------------------------------------------- /src/lib/broker/errors.ts: -------------------------------------------------------------------------------- 1 | import { ErrorNameConditionMapper } from "@azure/core-amqp" 2 | import type { AmqpError } from "rhea" 3 | 4 | export abstract class StoreBusError extends Error { 5 | constructor( 6 | name: string, 7 | public readonly condition: ErrorNameConditionMapper, 8 | public readonly description?: string, 9 | ) { 10 | super(description) 11 | this.name = name 12 | } 13 | 14 | get amqpError(): AmqpError { 15 | return { 16 | condition: this.condition, 17 | description: this.description, 18 | } 19 | } 20 | } 21 | 22 | export class SessionCannotBeLockedError extends StoreBusError { 23 | constructor(public session_id: string) { 24 | super( 25 | "SessionLockedError", 26 | ErrorNameConditionMapper.SessionCannotBeLockedError, 27 | `The requested session '${session_id}' cannot be accepted. It may be locked by another receiver.`, 28 | ) 29 | } 30 | } 31 | 32 | export class SessionRequiredError extends StoreBusError { 33 | constructor() { 34 | super( 35 | "SessionRequiredError", 36 | ErrorNameConditionMapper.InvalidOperationError, 37 | "The SessionId was not set on a message, and it cannot be sent to the entity. Entities that have session support enabled can only receive messages that have the SessionId set to a valid value.", 38 | ) 39 | } 40 | } 41 | 42 | export class CannotCreateSessionfulReceiverError extends StoreBusError { 43 | constructor() { 44 | super( 45 | "CannotCreateSessionfulReceiverError", 46 | ErrorNameConditionMapper.InvalidOperationError, 47 | "A sessionful message receiver cannot be created on an entity that does not require sessions. Ensure RequiresSession is set to true when creating a Queue or Subscription to enable sessionful behavior.", 48 | ) 49 | } 50 | } 51 | 52 | export class AutoForwardingRequiredError extends StoreBusError { 53 | constructor() { 54 | super( 55 | "AutoForwardingRequiredError", 56 | ErrorNameConditionMapper.InvalidOperationError, 57 | "Cannot create a message receiver on an entity with auto-forwarding enabled.", 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/broker/index.ts: -------------------------------------------------------------------------------- 1 | import type { ListenOptions } from "net" 2 | import { create_container } from "rhea" 3 | 4 | export async function serveAMQP(port: number) { 5 | const container = create_container({}) 6 | const containerListenOptions: ListenOptions = { 7 | port, 8 | } 9 | const server = container.listen(containerListenOptions) 10 | 11 | container.on("sender_open", () => {}) 12 | 13 | return server 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/broker/types.ts: -------------------------------------------------------------------------------- 1 | import type { Delivery, Sender } from "rhea" 2 | import type { azure_routes } from "../integration/azure/routes.js" 3 | import type { IntegrationStore } from "../integration/integration.js" 4 | 5 | export type BrokerStore = IntegrationStore 6 | 7 | export type QueueModel = BrokerStore["sb_queue"]["_type"] & { 8 | _model: "sb_queue" 9 | } 10 | 11 | export type TopicModel = BrokerStore["sb_topic"]["_type"] & { 12 | _model: "sb_topic" 13 | } 14 | 15 | export type SubscriptionModel = BrokerStore["sb_subscription"]["_type"] & { 16 | _model: "sb_subscription" 17 | } 18 | 19 | export type QueueOrTopicModel = QueueModel | TopicModel 20 | export type QueueOrSubscriptionModel = QueueModel | SubscriptionModel 21 | export type QueueOrTopicOrSubscriptionModel = 22 | | QueueModel 23 | | TopicModel 24 | | SubscriptionModel 25 | 26 | export interface QualifiedNamespaceId { 27 | subscription_id: string 28 | namespace_name: string 29 | resource_group_name: string 30 | } 31 | 32 | export type SubqueueType = "deadletter" 33 | 34 | export interface QualifiedQueueOrTopicId extends QualifiedNamespaceId { 35 | queue_or_topic_name: string 36 | } 37 | 38 | export interface QualifiedQueueOrTopicOrSubscriptionId 39 | extends QualifiedQueueOrTopicId { 40 | subscription_name?: string 41 | } 42 | 43 | export interface _QualifiedQueueId extends QualifiedNamespaceId { 44 | queue_name: string 45 | } 46 | 47 | export interface _QualifiedTopicId extends QualifiedNamespaceId { 48 | topic_name: string 49 | } 50 | 51 | export interface _QualifiedSubscriptionId extends _QualifiedTopicId { 52 | subscription_name: string 53 | } 54 | 55 | export type QualifiedQueueOrSubscriptionIdWithSubqueueType = ( 56 | | _QualifiedQueueId 57 | | _QualifiedSubscriptionId 58 | ) & { 59 | subqueue: SubqueueType | undefined 60 | } 61 | 62 | export type QualifiedMessageDestinationId = ( 63 | | _QualifiedQueueId 64 | | _QualifiedTopicId 65 | | QualifiedQueueOrTopicId 66 | ) & { 67 | subqueue: SubqueueType | undefined 68 | } 69 | 70 | export type QualifiedMessageSourceId = 71 | QualifiedQueueOrSubscriptionIdWithSubqueueType 72 | 73 | export type DeliveryTag = Pick & Partial> 74 | export type SenderName = Pick 75 | -------------------------------------------------------------------------------- /src/lib/broker/url.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | import { BrokerConstants } from "./constants.js" 3 | import { Constants } from "@azure/core-amqp" 4 | 5 | const baseTuple = [ 6 | z.string().max(0), 7 | // z.string(), 8 | // z.string(), 9 | // z.string(), 10 | z.string(), 11 | ] as const 12 | 13 | const brokerPathnameSchema = z 14 | .union([ 15 | z.tuple([...baseTuple]), 16 | // z.tuple([...baseTuple, z.string()]), 17 | z.tuple([ 18 | ...baseTuple, 19 | z.literal(BrokerConstants.subscriptionsSubqueue), 20 | z.string(), 21 | ]), 22 | ]) 23 | .transform( 24 | ([ 25 | , 26 | // subscription_id, 27 | // resource_group_name, 28 | // namespace_name, 29 | queue_or_topic_name, 30 | subqueue_type, 31 | subqueue_name, 32 | ]) => { 33 | return { 34 | // subscription_id, 35 | // resource_group_name, 36 | // namespace_name, 37 | queue_or_topic_name, 38 | ...(subqueue_type === "Subscriptions" && subqueue_name 39 | ? { subscription_name: subqueue_name } 40 | : {}), 41 | } 42 | }, 43 | ) 44 | 45 | const brokerHostnameSchema = z 46 | .tuple([z.string(), z.string(), z.string()]) 47 | .rest(z.string()) 48 | .transform(([subscription_id, resource_group_name, namespace_name]) => ({ 49 | namespace_name, 50 | resource_group_name, 51 | subscription_id, 52 | })) 53 | 54 | export const parseBrokerURL = (url: URL) => { 55 | const hostParts = url.hostname.split(".") 56 | const parts = url.pathname.split("/") 57 | 58 | const internal = z.enum([Constants.management]).safeParse(parts.at(-1)) 59 | if (internal.success) parts.pop() 60 | 61 | const subqueue = z 62 | .literal(BrokerConstants.deadLetterSubqueue) 63 | .transform(() => "deadletter" as const) 64 | .safeParse(parts.at(-1)) 65 | if (subqueue.success) parts.pop() 66 | 67 | return { 68 | ...brokerHostnameSchema.parse(hostParts), 69 | ...brokerPathnameSchema.parse(parts), 70 | 71 | ...(subqueue.success 72 | ? { 73 | subqueue: subqueue.data, 74 | } 75 | : {}), 76 | 77 | ...(internal.success 78 | ? { 79 | internal: internal.data, 80 | } 81 | : {}), 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/cert/certificate-store.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import { 3 | getDefaultConfigStore, 4 | type ConfigStore, 5 | } from "lib/config/config-store.js" 6 | import { getServerEnv, type ServerEnv } from "lib/server/env.js" 7 | import type { TlsOptions } from "tls" 8 | import { z } from "zod" 9 | 10 | export interface CertificateStore { 11 | get: () => Promise 12 | } 13 | 14 | export class ConfigCertificateStore implements CertificateStore { 15 | constructor( 16 | private readonly config: ConfigStore, 17 | private readonly env: ServerEnv, 18 | ) {} 19 | 20 | async get() { 21 | // TODO: re-enable cert caching 22 | 23 | // const { cert } = this.config.get() 24 | 25 | // if (cert && new Date(cert.expiration) > new Date()) { 26 | // return cert 27 | // } else if (cert) { 28 | // // invalidate cert 29 | // this.config.update({ cert: undefined }) 30 | // } 31 | 32 | const tls = z 33 | .object({ 34 | cert: z.string(), 35 | key: z.string(), 36 | }) 37 | .parse( 38 | await (await fetch(this.env.LOCALSANDBOX_CERT_RETRIEVAL_URL)).json(), 39 | ) 40 | 41 | const expiration = new Date( 42 | Date.now() + 43 | Temporal.Duration.from( 44 | this.env.LOCALSANDBOX_CERT_CACHE_EXPIRATION, 45 | ).total("milliseconds"), 46 | ) 47 | 48 | this.config.update({ 49 | cert: { ...tls, expiration: expiration.toISOString() }, 50 | }) 51 | 52 | return tls 53 | } 54 | } 55 | 56 | export class DefaultConfigCertificateStore extends ConfigCertificateStore { 57 | constructor() { 58 | super(getDefaultConfigStore(), getServerEnv()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/config/config-store.ts: -------------------------------------------------------------------------------- 1 | import Configstore from "lib/configstore/index.js" 2 | import { z } from "zod" 3 | 4 | const storeSchema = z 5 | .object({ 6 | server: z 7 | .object({ 8 | port: z.number(), 9 | pid: z.number(), 10 | }) 11 | .optional(), 12 | 13 | cert: z 14 | .object({ 15 | key: z.string(), 16 | cert: z.string(), 17 | expiration: z.string().datetime(), 18 | }) 19 | .optional(), 20 | }) 21 | .optional() 22 | .default({}) 23 | 24 | export type StoreConfig = z.output 25 | 26 | export class ConfigStore { 27 | constructor(private readonly key: string) {} 28 | 29 | private readonly store = new Configstore("localsandbox", { 30 | config: storeSchema.parse(undefined), 31 | }) 32 | 33 | get() { 34 | const config = storeSchema.safeParse(this.store.get(this.key)).data 35 | return config ?? {} 36 | } 37 | 38 | update(update: Partial>) { 39 | const new_config = storeSchema.parse({ 40 | ...this.store.get(this.key), 41 | ...update, 42 | }) 43 | 44 | this.store.set(this.key, new_config) 45 | } 46 | } 47 | 48 | const default_configstore = new ConfigStore("localsandbox") 49 | 50 | export const getDefaultConfigStore = () => default_configstore 51 | -------------------------------------------------------------------------------- /src/lib/configstore/LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) Google 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/lib/configstore/index.d.ts: -------------------------------------------------------------------------------- 1 | export default class Configstore { 2 | constructor(packageName: string, defaults?: any, options?: ConfigstoreOptions) 3 | 4 | /** 5 | * Get the path to the config file. Can be used to show the user 6 | * where it is, or better, open it for them. 7 | */ 8 | path: string 9 | 10 | /** 11 | * Get all items as an object or replace the current config with an object. 12 | */ 13 | all: any 14 | 15 | /** 16 | * Get the item count 17 | */ 18 | size: number 19 | 20 | /** 21 | * Get an item 22 | * @param key The string key to get 23 | * @return The contents of the config from key $key 24 | */ 25 | get(key: string): any 26 | 27 | /** 28 | * Set an item 29 | * @param key The string key 30 | * @param val The value to set 31 | */ 32 | set(key: string, val: any): void 33 | 34 | /** 35 | * Set all key/value pairs declared in $values 36 | * @param values The values object. 37 | */ 38 | set(values: any): void 39 | 40 | /** 41 | * Determines if a key is present in the config 42 | * @param key The string key to test for 43 | * @return True if the key is present 44 | */ 45 | has(key: string): boolean 46 | 47 | /** 48 | * Delete an item. 49 | * @param key The key to delete 50 | */ 51 | delete(key: string): void 52 | 53 | /** 54 | * Clear the config. 55 | * Equivalent to Configstore.all = {}; 56 | */ 57 | clear(): void 58 | } 59 | 60 | export interface ConfigstoreOptions { 61 | globalConfigPath?: boolean | undefined 62 | configPath?: string | undefined 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/edgespec-util/route-bundle-from-route-map.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type EdgeSpecRouteBundle, 3 | type EdgeSpecRouteMap, 4 | makeRequestAgainstEdgeSpec, 5 | } from "edgespec" 6 | import { getRouteMatcher } from "next-route-matcher" 7 | 8 | export function routeBundleFromRouteMap( 9 | routeMap: EdgeSpecRouteMap, 10 | ): EdgeSpecRouteBundle { 11 | const edgeSpecRouteBundle: EdgeSpecRouteBundle = { 12 | routeMatcher: getRouteMatcher(Object.keys(routeMap)), 13 | routeMapWithHandlers: routeMap, 14 | makeRequest: async (req, options) => 15 | await makeRequestAgainstEdgeSpec(edgeSpecRouteBundle, options)(req), 16 | } 17 | 18 | return edgeSpecRouteBundle 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/edgespec/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Seam Labs Inc. 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 | -------------------------------------------------------------------------------- /src/lib/edgespec/adapters/node.ts: -------------------------------------------------------------------------------- 1 | import type tls from "node:tls" 2 | import type { NodeHandler } from "@edge-runtime/node-utils" 3 | import http from "node:http" 4 | import https from "node:https" 5 | import { transformToNodeBuilder } from "edgespec/edge/transform-to-node.js" 6 | import type { Middleware } from "edgespec/middleware/index.js" 7 | import type { EdgeSpecAdapter } from "edgespec/types/edge-spec.js" 8 | 9 | export interface EdgeSpecNodeAdapterOptions { 10 | middleware?: Middleware[] 11 | port?: number 12 | } 13 | 14 | export const getNodeHandler: EdgeSpecAdapter< 15 | [EdgeSpecNodeAdapterOptions], 16 | NodeHandler 17 | > = (edgeSpec, { port, middleware = [] }) => { 18 | const transformToNode = transformToNodeBuilder({ 19 | defaultOrigin: `http://localhost${port ? `:${port}` : ""}`, 20 | }) 21 | 22 | return transformToNode( 23 | async (req) => 24 | await edgeSpec.makeRequest(req as Request, { 25 | middleware, 26 | }), 27 | ) 28 | } 29 | 30 | export const startServer: EdgeSpecAdapter< 31 | [EdgeSpecNodeAdapterOptions & { tls?: tls.SecureContextOptions }], 32 | Promise 33 | > = async (edgeSpec, opts) => { 34 | const handler = getNodeHandler(edgeSpec, opts) 35 | 36 | let server: http.Server | https.Server 37 | if (opts.tls) { 38 | server = https.createServer(opts.tls, handler) 39 | } else { 40 | server = http.createServer(getNodeHandler(edgeSpec, opts)) 41 | } 42 | 43 | const { port } = opts 44 | 45 | await new Promise((resolve, reject) => { 46 | server.on("error", reject) 47 | server.listen(port, resolve) 48 | }) 49 | 50 | return server 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/edgespec/config/config.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | const edgeSpecConfigSchema = z 4 | .object({ 5 | /** 6 | * Defaults to the current working directory. 7 | */ 8 | rootDirectory: z.string().optional(), 9 | /** 10 | * If this path is relative, it's resolved relative to the `rootDirectory` option. 11 | */ 12 | tsconfigPath: z.string().optional(), 13 | /** 14 | * If this path is relative, it's resolved relative to the `rootDirectory` option. 15 | */ 16 | routesDirectory: z.string().optional(), 17 | /** 18 | * The platform you're targeting. 19 | * 20 | * Defaults to `wintercg-minimal`, and you should use this whenever possible for maximal compatibility. 21 | * 22 | * Check [the docs](https://github.com/seamapi/edgespec/blob/main/docs/edgespec-config.md) for more information. 23 | */ 24 | platform: z 25 | .enum(["node", "wintercg-minimal"]) 26 | .default("wintercg-minimal") 27 | .optional(), 28 | }) 29 | .strict() 30 | 31 | export type EdgeSpecConfig = z.infer 32 | 33 | export const defineConfig = (config: EdgeSpecConfig): EdgeSpecConfig => { 34 | const parsedConfig = edgeSpecConfigSchema.safeParse(config) 35 | 36 | if (parsedConfig.success) { 37 | return parsedConfig.data 38 | } 39 | 40 | throw new Error(`Invalid config: ${parsedConfig.error.toString()}`) 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/edgespec/config/index.ts: -------------------------------------------------------------------------------- 1 | export { loadConfig } from "./utils.js" 2 | export * from "./config.js" 3 | -------------------------------------------------------------------------------- /src/lib/edgespec/edge/transform-to-node.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildToNodeHandler, 3 | type RequestOptions, 4 | } from "@edge-runtime/node-utils" 5 | import primitives from "@edge-runtime/primitives" 6 | 7 | export interface TransformToNodeOptions extends RequestOptions {} 8 | 9 | const dependencies = { 10 | ...primitives, 11 | Uint8Array, 12 | } 13 | 14 | export const transformToNodeBuilder = (options: TransformToNodeOptions) => 15 | buildToNodeHandler(dependencies, options) 16 | -------------------------------------------------------------------------------- /src/lib/edgespec/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { EdgeSpecRouteBundle } from "./types/index.js" 2 | 3 | /** 4 | * Loads a file created by `edgespec bundle` and returns the default export. 5 | * This is a very thin wrapper over `import()` that adds some types. 6 | */ 7 | export const loadBundle = async ( 8 | bundlePath: string, 9 | ): Promise => { 10 | const bundle = await import(bundlePath) 11 | // If the file is imported as CJS, the default export is nested. 12 | // Naming this with .mjs seems to break some on-the-fly transpiling tools downstream. 13 | return bundle.default.default ?? bundle.default 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/edgespec/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types/edge-spec.js" 2 | export * from "./create-with-edge-spec.js" 3 | export { defineConfig } from "./config/config.js" 4 | export type { EdgeSpecConfig } from "./config/config.js" 5 | export type * from "./types/index.js" 6 | export { 7 | EdgeSpecJsonResponse, 8 | EdgeSpecCustomResponse, 9 | EdgeSpecResponse, 10 | } from "./types/web-handler.js" 11 | export * from "./helpers.js" 12 | -------------------------------------------------------------------------------- /src/lib/edgespec/lib/async-work-tracker.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "node:events" 2 | 3 | /** 4 | * This is effectively a Promise that can be resolved multiple times in different scopes without messily holding references to the resolve function. 5 | */ 6 | export class AsyncWorkTracker extends EventEmitter { 7 | private state: "idle" | "pending" | "resolved" = "idle" 8 | private lastResult: Result | undefined 9 | 10 | /** 11 | * If work is still pending, this waits for the next work result. If work is already resolved, it returns the last result. 12 | */ 13 | async waitForResult(): Promise { 14 | if (this.state === "pending" || !this.lastResult) { 15 | return await new Promise((resolve) => { 16 | this.once("result", resolve) 17 | }) 18 | } 19 | 20 | if (!this.lastResult) { 21 | throw new Error("No last result (this should never happen)") 22 | } 23 | 24 | return this.lastResult 25 | } 26 | 27 | /** 28 | * Call this when you start async work. 29 | */ 30 | beginAsyncWork() { 31 | this.state = "pending" 32 | } 33 | 34 | /** 35 | * Call this when the async work is done with the result. 36 | */ 37 | finishAsyncWork(result: Result) { 38 | this.state = "resolved" 39 | this.emit("result", result) 40 | this.lastResult = result 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/edgespec/lib/format-zod-error.ts: -------------------------------------------------------------------------------- 1 | import type { ZodIssue, ZodError } from "zod" 2 | 3 | const zodIssueToString = (issue: ZodIssue) => { 4 | if (issue.path.join(".") === "") { 5 | return issue.message 6 | } 7 | if (issue.message === "Required") { 8 | return `\`${issue.path.join(".")}\` is required` 9 | } 10 | return `${issue.message} for "${issue.path.join(".")}"` 11 | } 12 | 13 | export const formatZodError = (error: ZodError): string => { 14 | let message: string 15 | if (error.issues.length === 1) { 16 | const issue = error.issues[0]! 17 | message = zodIssueToString(issue) 18 | } else { 19 | const message_components: string[] = [] 20 | for (const issue of error.issues) { 21 | message_components.push(zodIssueToString(issue)) 22 | } 23 | message = 24 | `${error.issues.length} Zod validation issues: ` + 25 | message_components.join(", ") 26 | } 27 | 28 | message += `. Full Zod error: ${JSON.stringify(error.issues, null, 2)}` 29 | 30 | return message 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/edgespec/lib/normalize-route-map.ts: -------------------------------------------------------------------------------- 1 | export const normalizeRouteMap = (routeMap: Record) => { 2 | const normalizedRoutes: Record = {} 3 | for (const route of Object.keys(routeMap)) { 4 | let routeWithSlash = route 5 | if (!route.startsWith("/")) { 6 | routeWithSlash = `/${route}` 7 | } 8 | normalizedRoutes[`${routeWithSlash.replace(/\.ts$/g, "")}`] = route 9 | if (route.endsWith("index.ts")) { 10 | normalizedRoutes[`${routeWithSlash.replace(/index\.ts$/g, "")}`] = route 11 | } 12 | } 13 | return normalizedRoutes 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/edgespec/middleware/http-exceptions.ts: -------------------------------------------------------------------------------- 1 | import { formatZodError } from "edgespec/lib/format-zod-error.js" 2 | import type { z } from "zod" 3 | 4 | export interface HttpException { 5 | status: number 6 | message?: string 7 | 8 | _isHttpException: true 9 | } 10 | 11 | export abstract class EdgeSpecMiddlewareError 12 | extends Error 13 | implements HttpException 14 | { 15 | _isHttpException = true as const 16 | 17 | constructor( 18 | public override message: string, 19 | public status: number = 500, 20 | ) { 21 | super(message) 22 | this.name = this.constructor.name 23 | } 24 | } 25 | 26 | export class MethodNotAllowedError extends EdgeSpecMiddlewareError { 27 | constructor(allowedMethods: readonly string[]) { 28 | super(`only ${allowedMethods.join(",")} accepted`, 405) 29 | } 30 | } 31 | 32 | export class NotFoundError extends EdgeSpecMiddlewareError { 33 | constructor(message: string) { 34 | super(message, 404) 35 | } 36 | } 37 | 38 | export abstract class BadRequestError extends EdgeSpecMiddlewareError { 39 | constructor(message: string) { 40 | super(message, 400) 41 | } 42 | } 43 | 44 | export class InvalidQueryParamsError extends BadRequestError {} 45 | export class InvalidContentTypeError extends BadRequestError {} 46 | export class InputParsingError extends BadRequestError {} 47 | 48 | export class InputValidationError extends BadRequestError { 49 | constructor(error: z.ZodError) { 50 | super(formatZodError(error)) 51 | } 52 | } 53 | 54 | export class ResponseValidationError extends EdgeSpecMiddlewareError { 55 | constructor(error: z.ZodError) { 56 | super(formatZodError(error), 500) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/edgespec/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./http-exceptions.js" 2 | export * from "./with-default-exception-handling.js" 3 | export * from "./with-logger.js" 4 | 5 | export type { Middleware, MiddlewareChain } from "./types.js" 6 | -------------------------------------------------------------------------------- /src/lib/edgespec/middleware/types.ts: -------------------------------------------------------------------------------- 1 | import type { MapArray } from "../types/util.js" 2 | import type { 3 | EdgeSpecRequest, 4 | SerializableToResponse, 5 | } from "../types/web-handler.js" 6 | 7 | export type Middleware< 8 | RequiredContext = {}, 9 | NewContext = {}, 10 | RequestOptions = {}, 11 | > = ( 12 | request: EdgeSpecRequest< 13 | { 14 | routeParams: Readonly> 15 | } & RequestOptions 16 | >, 17 | ctx: RequiredContext & Partial, 18 | next: ( 19 | request: EdgeSpecRequest, 20 | ctx: RequiredContext & Partial, 21 | ) => Promise, 22 | ) => 23 | | Response 24 | | SerializableToResponse 25 | | Promise 26 | 27 | export type MiddlewareChain = ReadonlyArray< 28 | Middleware 29 | > 30 | 31 | /** 32 | * Collect all result options from a middleware chain 33 | * 34 | * For example: 35 | * 36 | * ```ts 37 | * Middleware<{}, { auth: string }> 38 | * Middleware<{}, { user: string }> 39 | * 40 | * -> 41 | * 42 | * { auth: string, user: string } 43 | * ``` 44 | */ 45 | export type AccumulateMiddlewareChainResultOptions< 46 | MiddlewareChain, 47 | AccumulationType extends "union" | "intersection", 48 | > = MiddlewareChain extends readonly [ 49 | Middleware, 50 | ...infer Remaining, 51 | ] 52 | ? AccumulationType extends "intersection" 53 | ? ResultOptions & 54 | AccumulateMiddlewareChainResultOptions 55 | : 56 | | ResultOptions 57 | | AccumulateMiddlewareChainResultOptions 58 | : MiddlewareChain extends readonly any[] 59 | ? AccumulationType extends "intersection" 60 | ? {} 61 | : never 62 | : never 63 | 64 | /** 65 | * Picks out a subset of middlewares from a map, maintaining the order given in the array 66 | * 67 | * For example: 68 | * 69 | * ```ts 70 | * MiddlewareMap = { 71 | * "session_token": Middleware<{}, { session_token: string }>, 72 | * "pat": Middleware<{}, { pat: string }>, 73 | * "api_token": Middleware<{}, { api_token: string }> 74 | * } 75 | * Middlewares = ["session_token", "pat"] 76 | * 77 | * -> 78 | * 79 | * [ Middleware<{}, { session_token: string }>, Middleware<{}, { pat: string }> ] 80 | * ``` 81 | * 82 | */ 83 | export type MapMiddlewares< 84 | MiddlewareMap extends Record, 85 | Middlewares extends 86 | | ReadonlyArray 87 | | keyof MiddlewareMap 88 | | "none", 89 | > = 90 | Middlewares extends ReadonlyArray 91 | ? MapArray 92 | : Middlewares extends infer K extends keyof MiddlewareMap 93 | ? readonly [MiddlewareMap[K]] 94 | : readonly [] 95 | -------------------------------------------------------------------------------- /src/lib/edgespec/middleware/with-default-exception-handling.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware } from "edgespec/middleware/types.js" 2 | import type { Logger } from "./with-logger.js" 3 | import kleur from "kleur" 4 | 5 | interface CreateWithDefaultExceptionHandlingOptions { 6 | coloredLogs?: boolean 7 | logWhen?: (status: number, error: unknown) => boolean 8 | includeStackTraceInResponse?: boolean 9 | } 10 | 11 | /** 12 | * 13 | */ 14 | export const createWithDefaultExceptionHandling = 15 | ({ 16 | coloredLogs = true, 17 | logWhen = (status) => status >= 500, 18 | includeStackTraceInResponse = true, 19 | }: CreateWithDefaultExceptionHandlingOptions = {}): Middleware<{ 20 | logger?: Logger 21 | }> => 22 | async (req, ctx, next) => { 23 | try { 24 | return await next(req, ctx) 25 | } catch (e: any) { 26 | let response: Response 27 | if ("_isHttpException" in e) { 28 | response = Response.json( 29 | { 30 | message: e.message, 31 | stack: includeStackTraceInResponse ? e.stack : undefined, 32 | }, 33 | { 34 | status: e.status, 35 | }, 36 | ) 37 | } else { 38 | response = Response.json( 39 | { 40 | message: "Internal server error", 41 | stack: includeStackTraceInResponse ? e.stack : undefined, 42 | }, 43 | { 44 | status: 500, 45 | }, 46 | ) 47 | } 48 | 49 | if (logWhen(response.status, e)) { 50 | const logger = ctx.logger ?? console 51 | kleur.enabled = coloredLogs 52 | 53 | const { pathname } = new URL(req.url) 54 | const routeLog = 55 | response.status >= 500 56 | ? kleur.bgRed(`${pathname} threw an error`) 57 | : kleur.bgYellow(`${pathname} threw an error`) 58 | 59 | logger.error( 60 | `${routeLog}: ${e instanceof Error ? e.stack : e.toString()}`, 61 | ) 62 | } 63 | 64 | return response 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/edgespec/middleware/with-logger.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware } from "./types.js" 2 | 3 | export type Logger = { 4 | debug: (...args: any[]) => void 5 | info: (...args: any[]) => void 6 | warn: (...args: any[]) => void 7 | error: (...args: any[]) => void 8 | } 9 | 10 | /** 11 | * Attaches a provided logger to ctx.logger. 12 | * `ctx.logger` is used by internal EdgeSpec middleware when provided (instead of `console`). 13 | */ 14 | export const createWithLogger = 15 | ( 16 | logger: L, 17 | ): Middleware< 18 | {}, 19 | { 20 | logger: L 21 | } 22 | > => 23 | async (req, ctx, next) => { 24 | ctx.logger = logger 25 | return await next(req, ctx) 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/edgespec/middleware/with-methods.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware } from "edgespec/middleware/types.js" 2 | import type { HTTPMethods } from "edgespec/types/web-handler.js" 3 | import { MethodNotAllowedError } from "./http-exceptions.js" 4 | 5 | export const withMethods = 6 | (methods: readonly HTTPMethods[]): Middleware => 7 | async (req, ctx, next) => { 8 | if (!(methods as string[]).includes(req.method)) { 9 | throw new MethodNotAllowedError(methods) 10 | } 11 | 12 | return await next(req, ctx) 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/edgespec/middleware/with-unhandled-exception-handling.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware } from "edgespec/middleware/types.js" 2 | import type { Logger } from "./with-logger.js" 3 | 4 | export const withUnhandledExceptionHandling: Middleware<{ 5 | logger?: Logger 6 | }> = async (req, ctx, next) => { 7 | try { 8 | return await next(req, ctx) 9 | } catch (e: any) { 10 | const logger = ctx.logger ?? console 11 | 12 | if ("_isHttpException" in e) { 13 | logger.warn( 14 | "Caught unhandled HTTP exception thrown by EdgeSpec provided middleware. Consider adding createWithDefaultExceptionHandling middleware to your global or route spec.", 15 | ) 16 | } else { 17 | logger.warn( 18 | "Caught unknown unhandled exception; consider adding a exception handling middleware to your global or route spec.", 19 | ) 20 | } 21 | 22 | logger.error(e) 23 | 24 | return new Response(null, { 25 | status: 500, 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/edgespec/types/context.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type EdgeSpecCustomResponse, 3 | type EdgeSpecJsonResponse, 4 | type EdgeSpecMultiPartFormDataResponse, 5 | EdgeSpecResponse, 6 | type SerializableToResponse, 7 | } from "./web-handler.js" 8 | 9 | export type ResponseTypeToContext< 10 | ResponseType extends SerializableToResponse | Response, 11 | > = 12 | Exclude extends EdgeSpecJsonResponse 13 | ? { 14 | json: typeof EdgeSpecResponse.json 15 | } 16 | : Exclude extends EdgeSpecMultiPartFormDataResponse< 17 | infer T 18 | > 19 | ? { 20 | multipartFormData: typeof EdgeSpecResponse.multipartFormData 21 | } 22 | : Exclude extends EdgeSpecCustomResponse< 23 | infer T, 24 | infer C 25 | > 26 | ? { 27 | custom: typeof EdgeSpecResponse.custom 28 | } 29 | : { 30 | json: typeof EdgeSpecResponse.json 31 | multipartFormData: typeof EdgeSpecResponse.multipartFormData< 32 | Record 33 | > 34 | } 35 | 36 | const DEFAULT_CONTEXT = { 37 | // eslint-disable-next-line @typescript-eslint/unbound-method 38 | json: EdgeSpecResponse.json, 39 | // eslint-disable-next-line @typescript-eslint/unbound-method 40 | multipartFormData: EdgeSpecResponse.multipartFormData, 41 | // eslint-disable-next-line @typescript-eslint/unbound-method 42 | custom: EdgeSpecResponse.custom, 43 | } as const 44 | 45 | export const getDefaultContext = () => ({ ...DEFAULT_CONTEXT }) 46 | -------------------------------------------------------------------------------- /src/lib/edgespec/types/global-spec.ts: -------------------------------------------------------------------------------- 1 | import type { z } from "zod" 2 | import type { Middleware } from "../middleware/types.js" 3 | import type { InferRecordKey } from "./util.js" 4 | import type { SecuritySchemeObject } from "openapi3-ts/oas31" 5 | 6 | export type QueryArrayFormat = "brackets" | "comma" | "repeat" 7 | export type QueryArrayFormats = readonly QueryArrayFormat[] 8 | 9 | export type GlobalSpec = { 10 | authMiddleware: Record> 11 | beforeAuthMiddleware?: ReadonlyArray> 12 | afterAuthMiddleware?: ReadonlyArray> 13 | 14 | openapi?: { 15 | apiName?: string 16 | productionServerUrl?: string 17 | securitySchemas?: Record 18 | readonly globalSchemas?: Record 19 | } 20 | 21 | shouldValidateResponses?: boolean 22 | supportedArrayFormats?: QueryArrayFormats 23 | passErrors?: boolean 24 | 25 | /** 26 | * If an endpoint accepts multiple auth methods and they all fail, this hook will be called with the errors thrown by the middlewares. 27 | * You can inspect the errors and throw a more generic error in this hook if you want. 28 | */ 29 | onMultipleAuthMiddlewareFailures?: (errors: unknown[]) => void 30 | } 31 | 32 | export type GetAuthMiddlewaresFromGlobalSpec = 33 | InferRecordKey 34 | -------------------------------------------------------------------------------- /src/lib/edgespec/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | EdgeSpecRequest, 3 | EdgeSpecRequestOptions, 4 | HTTPMethods, 5 | EdgeSpecResponse, 6 | EdgeSpecJsonResponse, 7 | EdgeSpecMultiPartFormDataResponse, 8 | EdgeSpecCustomResponse, 9 | MiddlewareResponseData, 10 | SerializableToResponse, 11 | EdgeSpecRouteFn, 12 | EdgeSpecRouteParams, 13 | } from "./web-handler.js" 14 | 15 | export type { 16 | EdgeSpecAdapter, 17 | EdgeSpecOptions, 18 | EdgeSpecRouteBundle, 19 | } from "./edge-spec.js" 20 | 21 | export type { 22 | GetAuthMiddlewaresFromGlobalSpec, 23 | GlobalSpec, 24 | QueryArrayFormat, 25 | QueryArrayFormats, 26 | } from "./global-spec.js" 27 | 28 | export type { 29 | RouteSpec, 30 | CreateWithRouteSpecFn, 31 | EdgeSpecRouteFnFromSpecs, 32 | } from "./route-spec.js" 33 | 34 | export type { Middleware } from "../middleware/types.js" 35 | -------------------------------------------------------------------------------- /src/lib/edgespec/types/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This just pulls the "`I`" from a `Record` 3 | * 4 | * Had to create this utility since `keyof Record` doesn't seem to give I :/ 5 | */ 6 | export type InferRecordKey> = 7 | R extends Record ? K : never 8 | 9 | /** 10 | * Executes an "array map" function on a tuple through a record 11 | * 12 | * For example: 13 | * 14 | * ```ts 15 | * Map = { [name in "arthur"|"jane"|"john"]: `hi ${name}!` } 16 | * Arr = [ "jane", "arthur" ] 17 | * 18 | * -> 19 | * 20 | * [ "hi jane!", "hi arthur!" ] 21 | * ``` 22 | */ 23 | export type MapArray< 24 | Map extends Record, 25 | Arr extends ReadonlyArray, 26 | > = Arr extends readonly [ 27 | infer K extends keyof Map, 28 | ...infer Remaining extends Array, 29 | ] 30 | ? readonly [Map[K], ...MapArray] 31 | : readonly [] 32 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxsdev/LocalSandbox/4899dc13f423847e3f0991f09486e0f70df69ffe/src/lib/index.ts -------------------------------------------------------------------------------- /src/lib/integration/azure/service-bus/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./namespace.js" 2 | export * from "./queue.js" 3 | export * from "./topic.js" 4 | export * from "./subscription.js" 5 | -------------------------------------------------------------------------------- /src/lib/integration/azure/susbcriptions.ts: -------------------------------------------------------------------------------- 1 | import { azure_routes } from "./routes.js" 2 | import subscriptionRoutes from "generated/azure-rest-api-specs/resources/resource-manager/Microsoft.Resources/stable/2016-06-01/subscriptions.js" 3 | import { extractRoute } from "../../openapi/extract-route.js" 4 | 5 | azure_routes.implementRoute( 6 | ...extractRoute(subscriptionRoutes, "/subscriptions", "GET"), 7 | async (_, ctx) => { 8 | return ctx.json({ 9 | value: [ctx.subscription], 10 | // TODO: maybe don't do this?? 11 | nextLink: "", 12 | }) 13 | }, 14 | ) 15 | -------------------------------------------------------------------------------- /src/lib/integration/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./azure/index.js" 2 | -------------------------------------------------------------------------------- /src/lib/logger/get-test-logger.ts: -------------------------------------------------------------------------------- 1 | import { PassThrough } from "stream" 2 | import { getLogger } from "lib/logger/index.js" 3 | 4 | export const getTestLogger = (app?: string) => { 5 | const stream = new PassThrough() 6 | const logger = getLogger({ 7 | stream, 8 | level: "debug", 9 | app, 10 | }) 11 | 12 | stream.on("data", (chunk) => { 13 | console.log((chunk.toString() as string).slice(0, -1)) 14 | }) 15 | 16 | return { 17 | logger, 18 | cleanup: async () => { 19 | stream.destroy() 20 | stream.removeAllListeners() 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/logger/index.ts: -------------------------------------------------------------------------------- 1 | import pino, { type DestinationStream } from "pino" 2 | import pretty from "pino-pretty" 3 | const { default: createLogger } = pino 4 | 5 | interface LoggerOptions { 6 | app?: string 7 | stream?: NodeJS.WritableStream 8 | level?: string 9 | } 10 | 11 | export const getLogger = ({ 12 | app, 13 | stream, 14 | level = "info", 15 | }: LoggerOptions = {}) => { 16 | const pretty_stream = pretty({ 17 | // sync: true, 18 | // colorize: colorette.isColorSupported, // --colorize 19 | colorize: true, 20 | colorizeObjects: true, // --colorizeObjects 21 | // crlf: false, // --crlf 22 | errorLikeObjectKeys: ["err", "error"], // --errorLikeObjectKeys (not required to match custom errorKey with pino >=8.21.0) 23 | 24 | // messageFormat: app ? `[${app}] {msg}` : false, // --messageFormat, 25 | // errorProps: "", // --errorProps 26 | // levelFirst: false, // --levelFirst 27 | // messageKey: "msg", // --messageKey (not required with pino >=8.21.0) 28 | // levelKey: "level", // --levelKey 29 | // messageFormat: false, // --messageFormat 30 | // timestampKey: "time", // --timestampKey 31 | // translateTime: false, // --translateTime 32 | ignore: "pid,hostname", // --ignore 33 | // include: "level,time", // --include 34 | // hideObject: false, // --hideObject 35 | // singleLine: false, // --singleLine 36 | // customColors: "err:red,info:blue", // --customColors 37 | // customLevels: "err:99,info:1", // --customLevels (not required with pino >=8.21.0) 38 | // levelLabel: "levelLabel", // --levelLabel 39 | // minimumLevel: "info", // --minimumLevel 40 | // useOnlyCustomProps: true, // --useOnlyCustomProps 41 | // // The file or file descriptor (1 is stdout) to write to 42 | // destination: 1, 43 | 44 | // // Alternatively, pass a `sonic-boom` instance (allowing more flexibility): 45 | // // destination: new SonicBoom({ dest: 'a/file', mkdir: true }) 46 | 47 | // // You can also configure some SonicBoom options directly 48 | sync: true, // by default we write asynchronously 49 | // append: true, // the file is opened with the 'a' flag 50 | // mkdir: true, // create the target destination 51 | 52 | destination: stream, 53 | 54 | // customPrettifiers: {}, 55 | }) 56 | 57 | const final_stream: DestinationStream = pretty_stream 58 | 59 | // if (stream) { 60 | // final_stream = pretty_stream.pipe(stream) 61 | // } 62 | 63 | return createLogger( 64 | { 65 | name: app, 66 | level, 67 | }, 68 | final_stream, 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/logger/with-logger.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware } from "edgespec" 2 | import type { Logger } from "pino" 3 | 4 | export const withLogger = 5 | (logger: Logger): Middleware<{}, { logger: Logger }> => 6 | async (req, ctx, next) => { 7 | ctx.logger = logger 8 | return await next(req, ctx) 9 | } 10 | 11 | export const withExternallyPopulatedLogger: Middleware< 12 | {}, 13 | { logger: Logger } 14 | > = async (req, ctx, next) => { 15 | if (!ctx.logger) { 16 | throw new Error("Could not find logger in API route context!") 17 | } 18 | return await next(req, ctx) 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/openapi/extract-route.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/consistent-type-assertions */ 2 | import type { HTTPMethods } from "edgespec" 3 | 4 | export const extractRoute = < 5 | const R extends Readonly< 6 | Record> 7 | >, 8 | const K extends keyof R, 9 | const M extends R[K][number]["methods"][number], 10 | >( 11 | routes: R, 12 | key: K, 13 | method: M, 14 | ) => 15 | [ 16 | key, 17 | { 18 | ...routes[key].find((val) => val.methods.includes(method)), 19 | auth: "bearer", 20 | } as Extract & { auth: "bearer" }, 21 | ] as const 22 | -------------------------------------------------------------------------------- /src/lib/server/check-config.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from "pino" 2 | import type { ServerEnv } from "./env.js" 3 | import detectPort from "detect-port" 4 | 5 | export const checkConfig = async ({ 6 | env, 7 | logger, 8 | }: { 9 | env: ServerEnv 10 | logger?: Pick 11 | }) => { 12 | logger ??= console 13 | 14 | if ((await detectPort(env.LOCALSANDBOX_PORT)) !== env.LOCALSANDBOX_PORT) { 15 | logger.error(`Port ${env.LOCALSANDBOX_PORT} is already in use`) 16 | return false 17 | } 18 | 19 | if ( 20 | (await detectPort(env.LOCALSANDBOX_AMQP_PORT)) !== 21 | env.LOCALSANDBOX_AMQP_PORT 22 | ) { 23 | logger.error(`Port ${env.LOCALSANDBOX_AMQP_PORT} is already in use`) 24 | return false 25 | } 26 | 27 | return true 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/server/env.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import { z } from "zod" 3 | 4 | export const DEFAULT_LOCALSANDBOX_PORT = 7329 5 | export const DEFAULT_LOCALSANDBOX_AMQP_PORT = 5672 6 | 7 | export const DEFAULT_RESOURCE_NAME = "default" as const 8 | 9 | const resource_name = z.union([ 10 | z.enum(["disabled", "false"]).transform(() => undefined), 11 | z.string().optional().default(DEFAULT_RESOURCE_NAME), 12 | ]) 13 | 14 | const envSchema = z.object({ 15 | LOG_LEVEL: z.string().default("info"), 16 | LOCALSANDBOX_PORT: z.coerce.number().default(7329), 17 | LOCALSANDBOX_AMQP_PORT: z.coerce.number().default(5672), 18 | 19 | LOCALSANDBOX_DEFAULT_LOCATION: z.string().optional().default("westus2"), 20 | 21 | LOCALSANDBOX_DEFAULT_SUBSCRIPTION_ID: resource_name, 22 | LOCALSANDBOX_DEFAULT_RESOURCES: z.coerce 23 | .string() 24 | .optional() 25 | .default("true") 26 | .transform((v) => v.toLowerCase() === "true" || v === "1"), 27 | LOCALSANDBOX_DEFAULT_RESOURCE_GROUP: resource_name, 28 | LOCALSANDBOX_DEFAULT_NAMESPACE: resource_name, 29 | LOCALSANDBOX_DEFAULT_QUEUE: resource_name, 30 | 31 | LOCALSANDBOX_HTTPS: z.coerce.boolean().optional().default(true), 32 | LOCALSANDBOX_CERT_RETRIEVAL_URL: z 33 | .string() 34 | .url() 35 | .optional() 36 | .default("https://cert.localsandbox.sh"), 37 | LOCALSANDBOX_CERT_CACHE_EXPIRATION: z 38 | .string() 39 | .duration() 40 | .optional() 41 | .default(Temporal.Duration.from({ days: 1 }).toString()), 42 | }) 43 | 44 | export type ServerEnv = z.output 45 | 46 | export const getServerEnv = ( 47 | overrides: Partial> = {}, 48 | ) => envSchema.parse({ ...process.env, ...overrides }) 49 | -------------------------------------------------------------------------------- /src/lib/tls/index.ts: -------------------------------------------------------------------------------- 1 | export const TEST_CERT = ` 2 | -----BEGIN CERTIFICATE----- 3 | MIIDfzCCAmegAwIBAgIEGwGErjANBgkqhkiG9w0BAQsFADBbMScwJQYDVQQDDB5SZWdlcnkgU2Vs 4 | Zi1TaWduZWQgQ2VydGlmaWNhdGUxIzAhBgNVBAoMGlJlZ2VyeSwgaHR0cHM6Ly9yZWdlcnkuY29t 5 | MQswCQYDVQQGEwJVQTAgFw0yNDA4MjMwMDAwMDBaGA8yMTI0MDgyMzAwNTAxOVowQTENMAsGA1UE 6 | AwwEdGVzdDEjMCEGA1UECgwaUmVnZXJ5LCBodHRwczovL3JlZ2VyeS5jb20xCzAJBgNVBAYTAlVB 7 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9vHYbSZfFE6Cbgf0vwp5raxQYA+oQban 8 | V/rwHweKUWYfcNyqc4pbDWcCG6qfpcIn4DeeNeGsR9dL26TVuJqQf8UP9K7+UkrBTFupv2I47pWz 9 | +TFh5A71Z3vjBycZRAZ6HLKnIPWPinsC0qlv2x+VlDGujro5JeU5p8fQW2a2Sdbi80rM7J1qNxlf 10 | ajA1FCiiRVbs1TvkPtj5N2BAGcDFCnD99to2++TSzXk5PflwliUnZRWo1jb55rN9+j3BCtqPYjV2 11 | mkFpCOkK3cX8RADFfOg8R/GuYRHxXbIjaWCDPKyr76ExTGjm76qfRvoWJqEtMgTLAfkdl4ntVyuW 12 | BR/TBQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU 13 | 78b032SnKNcyz8GRKi5JOqEtZ78wHwYDVR0jBBgwFoAU78b032SnKNcyz8GRKi5JOqEtZ78wDQYJ 14 | KoZIhvcNAQELBQADggEBADBLDjnKu38vgQ+jaqS1hcTiv4Qcp+WJnytXhgoJyUTh9RUCm9RPT0dV 15 | eyml+OKj/GJzyFAF/4cCpEIvixyxLC97NpQfm1aR7el3jccrku+bo8WOjzxn2lVGf29NJ+QL5kBj 16 | O511ragZj54OMtPo1ZENl5hTaN1WvAC/aCT+c0VPsURZsbDU1ODqohjrWP4kkCBL/JI3kKSjBIqP 17 | Gy22da9o3r/xSfjAD9HQAFHuutEWT0rCeyCtCf7Zg355n9QiD33TVHu+ehcJEQ4NLbMmVFqDhvkz 18 | VkSnrT6C9q6OjvJvJ1f3yg6zmq0zmXOBZSp81pf6HneOxzCD623NQRuOZLA= 19 | -----END CERTIFICATE----- 20 | `.trim() 21 | 22 | export const TEST_PK = ` 23 | -----BEGIN RSA PRIVATE KEY----- 24 | MIIEowIBAAKCAQEA9vHYbSZfFE6Cbgf0vwp5raxQYA+oQbanV/rwHweKUWYfcNyq 25 | c4pbDWcCG6qfpcIn4DeeNeGsR9dL26TVuJqQf8UP9K7+UkrBTFupv2I47pWz+TFh 26 | 5A71Z3vjBycZRAZ6HLKnIPWPinsC0qlv2x+VlDGujro5JeU5p8fQW2a2Sdbi80rM 27 | 7J1qNxlfajA1FCiiRVbs1TvkPtj5N2BAGcDFCnD99to2++TSzXk5PflwliUnZRWo 28 | 1jb55rN9+j3BCtqPYjV2mkFpCOkK3cX8RADFfOg8R/GuYRHxXbIjaWCDPKyr76Ex 29 | TGjm76qfRvoWJqEtMgTLAfkdl4ntVyuWBR/TBQIDAQABAoIBABojR5HoB5TN6YrN 30 | b0egS3hJPpmoVpocA/LxReS25tpOSaInzSfdG12cC1JT2UGRfyiBoo6X9CUHggk9 31 | 1XxMaeKIQHPY6OTbckHLivhNpHKGaG4GHtMljS6Wo5VQe2FioR6z/zIjI74X3pjf 32 | I86I9YthxdToG5/p9xQN930RLla6wYErF0qkRuAv+H0jx72YI4yJHZCOqywjt2y3 33 | Qa9PkSR8ANj2G1g8Kq5IKncP3Mok8r54I/7mJwilp7irRZj24cHRDys6Bmz9QIDx 34 | GPS+2QthxwOLzWqnOx7pb+HiND+/vJwk1Y9bjLF3IyvuO39wCyQanftFH6dBvOG2 35 | H+P2v7ECgYEA/PkCXG/Fzrx9gvKCCI430w6fMxMuOf9jz6zd+/nt517KrrLiioqW 36 | WgLAT7iUIB/P799jKNUW7hpnC4YhJT/qDNREhh2spzHLmJAa8csWM26J3x/5QqkW 37 | qXLyc11IfwfH4UEX5OhB3FCGrPj5ORn8UmZKKroMeDv9V1MTeu1+w9ECgYEA+eZe 38 | h0lK8/NUlQZYQShdEIwjH9b9ANMZiyN2tZX+OxpB6CePxmk5VBUn6bmRMx9iqeJj 39 | XjUQN2Peh91pL4OokKBYVDiFdFlFotzymBKttCN3YUlpurl2J6XKNDXUfnloffX+ 40 | 15k+0MqHQwEDmzbJrs8YpbqE+UPqZKOdhcDerPUCgYAdYOHIUGa9gqBk47r8OV/8 41 | T9dnPBQDQkiaJq5FBBp/4z9QmI+8nSmm3GjvGTWCoY8pgVznsg+OqVxMN1CEHe8V 42 | fFVU6f9SD3NgjWPDrt0uLekvE2yENFTgauwDP9MahZHN9BxNRjfX2TY6wlNXMVBf 43 | VWfJnH+0OutKB+jcPtaY8QKBgCvaxqn9Lb8j66r/YwuENtjJjvxucRXs9eWaAqIZ 44 | QXVDxV8lWjDalGnyEIAOxbFwB5OCnCeTLlZaG1pCe8wP0cwXp4iYJqtlYzgSiCwx 45 | 0vPy6WdUR86x709D4/lHnRPY4IKCYgeZ6BEiCZyzl9tsQPaBd3TWB7Hqvj6NC/7F 46 | +w3lAoGBANEjfIo36UkPx+SGB6CulkKA9dCmNLQ2M3dOHBUuId04vn/YHrUJxH/w 47 | DdZqRg9IkpehFG8eUW6taG/UmIe4JObWrUBfEH4kv3VaV5tT41q/76LcEnS0USVi 48 | dS/vQnmDWJw3sB3m2r/iE1P8Avr+XlaLBGRARq5K+l09vb4fOjFK 49 | -----END RSA PRIVATE KEY----- 50 | `.trim() 51 | -------------------------------------------------------------------------------- /src/lib/util/bearer-token.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | export const bearerToken = z.string().transform((str) => { 4 | const matches = str.match(/^Bearer\s+(.+)$/) 5 | if (matches) { 6 | return matches[1] // Extract the token part 7 | } 8 | throw new Error("Invalid Bearer token format") 9 | }) 10 | -------------------------------------------------------------------------------- /src/lib/util/is-errno-exception.ts: -------------------------------------------------------------------------------- 1 | export function isErrnoException( 2 | error: unknown, 3 | ): error is NodeJS.ErrnoException { 4 | return ( 5 | isArbitraryObject(error) && 6 | error instanceof Error && 7 | (typeof error["errno"] === "number" || 8 | typeof error["errno"] === "undefined") && 9 | typeof error["code"] === "string" && 10 | (typeof error["path"] === "string" || 11 | typeof error["path"] === "undefined") && 12 | (typeof error["syscall"] === "string" || 13 | typeof error["syscall"] === "undefined") 14 | ) 15 | } 16 | 17 | export function isAddressInUseException( 18 | error: NodeJS.ErrnoException, 19 | ): error is NodeJS.ErrnoException & { port: number } { 20 | return error.code === "EADDRINUSE" && "port" in error 21 | } 22 | 23 | type ArbitraryObject = Record 24 | 25 | function isArbitraryObject( 26 | potentialObject: unknown, 27 | ): potentialObject is ArbitraryObject { 28 | return typeof potentialObject === "object" && potentialObject !== null 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/util/long.ts: -------------------------------------------------------------------------------- 1 | import rhea from "rhea" 2 | import Long from "long" 3 | import { z } from "zod" 4 | 5 | export const serializedLong = z.union([ 6 | z.number().transform((v) => Long.fromNumber(v)), 7 | z 8 | .number() 9 | .array() 10 | .transform((v) => Long.fromBytesBE(v)), 11 | z.instanceof(Buffer).transform((v) => Long.fromBytesBE(v as any)), 12 | z 13 | .instanceof(rhea.Typed) 14 | .transform((v) => 15 | Buffer.isBuffer(v.value) 16 | ? Long.fromBytesBE(v.value as any) 17 | : Long.fromNumber(v.value), 18 | ), 19 | ]) 20 | 21 | // const unserializedLongToArrayLike = z.instanceof(Long).transform((v) => { 22 | // if (v.getHighBits()) { 23 | // return v.toBytesBE() 24 | // } else { 25 | // // rhea.types.wrap_long 26 | // return v.toNumber() 27 | // } 28 | // }) 29 | 30 | // const unserializedLongToBufferLike = z.instanceof(Long).transform((v) => { 31 | // if (v.getHighBits()) { 32 | // return Buffer.from(v.toBytesBE()) 33 | // } else { 34 | // // rhea.types.wrap_long 35 | // return v.toNumber() 36 | // } 37 | // }) 38 | 39 | export const unserializedLongToRheaParsable = z 40 | .instanceof(Long) 41 | .transform((v) => rhea.types.wrap_long(Buffer.from(v.toBytesBE()))) 42 | 43 | export type RheaEncodedLong = z.output 44 | -------------------------------------------------------------------------------- /src/lib/util/service-bus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Reverses the operation of sdk/servicebus/service-bus/src/util/utils.ts 3 | */ 4 | export const unreorderLockToken = (lockTokenBytes: Buffer) => { 5 | return Buffer.from([ 6 | lockTokenBytes[3] ?? 0, 7 | lockTokenBytes[2] ?? 0, 8 | lockTokenBytes[1] ?? 0, 9 | lockTokenBytes[0] ?? 0, 10 | 11 | lockTokenBytes[5] ?? 0, 12 | lockTokenBytes[4] ?? 0, 13 | 14 | lockTokenBytes[7] ?? 0, 15 | lockTokenBytes[6] ?? 0, 16 | 17 | lockTokenBytes[8] ?? 0, 18 | lockTokenBytes[9] ?? 0, 19 | 20 | lockTokenBytes[10] ?? 0, 21 | lockTokenBytes[11] ?? 0, 22 | lockTokenBytes[12] ?? 0, 23 | lockTokenBytes[13] ?? 0, 24 | lockTokenBytes[14] ?? 0, 25 | lockTokenBytes[15] ?? 0, 26 | ]) 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/util/timeout.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | const Timeout = setTimeout(function () {}, 0).constructor 4 | 5 | export const zodTimeout = z 6 | .instanceof(Timeout as any) 7 | .transform((v) => v as NodeJS.Timeout) 8 | -------------------------------------------------------------------------------- /src/lib/util/uuid.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod" 2 | 3 | export const uuidToString = z.union([ 4 | z.string().uuid(), 5 | z.instanceof(Buffer).transform((buf) => { 6 | const buf_uuid = Buffer.alloc(16) 7 | buf.copy(buf_uuid) 8 | return [ 9 | buf_uuid.subarray(0, 4), 10 | buf_uuid.subarray(4, 6), 11 | buf_uuid.subarray(6, 8), 12 | buf_uuid.subarray(8, 10), 13 | buf_uuid.subarray(10, 16), 14 | ] 15 | .map((b) => b.toString("hex")) 16 | .join("-") 17 | }), 18 | ]) 19 | -------------------------------------------------------------------------------- /test/azure/model.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "vitest" 2 | import { createModelSpecs, getStore } from "lib/integration/integration.js" 3 | import { z } from "zod" 4 | import { randomUUID } from "node:crypto" 5 | 6 | test("one-to-many", async ({ expect }) => { 7 | const store = getStore( 8 | createModelSpecs({ 9 | user: { 10 | primaryKey: "id", 11 | schema: z.object({ 12 | id: z 13 | .string() 14 | .optional() 15 | .transform((val) => val ?? randomUUID()), 16 | }), 17 | hasOne: ["subscription"], 18 | }, 19 | subscription: { 20 | primaryKey: "subscriptionId", 21 | schema: z.object({ 22 | authorizationSource: z.string(), 23 | subscriptionId: z.string(), 24 | displayName: z.string(), 25 | state: z.string(), 26 | }), 27 | }, 28 | }), 29 | ) 30 | 31 | const subscription = store.subscription 32 | .insert() 33 | .values({ 34 | authorizationSource: "", 35 | displayName: "", 36 | state: "", 37 | subscriptionId: "1234", 38 | }) 39 | .executeTakeFirstOrThrow() 40 | 41 | expect(subscription.users()).toHaveLength(0) 42 | 43 | const user = store.user 44 | .insert() 45 | .values({ 46 | subscription_id: subscription.subscriptionId, 47 | }) 48 | .executeTakeFirstOrThrow() 49 | 50 | expect(user.subscription().subscriptionId).toBe(subscription.subscriptionId) 51 | expect(subscription.users()).toHaveLength(1) 52 | expect(subscription.users()[0]?.id).toBe(user.id) 53 | }) 54 | -------------------------------------------------------------------------------- /test/azure/namespace.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | import all from "it-all" 3 | 4 | fixturedTest("list azure namespaces", async ({ azure, expect }) => { 5 | const rg = await azure.rg() 6 | 7 | await expect( 8 | azure.sb_management_client.namespaces.beginCreateOrUpdateAndWait( 9 | rg.name!, 10 | "test", 11 | { location: azure.location }, 12 | ), 13 | ).resolves.toBeTruthy() 14 | 15 | await expect( 16 | all(azure.sb_management_client.namespaces.listByResourceGroup(rg.name!)), 17 | ).resolves.toHaveLength(1) 18 | }) 19 | -------------------------------------------------------------------------------- /test/azure/resource_group.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "../fixtured-test.js" 2 | import type { ResourceGroup } from "@azure/arm-resources" 3 | 4 | fixturedTest("can create resource group", async ({ azure, expect }) => { 5 | const resource_group = 6 | await azure.resource_client.resourceGroups.createOrUpdate("rg", { 7 | location: "westus2", 8 | }) 9 | 10 | expect(resource_group).toMatchObject({ 11 | location: "westus2", 12 | }) 13 | expect(resource_group.id).toBeTruthy() 14 | }) 15 | 16 | fixturedTest("can upsert resource group", async ({ azure, expect }) => { 17 | await azure.resource_client.resourceGroups.createOrUpdate("rg", { 18 | location: "westus2", 19 | }) 20 | 21 | const resource_group2 = 22 | await azure.resource_client.resourceGroups.createOrUpdate("rg", { 23 | location: "westus3", 24 | }) 25 | 26 | expect(resource_group2).toMatchObject({ 27 | location: "westus3", 28 | }) 29 | }) 30 | 31 | fixturedTest("can update resource group", async ({ azure, expect }) => { 32 | await azure.resource_client.resourceGroups.createOrUpdate("rg", { 33 | location: "westus2", 34 | }) 35 | 36 | const resource_group2 = await azure.resource_client.resourceGroups.update( 37 | "rg", 38 | { 39 | tags: { 40 | hello: "world", 41 | }, 42 | }, 43 | ) 44 | 45 | expect(resource_group2).toMatchObject({ 46 | location: "westus2", 47 | tags: { hello: "world" }, 48 | }) 49 | }) 50 | 51 | fixturedTest("can get a resource group", async ({ azure, expect }) => { 52 | await azure.resource_client.resourceGroups.createOrUpdate("rg", { 53 | location: "westus2", 54 | }) 55 | 56 | const resource_group = await azure.resource_client.resourceGroups.get("rg") 57 | 58 | expect(resource_group).toMatchObject({ 59 | location: "westus2", 60 | }) 61 | }) 62 | 63 | fixturedTest("can check a resource's existence", async ({ azure, expect }) => { 64 | { 65 | const { body: exists } = 66 | await azure.resource_client.resourceGroups.checkExistence("rg") 67 | expect(exists).toBeFalsy() 68 | } 69 | 70 | await azure.resource_client.resourceGroups.createOrUpdate("rg", { 71 | location: "westus2", 72 | }) 73 | 74 | { 75 | const { body: exists } = 76 | await azure.resource_client.resourceGroups.checkExistence("rg") 77 | expect(exists).toBeTruthy() 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /test/azure/service_bus.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "../fixtured-test.js" 2 | 3 | fixturedTest("can create namespace", async ({ azure, azure_rg, expect }) => { 4 | const namespace = 5 | await azure.sb_management_client.namespaces.beginCreateOrUpdateAndWait( 6 | azure_rg.name!, 7 | "namespace", 8 | { 9 | location: azure.location, 10 | }, 11 | ) 12 | 13 | expect(namespace.id).toBeTruthy() 14 | }) 15 | 16 | fixturedTest( 17 | "can create queue", 18 | async ({ azure, azure_sb_namespace, azure_rg, expect }) => { 19 | const queue = await azure.sb_management_client.queues.createOrUpdate( 20 | azure_rg.name!, 21 | azure_sb_namespace.name!, 22 | "queue", 23 | { 24 | requiresDuplicateDetection: true, 25 | }, 26 | ) 27 | 28 | expect(queue.id).toBeTruthy() 29 | expect(queue.location).toBe(azure.location) 30 | expect(queue.name).toBe("queue") 31 | expect(queue.maxDeliveryCount).toBe(10) 32 | expect(queue.requiresDuplicateDetection).toBe(true) 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /test/azure/subscription.test.ts: -------------------------------------------------------------------------------- 1 | import { type Subscription, SubscriptionClient } from "@azure/arm-subscriptions" 2 | import { fixturedTest } from "../fixtured-test.js" 3 | import toArray from "it-all" 4 | import { 5 | DEFAULT_SUBSCRIPTION_AUTHORIZATION_SOURCE, 6 | DEFAULT_SUBSCRIPTION_DISPLAY_NAME, 7 | } from "lib/integration/azure/routes.js" 8 | 9 | fixturedTest("has basic subscription", async ({ azure, expect }) => { 10 | const subscriptions = await toArray( 11 | azure.subscription_client.subscriptions.list(), 12 | ) 13 | 14 | expect(subscriptions).toStrictEqual([ 15 | { 16 | subscriptionId: azure.id, 17 | id: `/subscriptions/${azure.id}`, 18 | displayName: DEFAULT_SUBSCRIPTION_DISPLAY_NAME, 19 | authorizationSource: DEFAULT_SUBSCRIPTION_AUTHORIZATION_SOURCE, 20 | state: "Enabled", 21 | }, 22 | ]) 23 | }) 24 | 25 | fixturedTest("subscriptions isolated to account", async ({ azure, expect }) => { 26 | const alt_id = "1234" 27 | 28 | const alternate_client = new SubscriptionClient( 29 | { 30 | getToken: async () => { 31 | return { 32 | token: alt_id, 33 | expiresOnTimestamp: Infinity, 34 | } 35 | }, 36 | }, 37 | { 38 | ...azure.service_client_options, 39 | }, 40 | ) 41 | 42 | { 43 | const subscriptions = await toArray(alternate_client.subscriptions.list()) 44 | 45 | expect(subscriptions).toHaveLength(1) 46 | expect(subscriptions).toMatchObject([ 47 | { 48 | subscriptionId: alt_id, 49 | }, 50 | ]) 51 | } 52 | 53 | { 54 | const subscriptions = await toArray( 55 | azure.subscription_client.subscriptions.list(), 56 | ) 57 | 58 | expect(subscriptions).toStrictEqual([ 59 | { 60 | subscriptionId: azure.id, 61 | id: `/subscriptions/${azure.id}`, 62 | displayName: DEFAULT_SUBSCRIPTION_DISPLAY_NAME, 63 | authorizationSource: DEFAULT_SUBSCRIPTION_AUTHORIZATION_SOURCE, 64 | state: "Enabled", 65 | }, 66 | ]) 67 | } 68 | }) 69 | -------------------------------------------------------------------------------- /test/lib/broker/url.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from "vitest" 2 | import { parseBrokerURL } from "lib/broker/url.js" 3 | 4 | test("can parse queue or topic URL", ({ expect }) => { 5 | const url = new URL( 6 | "sb://subscription.rg.namespace.localhost.localsandbox.sh/queue-or-topic", 7 | ) 8 | expect(parseBrokerURL(url)).toMatchInlineSnapshot(` 9 | { 10 | "namespace_name": "namespace", 11 | "queue_or_topic_name": "queue-or-topic", 12 | "resource_group_name": "rg", 13 | "subscription_id": "subscription", 14 | } 15 | `) 16 | }) 17 | 18 | test("can parse subscription URL", ({ expect }) => { 19 | const url = new URL( 20 | "sb://subscription.rg.namespace.localhost.localsandbox.sh/queue-or-topic/Subscriptions/subscription", 21 | ) 22 | expect(parseBrokerURL(url)).toMatchInlineSnapshot(` 23 | { 24 | "namespace_name": "namespace", 25 | "queue_or_topic_name": "queue-or-topic", 26 | "resource_group_name": "rg", 27 | "subscription_id": "subscription", 28 | "subscription_name": "subscription", 29 | } 30 | `) 31 | }) 32 | 33 | test("can parse queue w/ deadletter URL", ({ expect }) => { 34 | const url = new URL( 35 | "sb://subscription.rg.namespace.localhost.localsandbox.sh/queue-or-topic/$DeadLetterQueue", 36 | ) 37 | expect(parseBrokerURL(url)).toMatchInlineSnapshot(` 38 | { 39 | "namespace_name": "namespace", 40 | "queue_or_topic_name": "queue-or-topic", 41 | "resource_group_name": "rg", 42 | "subqueue": "deadletter", 43 | "subscription_id": "subscription", 44 | } 45 | `) 46 | }) 47 | 48 | test("can parse queue w/ management url", ({ expect }) => { 49 | const url = new URL( 50 | "sb://subscription.rg.namespace.localhost.localsandbox.sh/queue-or-topic/$management", 51 | ) 52 | expect(parseBrokerURL(url)).toMatchInlineSnapshot(` 53 | { 54 | "internal": "$management", 55 | "namespace_name": "namespace", 56 | "queue_or_topic_name": "queue-or-topic", 57 | "resource_group_name": "rg", 58 | "subscription_id": "subscription", 59 | } 60 | `) 61 | }) 62 | 63 | test("can parse subscription w/ management url", ({ expect }) => { 64 | const url = new URL( 65 | "sb://subscription.rg.namespace.localhost.localsandbox.sh/topic/Subscriptions/subscription/$management", 66 | ) 67 | expect(parseBrokerURL(url)).toMatchInlineSnapshot(` 68 | { 69 | "internal": "$management", 70 | "namespace_name": "namespace", 71 | "queue_or_topic_name": "topic", 72 | "resource_group_name": "rg", 73 | "subscription_id": "subscription", 74 | "subscription_name": "subscription", 75 | } 76 | `) 77 | }) 78 | 79 | test("can parse subscription w/ DLQ", ({ expect }) => { 80 | const url = new URL( 81 | "sb://subscription.rg.namespace.localhost.localsandbox.sh/topic/Subscriptions/subscription/$DeadLetterQueue", 82 | ) 83 | expect(parseBrokerURL(url)).toMatchInlineSnapshot(` 84 | { 85 | "namespace_name": "namespace", 86 | "queue_or_topic_name": "topic", 87 | "resource_group_name": "rg", 88 | "subqueue": "deadletter", 89 | "subscription_id": "subscription", 90 | "subscription_name": "subscription", 91 | } 92 | `) 93 | }) 94 | -------------------------------------------------------------------------------- /test/lib/util/uuid.test.ts: -------------------------------------------------------------------------------- 1 | import { test, describe } from "vitest" 2 | import { uuidToString } from "lib/util/uuid.js" 3 | import { unreorderLockToken } from "lib/util/service-bus.js" 4 | 5 | describe("uuid", () => { 6 | test("can parse buffer to uuid string", ({ expect }) => { 7 | expect(uuidToString.parse(Buffer.from([48]))).toMatchInlineSnapshot( 8 | `"30000000-0000-0000-0000-000000000000"`, 9 | ) 10 | }) 11 | }) 12 | 13 | test("can parse reordered uuid", ({ expect }) => { 14 | expect( 15 | uuidToString.parse( 16 | unreorderLockToken( 17 | Buffer.from([0, 0, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), 18 | ), 19 | ), 20 | ).toMatchInlineSnapshot(`"30000000-0000-0000-0000-000000000000"`) 21 | }) 22 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/abandon.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "can abandon single message from queue", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createQueue } = azure_queue 7 | 8 | const queue = await createQueue({ maxDeliveryCount: 1 }) 9 | 10 | const sender = createSender(queue.name!) 11 | 12 | await sender.sendMessages({ 13 | body: "hello world!", 14 | }) 15 | 16 | const receiver = createReceiver(queue.name!, {}) 17 | 18 | { 19 | const [message] = await receiver.receiveMessages(1) 20 | 21 | expect(message!.body).toBe("hello world!") 22 | await receiver.abandonMessage(message!) 23 | } 24 | 25 | { 26 | const [message] = await receiver.receiveMessages(1, { 27 | maxWaitTimeInMs: 0, 28 | }) 29 | 30 | expect(message).toBeUndefined() 31 | } 32 | }, 33 | ) 34 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/accessed-at.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "accessedAt is updated on receiver open", 5 | async ({ azure_queue, expect }) => { 6 | const { createReceiver, createQueue, getQueue } = azure_queue 7 | 8 | const queue = await createQueue({}) 9 | expect(queue.accessedAt?.getTime()).toBeLessThan(0) 10 | 11 | const receiver = createReceiver(queue.name!) 12 | 13 | const receiveDate = new Date() 14 | await receiver.receiveMessages(1, { 15 | maxWaitTimeInMs: 0, 16 | }) 17 | 18 | const { accessedAt } = await getQueue(queue.name!) 19 | 20 | expect(accessedAt).toBeDefined() 21 | 22 | expect(accessedAt!.getTime()).toBeGreaterThan(receiveDate.getTime()) 23 | expect(accessedAt!.getTime()).toBeGreaterThan(queue.accessedAt!.getTime()) 24 | }, 25 | ) 26 | 27 | fixturedTest( 28 | "accessedAt is updated on message sent", 29 | async ({ azure_queue, expect, expectCorrelatedTime }) => { 30 | const { createSender, createQueue, getQueue } = azure_queue 31 | 32 | const queue = await createQueue({}) 33 | 34 | expect(queue.accessedAt?.getTime()).toBeLessThan(0) 35 | 36 | const sender = createSender(queue.name!) 37 | 38 | const sendDate = new Date() 39 | await sender.sendMessages({ 40 | body: "hello world!", 41 | }) 42 | 43 | const { accessedAt } = await getQueue(queue.name!) 44 | 45 | expect(accessedAt).toBeDefined() 46 | 47 | expect(accessedAt!.getTime()).toBeGreaterThan(sendDate.getTime()) 48 | expect(accessedAt!.getTime()).toBeGreaterThan(queue.accessedAt!.getTime()) 49 | }, 50 | ) 51 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/active-message-count.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("gives accurate messageCount", async ({ azure_queue, expect }) => { 4 | const { createSender, createReceiver, createQueue, getQueue } = azure_queue 5 | 6 | const queue = await createQueue({}) 7 | 8 | const sender = createSender(queue.name!) 9 | 10 | await sender.sendMessages({ 11 | body: "hello world!", 12 | }) 13 | 14 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 15 | countDetails: { 16 | activeMessageCount: 1, 17 | }, 18 | }) 19 | 20 | const receiver = createReceiver(queue.name!) 21 | 22 | const [message] = await receiver.receiveMessages(1) 23 | expect(message!.body).toBe("hello world!") 24 | 25 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 26 | countDetails: { 27 | activeMessageCount: 1, 28 | }, 29 | }) 30 | 31 | await receiver.completeMessage(message!) 32 | 33 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 34 | countDetails: { 35 | activeMessageCount: 0, 36 | }, 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/cancel-scheduled.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "can cancel a scheduled message from a topic", 5 | { 6 | timeout: 60000, 7 | }, 8 | async ({ azure_queue, env, expect }) => { 9 | const { createSender, createReceiver, createQueue, getQueue } = azure_queue 10 | 11 | const queue = await createQueue({}) 12 | 13 | const sender = createSender(queue.name!) 14 | 15 | const schedule_in_ms = env.TEST_AZURE_E2E ? 20000 : 500 16 | const schedule_at = new Date(Date.now() + schedule_in_ms) 17 | 18 | const msg = await sender.scheduleMessages( 19 | { 20 | body: "hello world!", 21 | }, 22 | schedule_at, 23 | ) 24 | 25 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 26 | countDetails: { 27 | scheduledMessageCount: 1, 28 | }, 29 | }) 30 | 31 | await sender.cancelScheduledMessages(msg) 32 | const receiver = createReceiver(queue.name!) 33 | 34 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 35 | countDetails: { 36 | scheduledMessageCount: 0, 37 | }, 38 | }) 39 | 40 | { 41 | const [message] = await receiver.peekMessages(1) 42 | expect(message).not.toBeDefined() 43 | } 44 | 45 | expect(Date.now() < schedule_at.getTime()).toBe(true) 46 | 47 | const [message] = await receiver.receiveMessages(1, { 48 | maxWaitTimeInMs: schedule_in_ms * 1.2, 49 | }) 50 | expect(message).toBeFalsy() 51 | 52 | expect(Date.now() >= schedule_at.getTime()).toBe(true) 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/complete.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "can complete single message from queue", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createQueue } = azure_queue 7 | 8 | const queue = await createQueue({}) 9 | 10 | const sender = createSender(queue.name!) 11 | await sender.sendMessages({ 12 | body: "hello world!", 13 | }) 14 | 15 | const receiver = createReceiver(queue.name!) 16 | 17 | const [message] = await receiver.receiveMessages(1) 18 | expect(message!.lockToken).toBeDefined() 19 | expect(message!.body).toBe("hello world!") 20 | expect(message!.sequenceNumber!.toNumber()).toBe(1) 21 | 22 | await receiver.completeMessage(message!) 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/dead-letter-message-expiration.test.ts: -------------------------------------------------------------------------------- 1 | import delay from "delay" 2 | import { fixturedTest } from "test/fixtured-test.js" 3 | import { BrokerConstants } from "lib/broker/constants.js" 4 | 5 | fixturedTest( 6 | "dead letter expired messages with deadLetteringOnMessageExpiration", 7 | async ({ azure_queue, expect }) => { 8 | const { createSender, createReceiver, createQueue, getQueue } = azure_queue 9 | 10 | const dlq = await createQueue({}) 11 | 12 | const queue = await createQueue({ 13 | deadLetteringOnMessageExpiration: true, 14 | forwardDeadLetteredMessagesTo: dlq.name!, 15 | }) 16 | 17 | const sender = createSender(queue.name!) 18 | 19 | const ttlMs = 200 20 | 21 | await sender.sendMessages({ 22 | body: "hello world!", 23 | timeToLive: ttlMs, 24 | }) 25 | 26 | await delay(ttlMs) 27 | 28 | { 29 | const receiver = createReceiver(queue.name!) 30 | 31 | const messages = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 32 | expect(messages).toHaveLength(0) 33 | } 34 | 35 | { 36 | const receiver = createReceiver(dlq.name!) 37 | 38 | const [message] = await receiver.receiveMessages(1, { 39 | maxWaitTimeInMs: 5000, 40 | }) 41 | expect(message?.deadLetterSource).toBe(queue.name) 42 | expect(message?.deadLetterReason).toBe( 43 | BrokerConstants.errors.messageExpired.reason, 44 | ) 45 | expect(message?.deadLetterErrorDescription).toBe( 46 | BrokerConstants.errors.messageExpired.description, 47 | ) 48 | expect(message).toBeTruthy() 49 | 50 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 51 | countDetails: { 52 | transferDeadLetterMessageCount: 0, 53 | }, 54 | }) 55 | expect(message?.sequenceNumber).toBeDefined() 56 | expect(message?.enqueuedSequenceNumber).toBeUndefined() 57 | } 58 | }, 59 | ) 60 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/dead-letter.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "can manually dead letter message", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createQueue, getQueue } = azure_queue 7 | 8 | const dlq = await createQueue({}) 9 | const queue = await createQueue({ 10 | forwardDeadLetteredMessagesTo: dlq.name!, 11 | }) 12 | 13 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 14 | countDetails: { 15 | transferDeadLetterMessageCount: 0, 16 | }, 17 | }) 18 | 19 | const sender = createSender(queue.name!) 20 | 21 | await sender.sendMessages({ 22 | body: "hello world!", 23 | }) 24 | 25 | const receiver = createReceiver(queue.name!) 26 | 27 | const [message] = await receiver.receiveMessages(1) 28 | expect(message!.body).toBe("hello world!") 29 | expect(message?.sequenceNumber).toBeDefined() 30 | 31 | await receiver.deadLetterMessage(message!, { 32 | deadLetterReason: "dead letter reason", 33 | deadLetterErrorDescription: "dead letter error description", 34 | }) 35 | 36 | const receiver_dlq = createReceiver(dlq.name!) 37 | 38 | const [dead_lettered_message] = await receiver_dlq.receiveMessages(1) 39 | expect(dead_lettered_message!.body).toBe("hello world!") 40 | 41 | await receiver_dlq.completeMessage(dead_lettered_message!) 42 | 43 | expect(dead_lettered_message?.deadLetterSource).toBe(queue.name!) 44 | expect(dead_lettered_message?.deadLetterReason).toBe("dead letter reason") 45 | expect(dead_lettered_message?.deadLetterErrorDescription).toBe( 46 | "dead letter error description", 47 | ) 48 | 49 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 50 | countDetails: { 51 | transferDeadLetterMessageCount: 0, 52 | }, 53 | }) 54 | 55 | expect(dead_lettered_message?.sequenceNumber).toBeDefined() 56 | expect(dead_lettered_message?.enqueuedSequenceNumber).toBeUndefined() 57 | expect(dead_lettered_message?.sequenceNumber).toStrictEqual( 58 | message?.sequenceNumber, 59 | ) 60 | 61 | // TODO: ensure dead letter options are present 62 | }, 63 | ) 64 | 65 | fixturedTest.todo("fails properly when trying to dead letter to same queue") 66 | fixturedTest.todo("cannot dead-letter when dlq is not set") 67 | fixturedTest.todo("dead-letters automatically once max attempts are reached") 68 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/default-queue.test.ts: -------------------------------------------------------------------------------- 1 | import { ServiceBusClient } from "@azure/service-bus" 2 | import { fixturedTest } from "test/fixtured-test.js" 3 | 4 | fixturedTest("creates queue by default", async ({ azure, expect }) => { 5 | const sb_client = new ServiceBusClient( 6 | `Endpoint=sb://default.default.default.localhost;SharedAccessKeyName=${"1234"};SharedAccessKey=password;UseDevelopmentEmulator=true`, 7 | { 8 | customEndpointAddress: azure.sb_endpoint.toString(), 9 | ...azure.service_client_options, 10 | }, 11 | ) 12 | 13 | const sender = sb_client.createSender("default") 14 | await sender.sendMessages({ 15 | body: "hello world!", 16 | }) 17 | 18 | const receiver = sb_client.createReceiver("default") 19 | 20 | const [message] = await receiver.receiveMessages(1) 21 | expect(message!.body).toBe("hello world!") 22 | expect(message!.sequenceNumber!.toNumber()).toBe(1) 23 | 24 | await receiver.completeMessage(message!) 25 | }) 26 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/defaults.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "queue has populated defaults", 5 | async ({ azure_queue, expect }) => { 6 | const { createQueue, getQueue, location } = azure_queue 7 | 8 | const { name } = await createQueue({}) 9 | 10 | const queue = await getQueue(name!) 11 | 12 | expect(queue.createdAt).toBeDefined() 13 | expect(queue.updatedAt).toBeDefined() 14 | 15 | expect(queue).toMatchObject({ 16 | name, 17 | type: "Microsoft.ServiceBus/namespaces/queues", 18 | location, 19 | countDetails: { 20 | activeMessageCount: 0, 21 | deadLetterMessageCount: 0, 22 | scheduledMessageCount: 0, 23 | transferMessageCount: 0, 24 | transferDeadLetterMessageCount: 0, 25 | }, 26 | accessedAt: new Date("0001-01-01T07:52:58.000Z"), 27 | sizeInBytes: 0, 28 | messageCount: 0, 29 | lockDuration: "PT1M", 30 | maxSizeInMegabytes: 1024, 31 | maxMessageSizeInKilobytes: 256, 32 | requiresDuplicateDetection: false, 33 | requiresSession: false, 34 | defaultMessageTimeToLive: "P10675199DT2H48M5.4775807S", 35 | deadLetteringOnMessageExpiration: false, 36 | duplicateDetectionHistoryTimeWindow: "PT10M", 37 | maxDeliveryCount: 10, 38 | status: "Active", 39 | enableBatchedOperations: true, 40 | enablePartitioning: false, 41 | enableExpress: false, 42 | }) 43 | }, 44 | ) 45 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/defer.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("can defer single message", async ({ azure_queue, expect }) => { 4 | const { createSender, createReceiver, createQueue } = azure_queue 5 | 6 | const queue = await createQueue({}) 7 | 8 | const sender = createSender(queue.name!) 9 | 10 | await sender.sendMessages({ 11 | body: "hello world!", 12 | }) 13 | 14 | const receiver = createReceiver(queue.name!) 15 | 16 | { 17 | const [message] = await receiver.receiveMessages(1) 18 | await receiver.deferMessage(message!) 19 | 20 | const [deferred_message] = await receiver.receiveDeferredMessages( 21 | message!.sequenceNumber!, 22 | ) 23 | expect(deferred_message).toBeTruthy() 24 | expect(deferred_message).toMatchObject({ body: "hello world!" }) 25 | expect(deferred_message?.state).toBe("deferred") 26 | } 27 | 28 | { 29 | const messages = await receiver.receiveMessages(1, { 30 | maxWaitTimeInMs: 0, 31 | }) 32 | expect(messages).toHaveLength(0) 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/duplicate-detection.test.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import delay from "delay" 3 | import { fixturedTest } from "test/fixtured-test.js" 4 | 5 | fixturedTest( 6 | "can deduplicate recent messages", 7 | async ({ azure_queue, expect }) => { 8 | const { createSender, createReceiver, createQueue } = azure_queue 9 | 10 | const queue = await createQueue({ 11 | requiresDuplicateDetection: true, 12 | }) 13 | 14 | const sender = createSender(queue.name!) 15 | 16 | await sender.sendMessages({ 17 | body: "hello world!", 18 | messageId: "1234", 19 | }) 20 | 21 | await sender.sendMessages({ 22 | body: "hello world!", 23 | messageId: "1234", 24 | }) 25 | 26 | const receiver = createReceiver(queue.name!, {}) 27 | 28 | { 29 | const messages = await receiver.receiveMessages(2, { 30 | maxWaitTimeInMs: 0, 31 | }) 32 | 33 | expect(messages).toHaveLength(1) 34 | } 35 | }, 36 | ) 37 | 38 | fixturedTest( 39 | "duplicate detection does not occur for messages outside of history window period", 40 | { timeout: 40000 }, 41 | async ({ azure_queue, expect, env }) => { 42 | const { createSender, createReceiver, createQueue } = azure_queue 43 | 44 | const duplicateDetectionMs = env.TEST_AZURE_E2E ? 20000 : 200 45 | const queue = await createQueue({ 46 | requiresDuplicateDetection: true, 47 | duplicateDetectionHistoryTimeWindow: Temporal.Duration.from({ 48 | milliseconds: duplicateDetectionMs, 49 | }).toString(), 50 | }) 51 | 52 | const sender = createSender(queue.name!) 53 | 54 | await sender.sendMessages({ 55 | body: "hello world!", 56 | messageId: "1234", 57 | }) 58 | 59 | await delay(duplicateDetectionMs * 1.5) 60 | 61 | await sender.sendMessages({ 62 | body: "hello world!", 63 | messageId: "1234", 64 | }) 65 | 66 | const receiver = createReceiver(queue.name!, {}) 67 | 68 | { 69 | const messages = await receiver.receiveMessages(2, { 70 | maxWaitTimeInMs: 0, 71 | }) 72 | 73 | expect(messages).toHaveLength(2) 74 | } 75 | }, 76 | ) 77 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/expires-at.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "message has accurate expires at time", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createQueue } = azure_queue 7 | 8 | const queue = await createQueue({}) 9 | 10 | const sender = createSender(queue.name!) 11 | const receiver = createReceiver(queue.name!) 12 | 13 | const ttlMs = 100000 14 | 15 | await sender.sendMessages({ 16 | body: "hello world!", 17 | timeToLive: ttlMs, 18 | }) 19 | 20 | { 21 | const [message] = await receiver.receiveMessages(1) 22 | expect(message).toBeTruthy() 23 | 24 | expect(message?.expiresAtUtc).toStrictEqual( 25 | new Date(message!.enqueuedTimeUtc!.getTime() + ttlMs), 26 | ) 27 | } 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/large-message-batch.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "can parse and receive large message batch without delays", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createQueue } = azure_queue 7 | 8 | const queue = await createQueue({}) 9 | 10 | const sender = createSender(queue.name!) 11 | 12 | const batch = await sender.createMessageBatch({ 13 | maxSizeInBytes: 256_000, 14 | }) 15 | 16 | const num_messages = 1000 17 | 18 | for (let i = 0; i < num_messages; i++) { 19 | expect(batch.tryAddMessage({ body: "hello world!" })).toBe(true) 20 | } 21 | 22 | await sender.sendMessages(batch) 23 | 24 | const receiver = createReceiver(queue.name!) 25 | 26 | { 27 | const messages = await receiver.receiveMessages(num_messages) 28 | expect(messages).toHaveLength(num_messages) 29 | } 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/lock-duration.test.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import delay from "delay" 3 | import { fixturedTest } from "test/fixtured-test.js" 4 | 5 | fixturedTest( 6 | "can lock for a duration", 7 | async ({ azure_queue, expect, expectCorrelatedTime, env }) => { 8 | const { createSender, createReceiver, createQueue } = azure_queue 9 | 10 | const lockDurationMs = env.TEST_AZURE_E2E ? 5000 : 250 11 | 12 | const queue = await createQueue({ 13 | lockDuration: Temporal.Duration.from({ 14 | milliseconds: lockDurationMs, 15 | }).toString(), 16 | }) 17 | 18 | const sender = createSender(queue.name!) 19 | await sender.sendMessages({ 20 | body: "hello world!", 21 | }) 22 | 23 | const receiver = createReceiver(queue.name!, { 24 | maxAutoLockRenewalDurationInMs: 0, 25 | }) 26 | 27 | const [message] = await receiver.receiveMessages(1) 28 | expect(message!.body).toBe("hello world!") 29 | 30 | expectCorrelatedTime( 31 | message!.lockedUntilUtc!, 32 | new Date(message!.enqueuedTimeUtc!.getTime() + lockDurationMs), 33 | ) 34 | 35 | { 36 | const messages = await receiver.receiveMessages(1, { 37 | maxWaitTimeInMs: 0, 38 | }) 39 | 40 | expect(messages).toHaveLength(0) 41 | 42 | await delay(lockDurationMs) 43 | 44 | // TODO: should completing the message still work even after the message is no longer locked?? 45 | } 46 | 47 | { 48 | const [message] = await receiver.receiveMessages(1) 49 | expect(message!.body).toBe("hello world!") 50 | } 51 | }, 52 | ) 53 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/max-delivery-count-dlq.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | import { BrokerConstants } from "lib/broker/constants.js" 3 | 4 | fixturedTest( 5 | "expires messages after max delivery count and sends to DLQ", 6 | async ({ azure_queue, expect }) => { 7 | const { createSender, createReceiver, createQueue } = azure_queue 8 | 9 | const dlq = await createQueue({}) 10 | const queue = await createQueue({ 11 | maxDeliveryCount: 2, 12 | forwardDeadLetteredMessagesTo: dlq.name!, 13 | }) 14 | 15 | const sender = createSender(queue.name!) 16 | 17 | await sender.sendMessages({ 18 | body: "hello world!", 19 | }) 20 | 21 | { 22 | const receiver = createReceiver(queue.name!) 23 | 24 | const [message] = await receiver.receiveMessages(1, { 25 | maxWaitTimeInMs: 0, 26 | }) 27 | expect(message!.body).toBe("hello world!") 28 | expect(message!.deliveryCount).toBe(0) 29 | 30 | await receiver.abandonMessage(message!) 31 | } 32 | 33 | { 34 | const receiver = createReceiver(queue.name!) 35 | 36 | const [message] = await receiver.receiveMessages(1, { 37 | maxWaitTimeInMs: 0, 38 | }) 39 | expect(message!.body).toBe("hello world!") 40 | expect(message!.deliveryCount).toBe(1) 41 | 42 | await receiver.abandonMessage(message!) 43 | } 44 | 45 | { 46 | const receiver = createReceiver(dlq.name!) 47 | 48 | const [message] = await receiver.receiveMessages(1) 49 | expect(message).toBeDefined() 50 | expect(message?.deadLetterReason).toBe( 51 | BrokerConstants.errors.maxDeliveryCountExceeded.reason, 52 | ) 53 | expect(message?.deadLetterErrorDescription).toBe( 54 | BrokerConstants.errors.maxDeliveryCountExceeded.description, 55 | ) 56 | expect(message?.deadLetterSource).toBe(queue.name!) 57 | } 58 | }, 59 | ) 60 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/max-delivery-count.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "expires messages after max delivery count", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createQueue } = azure_queue 7 | 8 | const queue = await createQueue({ 9 | maxDeliveryCount: 2, 10 | }) 11 | 12 | const sender = createSender(queue.name!) 13 | 14 | await sender.sendMessages({ 15 | body: "hello world!", 16 | }) 17 | 18 | { 19 | const receiver = createReceiver(queue.name!) 20 | 21 | const [message] = await receiver.receiveMessages(1, { 22 | maxWaitTimeInMs: 0, 23 | }) 24 | expect(message!.body).toBe("hello world!") 25 | expect(message!.deliveryCount).toBe(0) 26 | 27 | await receiver.abandonMessage(message!) 28 | } 29 | 30 | { 31 | const receiver = createReceiver(queue.name!) 32 | 33 | const [message] = await receiver.receiveMessages(1, { 34 | maxWaitTimeInMs: 0, 35 | }) 36 | expect(message!.body).toBe("hello world!") 37 | expect(message!.deliveryCount).toBe(1) 38 | 39 | await receiver.abandonMessage(message!) 40 | } 41 | 42 | { 43 | const receiver = createReceiver(queue.name!) 44 | 45 | const messages = await receiver.receiveMessages(1, { 46 | maxWaitTimeInMs: 0, 47 | }) 48 | expect(messages).toHaveLength(0) 49 | } 50 | }, 51 | ) 52 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/message-batch.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "receives message batch in proper order", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createQueue } = azure_queue 7 | 8 | const queue = await createQueue({}) 9 | 10 | const sender = createSender(queue.name!) 11 | 12 | const batch = await sender.createMessageBatch({ 13 | maxSizeInBytes: 1024, 14 | }) 15 | batch.tryAddMessage({ 16 | body: "hello world!", 17 | }) 18 | batch.tryAddMessage({ 19 | body: "hello world2!", 20 | }) 21 | 22 | await sender.sendMessages(batch) 23 | 24 | const receiver = createReceiver(queue.name!) 25 | 26 | { 27 | const [message1, message2] = await receiver.receiveMessages(2) 28 | 29 | expect(message1!.body).toBe("hello world!") 30 | expect(message2!.body).toBe("hello world2!") 31 | 32 | await receiver.completeMessage(message1!) 33 | await receiver.completeMessage(message2!) 34 | } 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/message-content-properties.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("content type", async ({ azure_queue, expect }) => { 4 | const { createSender, createReceiver, createQueue } = azure_queue 5 | 6 | const queue = await createQueue({}) 7 | 8 | const sender = createSender(queue.name!) 9 | 10 | await sender.sendMessages({ 11 | body: { 12 | message: "hello world!", 13 | }, 14 | contentType: "application/json", 15 | }) 16 | 17 | const receiver = createReceiver(queue.name!) 18 | 19 | const [message] = await receiver.receiveMessages(1) 20 | expect(message!.body).toStrictEqual({ message: "hello world!" }) 21 | expect(message!.contentType).toBe("application/json") 22 | 23 | await receiver.completeMessage(message!) 24 | }) 25 | 26 | fixturedTest("subject", async ({ azure_queue, expect }) => { 27 | const { createSender, createReceiver, createQueue } = azure_queue 28 | 29 | const queue = await createQueue({}) 30 | 31 | const sender = createSender(queue.name!) 32 | 33 | await sender.sendMessages({ 34 | body: { 35 | message: "hello world!", 36 | }, 37 | subject: "Test Message", 38 | }) 39 | 40 | const receiver = createReceiver(queue.name!) 41 | 42 | const [message] = await receiver.receiveMessages(1) 43 | expect(message!.body).toStrictEqual({ message: "hello world!" }) 44 | expect(message!.subject).toBe("Test Message") 45 | 46 | await receiver.completeMessage(message!) 47 | }) 48 | 49 | fixturedTest("correlation id", async ({ azure_queue, expect }) => { 50 | const { createSender, createReceiver, createQueue } = azure_queue 51 | 52 | const queue = await createQueue({}) 53 | 54 | const sender = createSender(queue.name!) 55 | 56 | await sender.sendMessages({ 57 | body: { 58 | message: "hello world!", 59 | }, 60 | correlationId: "1234", 61 | }) 62 | 63 | const receiver = createReceiver(queue.name!) 64 | 65 | const [message] = await receiver.receiveMessages(1) 66 | expect(message!.body).toStrictEqual({ message: "hello world!" }) 67 | expect(message!.correlationId).toBe("1234") 68 | 69 | await receiver.completeMessage(message!) 70 | }) 71 | 72 | fixturedTest("message id", async ({ azure_queue, expect }) => { 73 | const { createSender, createReceiver, createQueue } = azure_queue 74 | 75 | const queue = await createQueue({}) 76 | 77 | const sender = createSender(queue.name!) 78 | 79 | await sender.sendMessages({ 80 | body: { 81 | message: "hello world!", 82 | }, 83 | messageId: "1234", 84 | }) 85 | 86 | const receiver = createReceiver(queue.name!) 87 | 88 | const [message] = await receiver.receiveMessages(1) 89 | expect(message!.body).toStrictEqual({ message: "hello world!" }) 90 | expect(message!.messageId).toBe("1234") 91 | 92 | await receiver.completeMessage(message!) 93 | }) 94 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/message-count.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("gives accurate messageCount", async ({ azure_queue, expect }) => { 4 | const { createSender, createReceiver, createQueue, getQueue } = azure_queue 5 | 6 | const queue = await createQueue({}) 7 | 8 | const sender = createSender(queue.name!) 9 | 10 | await sender.sendMessages({ 11 | body: "hello world!", 12 | }) 13 | 14 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 15 | messageCount: 1, 16 | }) 17 | 18 | const receiver = createReceiver(queue.name!) 19 | 20 | const [message] = await receiver.receiveMessages(1) 21 | expect(message!.body).toBe("hello world!") 22 | 23 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 24 | messageCount: 1, 25 | }) 26 | 27 | await receiver.completeMessage(message!) 28 | 29 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 30 | messageCount: 0, 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/multi-queue.test.ts: -------------------------------------------------------------------------------- 1 | import Long from "long" 2 | import { fixturedTest } from "test/fixtured-test.js" 3 | 4 | fixturedTest( 5 | "can receive concurrently from multiple queues without race conditions", 6 | async ({ azure_queue, expect }) => { 7 | const { createSender, createReceiver, createQueue } = azure_queue 8 | 9 | const queue = await createQueue({}) 10 | const queue2 = await createQueue({}) 11 | 12 | const sender = createSender(queue.name!) 13 | 14 | const sender2 = createSender(queue2.name!) 15 | 16 | await Promise.all([ 17 | sender.sendMessages({ 18 | body: "hello world!", 19 | }), 20 | sender2.sendMessages({ 21 | body: "hello world2!", 22 | }), 23 | ]) 24 | 25 | { 26 | const receiver = createReceiver(queue.name!) 27 | 28 | const [message] = await receiver.receiveMessages(1) 29 | expect(message!.body).toBe("hello world!") 30 | expect(message!.sequenceNumber).toEqual(new Long(1)) 31 | 32 | await receiver.completeMessage(message!) 33 | } 34 | 35 | { 36 | const receiver = createReceiver(queue2.name!) 37 | 38 | const [message] = await receiver.receiveMessages(1) 39 | expect(message!.body).toBe("hello world2!") 40 | expect(message!.sequenceNumber).toEqual(new Long(1)) 41 | 42 | await receiver.completeMessage(message!) 43 | } 44 | }, 45 | ) 46 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/naming-scheme.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "naming scheme of queue id is correct", 5 | async ({ azure_queue, expect }) => { 6 | const { 7 | createQueue, 8 | sb: { namespace_name }, 9 | rg_name, 10 | subscription_id, 11 | } = azure_queue 12 | 13 | const queue = await createQueue({}) 14 | 15 | expect(queue.id).toBe( 16 | `/subscriptions/${subscription_id}/resourceGroups/${rg_name}/providers/Microsoft.ServiceBus/namespaces/${namespace_name}/queues/${queue.name!}`, 17 | ) 18 | }, 19 | ) 20 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/peek.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest.only("can peek message", async ({ azure_queue, expect }) => { 4 | const { createSender, createReceiver, createQueue } = azure_queue 5 | 6 | const queue = await createQueue({}) 7 | 8 | const sender = createSender(queue.name!) 9 | const receiver = createReceiver(queue.name!) 10 | 11 | { 12 | const [message] = await receiver.peekMessages(1) 13 | expect(message).toBeFalsy() 14 | } 15 | 16 | await sender.sendMessages({ 17 | body: "hello world!", 18 | }) 19 | 20 | { 21 | const [message] = await receiver.peekMessages(1) 22 | expect(message).toBeTruthy() 23 | } 24 | }) 25 | 26 | fixturedTest("can peek multiple messages", async ({ azure_queue, expect }) => { 27 | const { createSender, createReceiver, createQueue } = azure_queue 28 | 29 | const queue = await createQueue({}) 30 | 31 | const sender = createSender(queue.name!) 32 | const receiver = createReceiver(queue.name!) 33 | 34 | { 35 | const [message] = await receiver.peekMessages(2) 36 | expect(message).toBeFalsy() 37 | } 38 | 39 | await sender.sendMessages({ 40 | body: "hello world!", 41 | }) 42 | 43 | await sender.sendMessages({ 44 | body: "hello world2!", 45 | }) 46 | 47 | await sender.sendMessages({ 48 | body: "hello world3!", 49 | }) 50 | 51 | { 52 | const [message1, message2] = await receiver.peekMessages(2) 53 | expect(message1!.body).toBe("hello world!") 54 | expect(message1!.enqueuedTimeUtc).toBeTruthy() 55 | expect(message2!.body).toBe("hello world2!") 56 | } 57 | }) 58 | 59 | fixturedTest.todo("can peek scheduled messages") 60 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/receive-and-delete.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("receiveAndDelete mode", async ({ azure_queue, expect }) => { 4 | const { createSender, createReceiver, createQueue } = azure_queue 5 | 6 | const queue = await createQueue({}) 7 | 8 | const sender = createSender(queue.name!) 9 | await sender.sendMessages({ 10 | body: "hello world!", 11 | }) 12 | 13 | { 14 | const receiver = createReceiver(queue.name!, { 15 | receiveMode: "receiveAndDelete", 16 | }) 17 | 18 | const [message] = await receiver.receiveMessages(1) 19 | expect(message!.body).toBe("hello world!") 20 | } 21 | 22 | { 23 | const receiver = createReceiver(queue.name!) 24 | 25 | const messages = await receiver.receiveMessages(1, { 26 | maxWaitTimeInMs: 0, 27 | }) 28 | expect(messages).toHaveLength(0) 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/redelivery-order.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "redelivery order is based on sequence number", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createQueue } = azure_queue 7 | 8 | const queue = await createQueue({}) 9 | 10 | const sender = createSender(queue.name!) 11 | 12 | await sender.sendMessages({ body: "1" }) 13 | await sender.sendMessages({ body: "2" }) 14 | 15 | { 16 | const receiver = createReceiver(queue.name!) 17 | 18 | const [message] = await receiver.receiveMessages(1) 19 | 20 | const receiver2 = createReceiver(queue.name!) 21 | 22 | const [message2] = await receiver2.receiveMessages(1) 23 | expect(message2!.body).toBe("2") 24 | 25 | await receiver.abandonMessage(message!) 26 | await receiver2.abandonMessage(message2!) 27 | } 28 | 29 | { 30 | const receiver = createReceiver(queue.name!, {}) 31 | 32 | const [message1, message2] = await receiver.receiveMessages(2, { 33 | maxWaitTimeInMs: 0, 34 | }) 35 | 36 | expect(message1!.body).toBe("1") 37 | expect(message2!.body).toBe("2") 38 | } 39 | }, 40 | ) 41 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/renew-lock.test.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import delay from "delay" 3 | import { fixturedTest } from "test/fixtured-test.js" 4 | 5 | fixturedTest( 6 | "can renew lock", 7 | async ({ azure_queue, expect, expectCorrelatedTime, env }) => { 8 | const { createSender, createReceiver, createQueue } = azure_queue 9 | 10 | const lockDurationMs = env.TEST_AZURE_E2E ? 5000 : 500 11 | 12 | const queue = await createQueue({ 13 | lockDuration: Temporal.Duration.from({ 14 | milliseconds: lockDurationMs, 15 | }).toString(), 16 | }) 17 | 18 | const sender = createSender(queue.name!) 19 | 20 | await sender.sendMessages({ 21 | body: "hello world!", 22 | }) 23 | 24 | const receiver = createReceiver(queue.name!, { 25 | maxAutoLockRenewalDurationInMs: 0, 26 | }) 27 | 28 | const [message] = await receiver.receiveMessages(1) 29 | expect(message!.body).toBe("hello world!") 30 | 31 | { 32 | await delay(lockDurationMs / 2) 33 | 34 | { 35 | const messages = await receiver.receiveMessages(1, { 36 | maxWaitTimeInMs: 0, 37 | }) 38 | 39 | expect(messages).toHaveLength(0) 40 | } 41 | 42 | const relocked_at_date = await receiver.renewMessageLock(message!) 43 | expectCorrelatedTime( 44 | relocked_at_date, 45 | new Date(Date.now() + lockDurationMs), 46 | ) 47 | expect(message?.lockedUntilUtc).toStrictEqual(relocked_at_date) 48 | 49 | await delay(lockDurationMs) 50 | 51 | { 52 | const messages = await receiver.receiveMessages(1, { 53 | maxWaitTimeInMs: 0, 54 | }) 55 | 56 | expect(messages).toHaveLength(1) 57 | expect(messages[0]).toMatchObject({ body: "hello world!" }) 58 | } 59 | } 60 | }, 61 | ) 62 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/scheduled.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "Can schedule a message to a queue", 5 | async ({ azure_queue, expect, env }) => { 6 | const { createSender, createReceiver, createQueue, getQueue } = azure_queue 7 | 8 | const queue = await createQueue({}) 9 | 10 | const sender = createSender(queue.name!) 11 | 12 | const scheduleMs = env.TEST_AZURE_E2E ? 5000 : 200 13 | 14 | const schedule_at = new Date(Date.now() + scheduleMs) 15 | 16 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 17 | countDetails: { 18 | scheduledMessageCount: 0, 19 | }, 20 | }) 21 | 22 | await sender.scheduleMessages( 23 | { 24 | body: "hello world!", 25 | }, 26 | schedule_at, 27 | ) 28 | 29 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 30 | countDetails: { 31 | scheduledMessageCount: 1, 32 | }, 33 | }) 34 | 35 | const receiver = createReceiver(queue.name!) 36 | 37 | { 38 | const [message] = await receiver.peekMessages(1) 39 | expect(message!.state).toBe("scheduled") 40 | } 41 | 42 | expect(Date.now() < schedule_at.getTime()).toBe(true) 43 | 44 | const [message] = await receiver.receiveMessages(1) 45 | expect(message!.body).toBe("hello world!") 46 | expect(message!.scheduledEnqueueTimeUtc).toStrictEqual(schedule_at) 47 | expect(message!.state).toBe("scheduled") 48 | 49 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 50 | countDetails: { 51 | scheduledMessageCount: 0, 52 | }, 53 | messageCount: 1, 54 | }) 55 | 56 | await receiver.completeMessage(message!) 57 | 58 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 59 | countDetails: { 60 | scheduledMessageCount: 0, 61 | }, 62 | messageCount: 0, 63 | }) 64 | 65 | expect(Date.now() >= schedule_at.getTime()).toBe(true) 66 | }, 67 | ) 68 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/session/complete.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "can complete single message from queue w/ session id", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createQueue, acceptSession } = azure_queue 7 | 8 | const queue = await createQueue({ requiresSession: true }) 9 | 10 | const sender = createSender(queue.name!) 11 | await sender.sendMessages({ 12 | body: "hello world!", 13 | sessionId: "session", 14 | }) 15 | await sender.sendMessages({ 16 | body: "hello world!", 17 | sessionId: "session2", 18 | }) 19 | 20 | const receiver = await acceptSession(queue.name!, "session2") 21 | 22 | const [message] = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 23 | expect(message!.body).toBe("hello world!") 24 | expect(message!.sessionId).toBe("session2") 25 | expect(message!.sequenceNumber!.toNumber()).toBe(2) 26 | 27 | await receiver.completeMessage(message!) 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/session/deduplication.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "deduplication occurs across sessions", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, acceptSession, createQueue } = azure_queue 7 | 8 | const queue = await createQueue({ 9 | requiresSession: true, 10 | requiresDuplicateDetection: true, 11 | }) 12 | const sender = createSender(queue.name!) 13 | 14 | await sender.sendMessages({ 15 | body: "1", 16 | sessionId: "session1", 17 | messageId: "id", 18 | }) 19 | await sender.sendMessages({ 20 | body: "2", 21 | sessionId: "session2", 22 | messageId: "id", 23 | }) 24 | 25 | { 26 | const receiver = await acceptSession(queue.name!, "session1") 27 | const messages = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 28 | expect(messages).toHaveLength(1) 29 | } 30 | 31 | { 32 | const receiver = await acceptSession(queue.name!, "session2") 33 | const messages = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 34 | expect(messages).toHaveLength(0) 35 | } 36 | }, 37 | ) 38 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/session/locked.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "cannot consume messages while last message is locked", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, acceptSession, createQueue } = azure_queue 7 | 8 | const queue = await createQueue({ requiresSession: true }) 9 | const sender = createSender(queue.name!) 10 | const sessionId = "session" 11 | 12 | await sender.sendMessages({ body: "1", sessionId }) 13 | await sender.sendMessages({ body: "2", sessionId }) 14 | 15 | const receiver = await acceptSession(queue.name!, sessionId) 16 | 17 | const [message] = await receiver.receiveMessages(1) 18 | expect(message!.body).toBe("1") 19 | expect(message!.sessionId).toBe(sessionId) 20 | expect(message!.sequenceNumber!.toNumber()).toBe(1) 21 | 22 | await expect(acceptSession(queue.name!, sessionId)).rejects.toThrowError( 23 | "The requested session 'session' cannot be accepted. It may be locked by another receiver.", 24 | ) 25 | 26 | await expect(acceptSession(queue.name!, sessionId)).rejects.toThrowError( 27 | "The requested session 'session' cannot be accepted. It may be locked by another receiver.", 28 | ) 29 | 30 | await receiver.close() 31 | 32 | { 33 | const receiver = await acceptSession(queue.name!, sessionId) 34 | 35 | const [message] = await receiver.receiveMessages(1) 36 | expect(message!.body).toBe("2") 37 | expect(message!.sessionId).toBe(sessionId) 38 | expect(message!.sequenceNumber!.toNumber()).toBe(2) 39 | 40 | await receiver.completeMessage(message!) 41 | } 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/session/peek-session.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "deduplication occurs across sessions", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, acceptSession, createQueue } = azure_queue 7 | 8 | const queue = await createQueue({ requiresSession: true }) 9 | const sender = createSender(queue.name!) 10 | 11 | await sender.sendMessages({ body: "1", sessionId: "session1" }) 12 | await sender.sendMessages({ body: "2", sessionId: "session2" }) 13 | 14 | { 15 | const receiver = await acceptSession(queue.name!, "session1") 16 | const messages = await receiver.peekMessages(2) 17 | expect(messages).toHaveLength(1) 18 | } 19 | }, 20 | ) 21 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/session/requires-session-enabled.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "cannot accept session if requiresSession is false", 5 | { 6 | timeout: 60000, 7 | }, 8 | async ({ azure_queue, expect }) => { 9 | const { createQueue, acceptSession } = azure_queue 10 | 11 | const queue = await createQueue({ requiresSession: false }) 12 | 13 | await expect( 14 | acceptSession(queue.name!, "session", { 15 | maxAutoLockRenewalDurationInMs: 0, 16 | }), 17 | ) 18 | .rejects.toThrowError 19 | // TODO: message is not being sent right... 20 | // "A sessionful message receiver cannot be created on an entity that does not require sessions. Ensure RequiresSession is set to true when creating a Queue or Subscription to enable sessionful behavior.", 21 | () 22 | }, 23 | ) 24 | 25 | fixturedTest( 26 | "cannot send non-session message to session queue", 27 | async ({ azure_queue, expect }) => { 28 | const { createSender, createQueue } = azure_queue 29 | 30 | const queue = await createQueue({ requiresSession: true }) 31 | 32 | const sender = createSender(queue.name!) 33 | 34 | await expect( 35 | sender.sendMessages({ 36 | body: "hello world!", 37 | }), 38 | ).rejects.toThrowError( 39 | "ServiceBusError: InvalidOperationError: The SessionId was not set on a message, and it cannot be sent to the entity. Entities that have session support enabled can only receive messages that have the SessionId set to a valid value.", 40 | ) 41 | }, 42 | ) 43 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/sub-queue-type.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("deadLetter subqueueType", async ({ azure_queue, expect }) => { 4 | const { createSender, createReceiver, createQueue, getQueue } = azure_queue 5 | 6 | const queue = await createQueue({}) 7 | 8 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 9 | countDetails: { 10 | transferDeadLetterMessageCount: 0, 11 | }, 12 | }) 13 | 14 | const sender = createSender(queue.name!) 15 | await sender.sendMessages({ 16 | body: "hello world!", 17 | }) 18 | 19 | const receiver = createReceiver(queue.name!, {}) 20 | 21 | const [message] = await receiver.receiveMessages(1) 22 | expect(message!.body).toBe("hello world!") 23 | expect(message?.sequenceNumber).toBeDefined() 24 | 25 | await receiver.deadLetterMessage(message!, { 26 | deadLetterReason: "dead letter reason", 27 | deadLetterErrorDescription: "dead letter error description", 28 | }) 29 | 30 | const receiver_dlq = createReceiver(queue.name!, { 31 | subQueueType: "deadLetter", 32 | }) 33 | 34 | const [dead_lettered_message] = await receiver_dlq.receiveMessages(1, { 35 | maxWaitTimeInMs: 1000, 36 | }) 37 | expect(dead_lettered_message!.body).toBe("hello world!") 38 | expect(dead_lettered_message!.sequenceNumber).toEqual(message!.sequenceNumber) 39 | 40 | await receiver_dlq.completeMessage(dead_lettered_message!) 41 | 42 | expect(dead_lettered_message?.deadLetterSource).toBeUndefined() 43 | expect(dead_lettered_message?.deadLetterReason).toBe("dead letter reason") 44 | expect(dead_lettered_message?.deadLetterErrorDescription).toBe( 45 | "dead letter error description", 46 | ) 47 | 48 | await expect(getQueue(queue.name!)).resolves.toMatchObject({ 49 | countDetails: { 50 | transferDeadLetterMessageCount: 0, 51 | }, 52 | }) 53 | 54 | expect(dead_lettered_message?.sequenceNumber).toBeDefined() 55 | expect(dead_lettered_message?.enqueuedSequenceNumber).toBeUndefined() 56 | expect(dead_lettered_message?.sequenceNumber).not.toStrictEqual( 57 | message?.enqueuedSequenceNumber, 58 | ) 59 | }) 60 | 61 | fixturedTest( 62 | "deadLetter subqueueType fails when auto-forwarding is enabled", 63 | async ({ azure_queue, expect }) => { 64 | const { createReceiver, createQueue } = azure_queue 65 | 66 | const dlq = await createQueue({}) 67 | const queue = await createQueue({ 68 | forwardDeadLetteredMessagesTo: dlq.name!, 69 | }) 70 | 71 | const receiver_dlq = createReceiver(queue.name!, { 72 | subQueueType: "deadLetter", 73 | }) 74 | 75 | await expect( 76 | receiver_dlq.receiveMessages(1, { 77 | maxWaitTimeInMs: 0, 78 | }), 79 | ).rejects.toThrowError("") 80 | }, 81 | ) 82 | -------------------------------------------------------------------------------- /test/parity/service-bus/queue/ttl.test.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import delay from "delay" 3 | import { fixturedTest } from "test/fixtured-test.js" 4 | 5 | fixturedTest( 6 | "message ttl is respected", 7 | async ({ azure_queue, expect, env }) => { 8 | const { createSender, createReceiver, createQueue } = azure_queue 9 | 10 | const queue = await createQueue({}) 11 | 12 | const sender = createSender(queue.name!) 13 | 14 | const ttlMs = env.TEST_AZURE_E2E ? 5000 : 200 15 | 16 | await sender.sendMessages({ 17 | body: "hello world!", 18 | timeToLive: ttlMs, 19 | }) 20 | 21 | await delay(ttlMs) 22 | 23 | const receiver = createReceiver(queue.name!) 24 | 25 | const messages = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 26 | expect(messages).toHaveLength(0) 27 | }, 28 | ) 29 | 30 | fixturedTest( 31 | "message default ttl is respected", 32 | async ({ azure_queue, expect, env }) => { 33 | const { createSender, createReceiver, createQueue } = azure_queue 34 | 35 | const ttlMs = env.TEST_AZURE_E2E ? 5000 : 200 36 | 37 | const queue = await createQueue({ 38 | defaultMessageTimeToLive: Temporal.Duration.from({ 39 | milliseconds: ttlMs, 40 | }).toString(), 41 | }) 42 | 43 | const sender = createSender(queue.name!) 44 | 45 | await sender.sendMessages({ 46 | body: "hello world!", 47 | }) 48 | 49 | await delay(ttlMs) 50 | 51 | const receiver = createReceiver(queue.name!) 52 | 53 | const messages = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 54 | expect(messages).toHaveLength(0) 55 | }, 56 | ) 57 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/abandon.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "can abandon single message from queue", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createTopic, createSubscription } = 7 | azure_queue 8 | 9 | const topic = await createTopic({}) 10 | const subscription = await createSubscription(topic.name!, { 11 | maxDeliveryCount: 1, 12 | }) 13 | 14 | const sender = createSender(topic.name!) 15 | await sender.sendMessages({ body: "hello world!" }) 16 | 17 | const receiver = createReceiver(topic.name!, subscription.name!) 18 | 19 | { 20 | const [message] = await receiver.receiveMessages(1) 21 | 22 | expect(message!.body).toBe("hello world!") 23 | await receiver.abandonMessage(message!) 24 | } 25 | 26 | { 27 | const [message] = await receiver.receiveMessages(1, { 28 | maxWaitTimeInMs: 0, 29 | }) 30 | 31 | expect(message).toBeUndefined() 32 | } 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/accessed-at.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "topic & subscription accessedAt is updated on receiver open", 5 | async ({ azure_queue, expect }) => { 6 | const { 7 | createReceiver, 8 | createTopic, 9 | getTopic, 10 | createSubscription, 11 | getSubscription, 12 | } = azure_queue 13 | 14 | const topic = await createTopic({}) 15 | expect(topic.accessedAt?.getTime()).toBeLessThan(0) 16 | 17 | const subscription = await createSubscription(topic.name!, {}) 18 | 19 | const receiver = createReceiver(topic.name!, subscription.name!) 20 | 21 | const receiveDate = new Date() 22 | await receiver.receiveMessages(1, { 23 | maxWaitTimeInMs: 0, 24 | }) 25 | 26 | { 27 | const { accessedAt } = await getTopic(topic.name!) 28 | 29 | expect(accessedAt).toBeDefined() 30 | expect(accessedAt!.getTime()).toBeGreaterThan(receiveDate.getTime()) 31 | expect(accessedAt!.getTime()).toBeGreaterThan(topic.accessedAt!.getTime()) 32 | } 33 | 34 | { 35 | const { accessedAt } = await getSubscription( 36 | topic.name!, 37 | subscription.name!, 38 | ) 39 | 40 | expect(accessedAt).toBeDefined() 41 | expect(accessedAt!.getTime()).toBeGreaterThan(receiveDate.getTime()) 42 | expect(accessedAt!.getTime()).toBeGreaterThan(topic.accessedAt!.getTime()) 43 | } 44 | }, 45 | ) 46 | 47 | fixturedTest( 48 | "accessedAt is updated on message sent", 49 | async ({ azure_queue, expect }) => { 50 | const { createSender, createTopic, getTopic } = azure_queue 51 | 52 | const topic = await createTopic({}) 53 | expect(topic.accessedAt?.getTime()).toBeLessThan(0) 54 | 55 | const sender = createSender(topic.name!) 56 | 57 | const sendDate = new Date() 58 | await sender.sendMessages({ 59 | body: "hello world!", 60 | }) 61 | 62 | { 63 | const { accessedAt } = await getTopic(topic.name!) 64 | 65 | expect(accessedAt).toBeDefined() 66 | expect(accessedAt!.getTime()).toBeGreaterThan(sendDate.getTime()) 67 | expect(accessedAt!.getTime()).toBeGreaterThan(topic.accessedAt!.getTime()) 68 | } 69 | }, 70 | ) 71 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/active-message-count.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("gives accurate messageCount", async ({ azure_queue, expect }) => { 4 | const { 5 | createSender, 6 | createReceiver, 7 | createTopic, 8 | getTopic, 9 | createSubscription, 10 | getSubscription, 11 | } = azure_queue 12 | 13 | const topic = await createTopic({}) 14 | const subscription = await createSubscription(topic.name!, {}) 15 | 16 | const sender = createSender(topic.name!) 17 | 18 | await sender.sendMessages({ 19 | body: "hello world!", 20 | }) 21 | 22 | await expect(getTopic(topic.name!)).resolves.toMatchObject({ 23 | countDetails: { 24 | activeMessageCount: 0, 25 | }, 26 | }) 27 | 28 | await expect( 29 | getSubscription(topic.name!, subscription.name!), 30 | ).resolves.toMatchObject({ 31 | countDetails: { 32 | activeMessageCount: 1, 33 | }, 34 | }) 35 | 36 | const receiver = createReceiver(topic.name!, subscription.name!) 37 | 38 | const [message] = await receiver.receiveMessages(1) 39 | expect(message!.body).toBe("hello world!") 40 | 41 | await expect( 42 | getSubscription(topic.name!, subscription.name!), 43 | ).resolves.toMatchObject({ 44 | countDetails: { 45 | activeMessageCount: 1, 46 | }, 47 | }) 48 | 49 | await receiver.completeMessage(message!) 50 | 51 | await expect( 52 | getSubscription(topic.name!, subscription.name!), 53 | ).resolves.toMatchObject({ 54 | countDetails: { 55 | activeMessageCount: 0, 56 | }, 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/cancel-scheduled.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "can cancel a scheduled message from a queue", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createTopic, createSubscription } = 7 | azure_queue 8 | 9 | const topic = await createTopic({}) 10 | const subscription = await createSubscription(topic.name!, {}) 11 | 12 | const sender = createSender(topic.name!) 13 | 14 | const schedule_at = new Date(Date.now() + 2500) 15 | 16 | const msg = await sender.scheduleMessages( 17 | { 18 | body: "hello world!", 19 | }, 20 | schedule_at, 21 | ) 22 | 23 | await sender.cancelScheduledMessages(msg) 24 | const receiver = createReceiver(topic.name!, subscription.name!) 25 | 26 | expect(Date.now() < schedule_at.getTime()).toBe(true) 27 | 28 | const [message] = await receiver.receiveMessages(1, { 29 | maxWaitTimeInMs: 1000, 30 | }) 31 | expect(message).toBeFalsy() 32 | }, 33 | ) 34 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/complete.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "can complete single message from subscription", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createTopic, createSubscription } = 7 | azure_queue 8 | 9 | const topic = await createTopic({}) 10 | const subscription1 = await createSubscription(topic.name!, {}) 11 | const subscription2 = await createSubscription(topic.name!, {}) 12 | 13 | const sender = createSender(topic.name!) 14 | 15 | await sender.sendMessages({ 16 | body: "hello world!", 17 | }) 18 | 19 | { 20 | const receiver = createReceiver(topic.name!, subscription1.name!) 21 | 22 | const [message] = await receiver.receiveMessages(1, { 23 | maxWaitTimeInMs: 1000, 24 | }) 25 | 26 | expect(message!.body).toBe("hello world!") 27 | expect(message!.sequenceNumber!.toNumber()).toBe(1) 28 | expect(message!.enqueuedSequenceNumber!).toBe(1) 29 | 30 | await receiver.completeMessage(message!) 31 | } 32 | 33 | { 34 | const receiver = createReceiver(topic.name!, subscription2.name!) 35 | 36 | const [message] = await receiver.receiveMessages(1, { 37 | maxWaitTimeInMs: 1000, 38 | }) 39 | 40 | expect(message!.body).toBe("hello world!") 41 | expect(message!.sequenceNumber!.toNumber()).toBe(1) 42 | expect(message!.enqueuedSequenceNumber!).toBe(1) 43 | 44 | await receiver.completeMessage(message!) 45 | } 46 | 47 | const subscription3 = await createSubscription(topic.name!, {}) 48 | 49 | await sender.sendMessages({ 50 | body: "hello world!", 51 | }) 52 | 53 | { 54 | const receiver = createReceiver(topic.name!, subscription3.name!) 55 | 56 | const [message] = await receiver.receiveMessages(1, { 57 | maxWaitTimeInMs: 1000, 58 | }) 59 | 60 | expect(message!.body).toBe("hello world!") 61 | expect(message!.sequenceNumber!.toNumber()).toBe(1) 62 | expect(message!.enqueuedSequenceNumber!).toBe(2) 63 | 64 | await receiver.completeMessage(message!) 65 | } 66 | }, 67 | ) 68 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/dead-letter-message-expiration.test.ts: -------------------------------------------------------------------------------- 1 | import delay from "delay" 2 | import { fixturedTest } from "test/fixtured-test.js" 3 | import { BrokerConstants } from "lib/broker/constants.js" 4 | 5 | fixturedTest( 6 | "dead letter expired messages with deadLetteringOnMessageExpiration", 7 | async ({ azure_queue, expect }) => { 8 | const { 9 | createSender, 10 | createReceiver, 11 | createQueue, 12 | createTopic, 13 | createSubscription, 14 | getTopic, 15 | getSubscription, 16 | getQueue, 17 | } = azure_queue 18 | 19 | const dlq = await createQueue({}) 20 | 21 | const topic = await createTopic({}) 22 | const subscription = await createSubscription(topic.name!, { 23 | deadLetteringOnMessageExpiration: true, 24 | forwardDeadLetteredMessagesTo: dlq.name!, 25 | }) 26 | 27 | const sender = createSender(topic.name!) 28 | 29 | const ttlMs = 200 30 | 31 | await sender.sendMessages({ 32 | body: "hello world!", 33 | timeToLive: ttlMs, 34 | }) 35 | 36 | await delay(ttlMs) 37 | 38 | { 39 | const receiver = createReceiver(topic.name!, subscription.name!) 40 | 41 | const messages = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 42 | expect(messages).toHaveLength(0) 43 | } 44 | 45 | { 46 | const receiver = createReceiver(dlq.name!) 47 | 48 | const [message] = await receiver.receiveMessages(1, { 49 | maxWaitTimeInMs: 5000, 50 | }) 51 | expect(message?.deadLetterSource).toBe( 52 | `${topic.name}/Subscriptions/${subscription.name}`, 53 | ) 54 | expect(message?.deadLetterReason).toBe( 55 | BrokerConstants.errors.messageExpired.reason, 56 | ) 57 | expect(message?.deadLetterErrorDescription).toBe( 58 | BrokerConstants.errors.messageExpired.description, 59 | ) 60 | expect(message).toBeTruthy() 61 | 62 | await expect( 63 | getSubscription(topic.name!, subscription.name!), 64 | ).resolves.toMatchObject({ 65 | countDetails: { 66 | transferDeadLetterMessageCount: 0, 67 | }, 68 | }) 69 | await expect(getTopic(topic.name!)).resolves.toMatchObject({ 70 | countDetails: { 71 | transferDeadLetterMessageCount: 0, 72 | }, 73 | }) 74 | await expect(getQueue(dlq.name!)).resolves.toMatchObject({ 75 | countDetails: { 76 | transferDeadLetterMessageCount: 0, 77 | }, 78 | }) 79 | 80 | expect(message?.sequenceNumber?.toNumber()).toStrictEqual(1) 81 | expect(message?.enqueuedSequenceNumber).toStrictEqual(1) 82 | } 83 | }, 84 | ) 85 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/dead-letter.test.ts: -------------------------------------------------------------------------------- 1 | import Long from "long" 2 | import { fixturedTest } from "test/fixtured-test.js" 3 | 4 | fixturedTest( 5 | "can manually dead letter message", 6 | async ({ azure_queue, expect }) => { 7 | const { 8 | createSender, 9 | createReceiver, 10 | createQueue, 11 | createTopic, 12 | createSubscription, 13 | getTopic, 14 | getSubscription, 15 | getQueue, 16 | } = azure_queue 17 | 18 | const dlq = await createQueue({}) 19 | const topic = await createTopic({}) 20 | const subscription = await createSubscription(topic.name!, { 21 | forwardDeadLetteredMessagesTo: dlq.name!, 22 | }) 23 | 24 | const sender = createSender(topic.name!) 25 | 26 | await sender.sendMessages({ body: "hello world!" }) 27 | await sender.sendMessages({ body: "hello world!" }) 28 | 29 | const receiver = createReceiver(topic.name!, subscription.name!) 30 | 31 | const [, message] = await receiver.receiveMessages(2, { 32 | maxWaitTimeInMs: 0, 33 | }) 34 | expect(message!.body).toBe("hello world!") 35 | expect(message?.sequenceNumber).toBeDefined() 36 | 37 | await receiver.deadLetterMessage(message!, { 38 | deadLetterReason: "dead letter reason", 39 | deadLetterErrorDescription: "dead letter error description", 40 | }) 41 | 42 | const receiver_dlq = createReceiver(dlq.name!) 43 | 44 | const [dead_lettered_message] = await receiver_dlq.receiveMessages(1, { 45 | maxWaitTimeInMs: 0, 46 | }) 47 | expect(dead_lettered_message!.body).toBe("hello world!") 48 | 49 | await receiver_dlq.completeMessage(dead_lettered_message!) 50 | 51 | expect(dead_lettered_message?.deadLetterSource).toBe( 52 | `${topic.name}/Subscriptions/${subscription.name}`, 53 | ) 54 | expect(dead_lettered_message?.deadLetterReason).toBe("dead letter reason") 55 | expect(dead_lettered_message?.deadLetterErrorDescription).toBe( 56 | "dead letter error description", 57 | ) 58 | 59 | await expect( 60 | getSubscription(topic.name!, subscription.name!), 61 | ).resolves.toMatchObject({ 62 | countDetails: { 63 | transferDeadLetterMessageCount: 0, 64 | }, 65 | }) 66 | await expect(getTopic(topic.name!)).resolves.toMatchObject({ 67 | countDetails: { 68 | transferDeadLetterMessageCount: 0, 69 | }, 70 | }) 71 | await expect(getQueue(dlq.name!)).resolves.toMatchObject({ 72 | countDetails: { 73 | transferDeadLetterMessageCount: 0, 74 | }, 75 | }) 76 | 77 | expect(dead_lettered_message?.sequenceNumber).toBeDefined() 78 | expect(dead_lettered_message?.enqueuedSequenceNumber).toBe( 79 | message?.sequenceNumber?.toNumber(), 80 | ) 81 | expect(dead_lettered_message?.sequenceNumber).toEqual(new Long(1)) 82 | 83 | // TODO: ensure dead letter options are present 84 | }, 85 | ) 86 | 87 | fixturedTest.todo("fails properly when trying to dead letter to same queue") 88 | fixturedTest.todo("cannot dead-letter when dlq is not set") 89 | fixturedTest.todo("dead-letters automatically once max attempts are reached") 90 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/defaults.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "topic has populated defaults", 5 | async ({ azure_queue, expect }) => { 6 | const { createTopic, getTopic, location } = azure_queue 7 | 8 | const { name } = await createTopic({}) 9 | 10 | const topic = await getTopic(name!) 11 | 12 | expect(topic.createdAt).toBeDefined() 13 | expect(topic.updatedAt).toBeDefined() 14 | 15 | expect(topic).toMatchObject({ 16 | name, 17 | type: "Microsoft.ServiceBus/namespaces/topics", 18 | location, 19 | sizeInBytes: 0, 20 | accessedAt: new Date("0001-01-01T00:00:00.000Z"), 21 | subscriptionCount: 0, 22 | countDetails: { 23 | activeMessageCount: 0, 24 | deadLetterMessageCount: 0, 25 | scheduledMessageCount: 0, 26 | transferMessageCount: 0, 27 | transferDeadLetterMessageCount: 0, 28 | }, 29 | defaultMessageTimeToLive: "P10675199DT2H48M5.4775807S", 30 | maxSizeInMegabytes: 1024, 31 | maxMessageSizeInKilobytes: 256, 32 | requiresDuplicateDetection: false, 33 | duplicateDetectionHistoryTimeWindow: "PT10M", 34 | enableBatchedOperations: true, 35 | status: "Active", 36 | supportOrdering: true, 37 | enablePartitioning: false, 38 | enableExpress: false, 39 | }) 40 | }, 41 | ) 42 | 43 | fixturedTest( 44 | "subscription has populated defaults", 45 | async ({ azure_queue, expect }) => { 46 | const { createTopic, createSubscription, getSubscription, location } = 47 | azure_queue 48 | 49 | const { name: topic_name } = await createTopic({}) 50 | const { name } = await createSubscription(topic_name!, {}) 51 | const subscription = await getSubscription(topic_name!, name!) 52 | 53 | expect(subscription.createdAt).toBeDefined() 54 | expect(subscription.updatedAt).toBeDefined() 55 | expect(subscription.accessedAt).toBeDefined() 56 | 57 | expect(subscription).toMatchObject({ 58 | name, 59 | type: "Microsoft.ServiceBus/namespaces/topics/subscriptions", 60 | location, 61 | messageCount: 0, 62 | countDetails: { 63 | activeMessageCount: 0, 64 | deadLetterMessageCount: 0, 65 | scheduledMessageCount: 0, 66 | transferMessageCount: 0, 67 | transferDeadLetterMessageCount: 0, 68 | }, 69 | lockDuration: "PT1M", 70 | requiresSession: false, 71 | defaultMessageTimeToLive: "P10675199DT2H48M5.4775807S", 72 | // TODO: re-enable once this is supported 73 | // deadLetteringOnFilterEvaluationExceptions: true, 74 | deadLetteringOnMessageExpiration: false, 75 | maxDeliveryCount: 10, 76 | status: "Active", 77 | enableBatchedOperations: true, 78 | isClientAffine: false, 79 | }) 80 | }, 81 | ) 82 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/defer.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("can defer single message", async ({ azure_queue, expect }) => { 4 | const { createSender, createReceiver, createTopic, createSubscription } = 5 | azure_queue 6 | 7 | const topic = await createTopic({}) 8 | const subscription = await createSubscription(topic.name!, {}) 9 | 10 | const sender = createSender(topic.name!) 11 | await sender.sendMessages({ body: "hello world!" }) 12 | 13 | const receiver = createReceiver(topic.name!, subscription.name!) 14 | 15 | { 16 | const [message] = await receiver.receiveMessages(1) 17 | await receiver.deferMessage(message!) 18 | 19 | const [deferred_message] = await receiver.receiveDeferredMessages( 20 | message!.sequenceNumber!, 21 | ) 22 | expect(deferred_message).toBeTruthy() 23 | expect(deferred_message).toMatchObject({ body: "hello world!" }) 24 | expect(deferred_message?.state).toBe("deferred") 25 | } 26 | 27 | { 28 | const messages = await receiver.receiveMessages(1, { 29 | maxWaitTimeInMs: 1000, 30 | }) 31 | expect(messages).toHaveLength(0) 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/duplicate-detection.test.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import delay from "delay" 3 | import { fixturedTest } from "test/fixtured-test.js" 4 | 5 | fixturedTest( 6 | "can deduplicate recent messages at topic level", 7 | async ({ azure_queue, expect }) => { 8 | const { createSender, createReceiver, createTopic, createSubscription } = 9 | azure_queue 10 | 11 | const topic = await createTopic({ requiresDuplicateDetection: true }) 12 | const subscription = await createSubscription(topic.name!, {}) 13 | 14 | const sender = createSender(topic.name!) 15 | 16 | await sender.sendMessages({ 17 | body: "hello world!", 18 | messageId: "1234", 19 | }) 20 | 21 | await sender.sendMessages({ 22 | body: "hello world!", 23 | messageId: "1234", 24 | }) 25 | 26 | const receiver = createReceiver(topic.name!, subscription.name!) 27 | 28 | { 29 | const messages = await receiver.receiveMessages(2, { 30 | maxWaitTimeInMs: 0, 31 | }) 32 | 33 | expect(messages).toHaveLength(1) 34 | } 35 | }, 36 | ) 37 | 38 | fixturedTest.todo("deduplication occurs independently in each subscription") 39 | 40 | fixturedTest( 41 | "duplicate detection does not occur for messages outside of history window period", 42 | { timeout: 40000 }, 43 | async ({ azure_queue, expect, env }) => { 44 | const { createSender, createReceiver, createSubscription, createTopic } = 45 | azure_queue 46 | 47 | const duplicateDetectionMs = env.TEST_AZURE_E2E ? 20000 : 200 48 | const topic = await createTopic({ 49 | requiresDuplicateDetection: true, 50 | duplicateDetectionHistoryTimeWindow: Temporal.Duration.from({ 51 | milliseconds: duplicateDetectionMs, 52 | }).toString(), 53 | }) 54 | const subscription = await createSubscription(topic.name!, {}) 55 | 56 | const sender = createSender(topic.name!) 57 | 58 | await sender.sendMessages({ 59 | body: "hello world!", 60 | messageId: "1234", 61 | }) 62 | 63 | await delay(duplicateDetectionMs * 1.5) 64 | 65 | await sender.sendMessages({ 66 | body: "hello world!", 67 | messageId: "1234", 68 | }) 69 | 70 | const receiver = createReceiver(topic.name!, subscription.name!, {}) 71 | 72 | { 73 | const messages = await receiver.receiveMessages(2, { 74 | maxWaitTimeInMs: 0, 75 | }) 76 | 77 | expect(messages).toHaveLength(2) 78 | } 79 | }, 80 | ) 81 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/expires-at.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "message has accurate expires at time", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createTopic, createSubscription } = 7 | azure_queue 8 | 9 | const topic = await createTopic({}) 10 | const subscription = await createSubscription(topic.name!, {}) 11 | 12 | const sender = createSender(topic.name!) 13 | const receiver = createReceiver(topic.name!, subscription.name!) 14 | 15 | const ttlMs = 100000 16 | 17 | await sender.sendMessages({ 18 | body: "hello world!", 19 | timeToLive: ttlMs, 20 | }) 21 | 22 | { 23 | const [message] = await receiver.receiveMessages(1) 24 | expect(message).toBeTruthy() 25 | 26 | expect(message?.expiresAtUtc).toStrictEqual( 27 | new Date(message!.enqueuedTimeUtc!.getTime() + ttlMs), 28 | ) 29 | } 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/lock-duration.test.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import delay from "delay" 3 | import { fixturedTest } from "test/fixtured-test.js" 4 | 5 | fixturedTest( 6 | "can lock for a duration", 7 | async ({ azure_queue, expect, expectCorrelatedTime, env }) => { 8 | const { createSender, createReceiver, createTopic, createSubscription } = 9 | azure_queue 10 | 11 | const lockDurationMs = env.TEST_AZURE_E2E ? 5000 : 250 12 | 13 | const topic = await createTopic({}) 14 | const subscription = await createSubscription(topic.name!, { 15 | lockDuration: Temporal.Duration.from({ 16 | milliseconds: lockDurationMs, 17 | }).toString(), 18 | }) 19 | 20 | const sender = createSender(topic.name!) 21 | await sender.sendMessages({ 22 | body: "hello world!", 23 | }) 24 | 25 | const receiver = createReceiver(topic.name!, subscription.name!, { 26 | maxAutoLockRenewalDurationInMs: 0, 27 | }) 28 | 29 | const [message] = await receiver.receiveMessages(1) 30 | expect(message!.body).toBe("hello world!") 31 | 32 | expectCorrelatedTime( 33 | message!.lockedUntilUtc!, 34 | new Date(message!.enqueuedTimeUtc!.getTime() + lockDurationMs), 35 | ) 36 | 37 | { 38 | const messages = await receiver.receiveMessages(1, { 39 | maxWaitTimeInMs: 0, 40 | }) 41 | 42 | expect(messages).toHaveLength(0) 43 | 44 | await delay(lockDurationMs) 45 | 46 | // TODO: should completing the message still work even after the message is no longer locked?? 47 | } 48 | 49 | { 50 | const [message] = await receiver.receiveMessages(1) 51 | expect(message!.body).toBe("hello world!") 52 | } 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/max-delivery-count-dlq.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | import { BrokerConstants } from "lib/broker/constants.js" 3 | 4 | fixturedTest( 5 | "expires messages after max delivery count and sends to DLQ", 6 | async ({ azure_queue, expect }) => { 7 | const { 8 | createSender, 9 | createReceiver, 10 | createQueue, 11 | createTopic, 12 | createSubscription, 13 | } = azure_queue 14 | 15 | const dlq = await createQueue({}) 16 | const topic = await createTopic({}) 17 | const subscription = await createSubscription(topic.name!, { 18 | maxDeliveryCount: 2, 19 | forwardDeadLetteredMessagesTo: dlq.name!, 20 | }) 21 | 22 | const sender = createSender(topic.name!) 23 | 24 | await sender.sendMessages({ 25 | body: "hello world!", 26 | }) 27 | 28 | { 29 | const receiver = createReceiver(topic.name!, subscription.name!) 30 | 31 | const [message] = await receiver.receiveMessages(1, { 32 | maxWaitTimeInMs: 0, 33 | }) 34 | expect(message!.body).toBe("hello world!") 35 | expect(message!.deliveryCount).toBe(0) 36 | 37 | await receiver.abandonMessage(message!) 38 | } 39 | 40 | { 41 | const receiver = createReceiver(topic.name!, subscription.name!) 42 | 43 | const [message] = await receiver.receiveMessages(1, { 44 | maxWaitTimeInMs: 0, 45 | }) 46 | expect(message!.body).toBe("hello world!") 47 | expect(message!.deliveryCount).toBe(1) 48 | 49 | await receiver.abandonMessage(message!) 50 | } 51 | 52 | { 53 | const receiver = createReceiver(dlq.name!) 54 | 55 | const [message] = await receiver.receiveMessages(1) 56 | expect(message).toBeDefined() 57 | expect(message?.deadLetterReason).toBe( 58 | BrokerConstants.errors.maxDeliveryCountExceeded.reason, 59 | ) 60 | expect(message?.deadLetterErrorDescription).toBe( 61 | BrokerConstants.errors.maxDeliveryCountExceeded.description, 62 | ) 63 | expect(message?.deadLetterSource).toBe( 64 | `${topic.name!}/Subscriptions/${subscription.name!}`, 65 | ) 66 | } 67 | }, 68 | ) 69 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/max-delivery-count.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "expires messages after max delivery count", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createSubscription, createTopic } = 7 | azure_queue 8 | 9 | const topic = await createTopic({}) 10 | const subscription = await createSubscription(topic.name!, { 11 | maxDeliveryCount: 2, 12 | }) 13 | 14 | const sender = createSender(topic.name!) 15 | await sender.sendMessages({ body: "hello world!" }) 16 | 17 | { 18 | const receiver = createReceiver(topic.name!, subscription.name!) 19 | 20 | const [message] = await receiver.receiveMessages(1, { 21 | maxWaitTimeInMs: 0, 22 | }) 23 | expect(message!.body).toBe("hello world!") 24 | expect(message!.deliveryCount).toBe(0) 25 | 26 | await receiver.abandonMessage(message!) 27 | } 28 | 29 | { 30 | const receiver = createReceiver(topic.name!, subscription.name!) 31 | 32 | const [message] = await receiver.receiveMessages(1, { 33 | maxWaitTimeInMs: 0, 34 | }) 35 | expect(message!.body).toBe("hello world!") 36 | expect(message!.deliveryCount).toBe(1) 37 | 38 | await receiver.abandonMessage(message!) 39 | } 40 | 41 | { 42 | const receiver = createReceiver(topic.name!, subscription.name!) 43 | 44 | const messages = await receiver.receiveMessages(1, { 45 | maxWaitTimeInMs: 0, 46 | }) 47 | expect(messages).toHaveLength(0) 48 | } 49 | }, 50 | ) 51 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/message-batch.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "receives message batch in proper order", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createReceiver, createTopic, createSubscription } = 7 | azure_queue 8 | 9 | const topic = await createTopic({}) 10 | const subscription = await createSubscription(topic.name!, {}) 11 | 12 | const sender = createSender(topic.name!) 13 | 14 | const batch = await sender.createMessageBatch({ maxSizeInBytes: 1024 }) 15 | batch.tryAddMessage({ body: "hello world!" }) 16 | batch.tryAddMessage({ body: "hello world2!" }) 17 | 18 | await sender.sendMessages(batch) 19 | 20 | const receiver = createReceiver(topic.name!, subscription.name!) 21 | 22 | { 23 | const [message1, message2] = await receiver.receiveMessages(2) 24 | 25 | expect(message1!.body).toBe("hello world!") 26 | expect(message2!.body).toBe("hello world2!") 27 | 28 | await receiver.completeMessage(message1!) 29 | await receiver.completeMessage(message2!) 30 | } 31 | }, 32 | ) 33 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/message-count.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("gives accurate messageCount", async ({ azure_queue, expect }) => { 4 | const { 5 | createSender, 6 | createReceiver, 7 | createTopic, 8 | createSubscription, 9 | getSubscription, 10 | } = azure_queue 11 | 12 | const topic = await createTopic({}) 13 | const subscription = await createSubscription(topic.name!, {}) 14 | 15 | const sender = createSender(topic.name!) 16 | 17 | await sender.sendMessages({ 18 | body: "hello world!", 19 | }) 20 | 21 | await expect( 22 | getSubscription(topic.name!, subscription.name!), 23 | ).resolves.toMatchObject({ 24 | messageCount: 1, 25 | }) 26 | 27 | const receiver = createReceiver(topic.name!, subscription.name!) 28 | 29 | const [message] = await receiver.receiveMessages(1) 30 | expect(message!.body).toBe("hello world!") 31 | 32 | await expect( 33 | getSubscription(topic.name!, subscription.name!), 34 | ).resolves.toMatchObject({ 35 | messageCount: 1, 36 | }) 37 | 38 | await receiver.completeMessage(message!) 39 | 40 | await expect( 41 | getSubscription(topic.name!, subscription.name!), 42 | ).resolves.toMatchObject({ 43 | messageCount: 0, 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/naming-scheme.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "naming scheme of topic & subscription id is correct", 5 | async ({ azure_queue, expect }) => { 6 | const { 7 | createTopic, 8 | createSubscription, 9 | sb: { namespace_name }, 10 | rg_name, 11 | subscription_id, 12 | } = azure_queue 13 | 14 | const topic = await createTopic({}) 15 | const subscription = await createSubscription(topic.name!, {}) 16 | 17 | expect(topic.id).toBe( 18 | `/subscriptions/${subscription_id}/resourceGroups/${rg_name}/providers/Microsoft.ServiceBus/namespaces/${namespace_name}/topics/${topic.name!}`, 19 | ) 20 | 21 | expect(subscription.id).toBe( 22 | `/subscriptions/${subscription_id}/resourceGroups/${rg_name}/providers/Microsoft.ServiceBus/namespaces/${namespace_name}/topics/${topic.name!}/subscriptions/${subscription.name!}`, 23 | ) 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/receive-and-delete.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("receiveAndDelete mode", async ({ azure_queue, expect }) => { 4 | const { createSender, createReceiver, createTopic, createSubscription } = 5 | azure_queue 6 | 7 | const topic = await createTopic({}) 8 | const subscription = await createSubscription(topic.name!, {}) 9 | 10 | const sender = createSender(topic.name!) 11 | await sender.sendMessages({ 12 | body: "hello world!", 13 | }) 14 | 15 | { 16 | const receiver = createReceiver(topic.name!, subscription.name!, { 17 | receiveMode: "receiveAndDelete", 18 | }) 19 | 20 | const [message] = await receiver.receiveMessages(1) 21 | expect(message!.body).toBe("hello world!") 22 | } 23 | 24 | { 25 | const receiver = createReceiver(topic.name!, subscription.name!) 26 | 27 | const messages = await receiver.receiveMessages(1, { 28 | maxWaitTimeInMs: 0, 29 | }) 30 | expect(messages).toHaveLength(0) 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/receive-from-topic.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest.todo("cannot receive from a topic") 4 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/renew-lock.test.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import delay from "delay" 3 | import { fixturedTest } from "test/fixtured-test.js" 4 | 5 | fixturedTest( 6 | "can renew lock", 7 | async ({ azure_queue, expect, expectCorrelatedTime, env }) => { 8 | const { createSender, createReceiver, createTopic, createSubscription } = 9 | azure_queue 10 | 11 | const lockDurationMs = env.TEST_AZURE_E2E ? 5000 : 500 12 | 13 | const topic = await createTopic({}) 14 | const subscription = await createSubscription(topic.name!, { 15 | lockDuration: Temporal.Duration.from({ 16 | milliseconds: lockDurationMs, 17 | }).toString(), 18 | }) 19 | 20 | const sender = createSender(topic.name!) 21 | 22 | await sender.sendMessages({ 23 | body: "hello world!", 24 | }) 25 | 26 | const receiver = createReceiver(topic.name!, subscription.name!, { 27 | maxAutoLockRenewalDurationInMs: 0, 28 | }) 29 | 30 | const [message] = await receiver.receiveMessages(1) 31 | expect(message!.body).toBe("hello world!") 32 | 33 | { 34 | await delay(lockDurationMs / 2) 35 | 36 | { 37 | const messages = await receiver.receiveMessages(1, { 38 | maxWaitTimeInMs: 0, 39 | }) 40 | 41 | expect(messages).toHaveLength(0) 42 | } 43 | 44 | const relocked_at_date = await receiver.renewMessageLock(message!) 45 | expectCorrelatedTime( 46 | relocked_at_date, 47 | new Date(Date.now() + lockDurationMs), 48 | ) 49 | expect(message?.lockedUntilUtc).toStrictEqual(relocked_at_date) 50 | 51 | await delay(lockDurationMs) 52 | 53 | { 54 | const messages = await receiver.receiveMessages(1, { 55 | maxWaitTimeInMs: 0, 56 | }) 57 | 58 | expect(messages).toHaveLength(1) 59 | expect(messages[0]).toMatchObject({ body: "hello world!" }) 60 | } 61 | } 62 | }, 63 | ) 64 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/send-to-subscription.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest.todo("cannot send to a subscription") 4 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/session/complete.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest( 4 | "can complete single message from queue w/ session id", 5 | async ({ azure_queue, expect }) => { 6 | const { createSender, createTopic, createSubscription, acceptSession } = 7 | azure_queue 8 | 9 | const topic = await createTopic({}) 10 | const subscription = await createSubscription(topic.name!, { 11 | requiresSession: true, 12 | }) 13 | 14 | const sender = createSender(topic.name!) 15 | await sender.sendMessages({ 16 | body: "hello world!", 17 | sessionId: "session", 18 | }) 19 | await sender.sendMessages({ 20 | body: "hello world!", 21 | sessionId: "session2", 22 | }) 23 | 24 | const receiver = await acceptSession( 25 | topic.name!, 26 | subscription.name!, 27 | "session2", 28 | ) 29 | 30 | const [message] = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 31 | expect(message!.body).toBe("hello world!") 32 | expect(message!.sessionId).toBe("session2") 33 | expect(message!.sequenceNumber!.toNumber()).toBe(2) 34 | 35 | await receiver.completeMessage(message!) 36 | }, 37 | ) 38 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/sub-queue-type.test.ts: -------------------------------------------------------------------------------- 1 | import { fixturedTest } from "test/fixtured-test.js" 2 | 3 | fixturedTest("deadLetter subqueueType", async ({ azure_queue, expect }) => { 4 | const { createSender, createReceiver, createTopic, createSubscription } = 5 | azure_queue 6 | 7 | const topic = await createTopic({}) 8 | const subscription = await createSubscription(topic.name!, {}) 9 | 10 | // await expect(getQueue(queue.name!)).resolves.toMatchObject({ 11 | // countDetails: { 12 | // transferDeadLetterMessageCount: 0, 13 | // }, 14 | // }) 15 | 16 | const sender = createSender(topic.name!) 17 | await sender.sendMessages({ body: "hello world!" }) 18 | 19 | const receiver = createReceiver(topic.name!, subscription.name!) 20 | 21 | const [message] = await receiver.receiveMessages(1) 22 | expect(message!.body).toBe("hello world!") 23 | expect(message?.sequenceNumber).toBeDefined() 24 | 25 | await receiver.deadLetterMessage(message!, { 26 | deadLetterReason: "dead letter reason", 27 | deadLetterErrorDescription: "dead letter error description", 28 | }) 29 | 30 | const receiver_dlq = createReceiver(topic.name!, subscription.name!, { 31 | subQueueType: "deadLetter", 32 | }) 33 | 34 | const [dead_lettered_message] = await receiver_dlq.receiveMessages(1, { 35 | maxWaitTimeInMs: 1000, 36 | }) 37 | expect(dead_lettered_message!.body).toBe("hello world!") 38 | expect(dead_lettered_message!.sequenceNumber).toEqual(message!.sequenceNumber) 39 | 40 | await receiver_dlq.completeMessage(dead_lettered_message!) 41 | 42 | expect(dead_lettered_message?.deadLetterSource).toBeUndefined() 43 | expect(dead_lettered_message?.deadLetterReason).toBe("dead letter reason") 44 | expect(dead_lettered_message?.deadLetterErrorDescription).toBe( 45 | "dead letter error description", 46 | ) 47 | 48 | // await expect(getQueue(queue.name!)).resolves.toMatchObject({ 49 | // countDetails: { 50 | // transferDeadLetterMessageCount: 0, 51 | // }, 52 | // }) 53 | 54 | expect(dead_lettered_message?.sequenceNumber).toBeDefined() 55 | expect(dead_lettered_message?.enqueuedSequenceNumber).toStrictEqual( 56 | message?.sequenceNumber?.toNumber(), 57 | ) 58 | expect(dead_lettered_message?.sequenceNumber).not.toStrictEqual( 59 | message?.enqueuedSequenceNumber, 60 | ) 61 | }) 62 | 63 | fixturedTest( 64 | "deadLetter subqueueType fails when auto-forwarding is enabled", 65 | async ({ azure_queue, expect }) => { 66 | const { createReceiver, createQueue, createTopic, createSubscription } = 67 | azure_queue 68 | 69 | const dlq = await createQueue({}) 70 | const topic = await createTopic({}) 71 | const subscription = await createSubscription(topic.name!, { 72 | forwardDeadLetteredMessagesTo: dlq.name!, 73 | }) 74 | 75 | const receiver_dlq = createReceiver(topic.name!, subscription.name!, { 76 | subQueueType: "deadLetter", 77 | }) 78 | 79 | await expect( 80 | receiver_dlq.receiveMessages(1, { 81 | maxWaitTimeInMs: 0, 82 | }), 83 | ).rejects.toThrowError( 84 | "Cannot create a message receiver on an entity with auto-forwarding enabled.", 85 | ) 86 | }, 87 | ) 88 | -------------------------------------------------------------------------------- /test/parity/service-bus/subscription/ttl.test.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import delay from "delay" 3 | import { fixturedTest } from "test/fixtured-test.js" 4 | 5 | fixturedTest( 6 | "message ttl is respected", 7 | async ({ azure_queue, expect, env }) => { 8 | const { createSender, createReceiver, createSubscription, createTopic } = 9 | azure_queue 10 | 11 | const topic = await createTopic({}) 12 | const subscription = await createSubscription(topic.name!, {}) 13 | 14 | const sender = createSender(topic.name!) 15 | 16 | const ttlMs = env.TEST_AZURE_E2E ? 5000 : 200 17 | 18 | await sender.sendMessages({ 19 | body: "hello world!", 20 | timeToLive: ttlMs, 21 | }) 22 | 23 | await delay(ttlMs) 24 | 25 | const receiver = createReceiver(topic.name!, subscription.name!) 26 | 27 | const messages = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 28 | expect(messages).toHaveLength(0) 29 | }, 30 | ) 31 | 32 | fixturedTest( 33 | "message default ttl is respected in topic", 34 | async ({ azure_queue, expect, env }) => { 35 | const { createSender, createReceiver, createTopic, createSubscription } = 36 | azure_queue 37 | 38 | const ttlMs = env.TEST_AZURE_E2E ? 5000 : 200 39 | 40 | const topic = await createTopic({ 41 | defaultMessageTimeToLive: Temporal.Duration.from({ 42 | milliseconds: ttlMs, 43 | }).toString(), 44 | }) 45 | const subscription = await createSubscription(topic.name!, {}) 46 | 47 | const sender = createSender(topic.name!) 48 | await sender.sendMessages({ body: "hello world!" }) 49 | 50 | await delay(ttlMs) 51 | 52 | const receiver = createReceiver(topic.name!, subscription.name!) 53 | 54 | const messages = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 55 | expect(messages).toHaveLength(0) 56 | }, 57 | ) 58 | 59 | fixturedTest( 60 | "message default ttl is respected in subscription", 61 | async ({ azure_queue, expect, env }) => { 62 | const { createSender, createReceiver, createTopic, createSubscription } = 63 | azure_queue 64 | 65 | const ttlMs = env.TEST_AZURE_E2E ? 5000 : 200 66 | 67 | const topic = await createTopic({}) 68 | const subscription = await createSubscription(topic.name!, { 69 | defaultMessageTimeToLive: Temporal.Duration.from({ 70 | milliseconds: ttlMs, 71 | }).toString(), 72 | }) 73 | 74 | const sender = createSender(topic.name!) 75 | await sender.sendMessages({ body: "hello world!" }) 76 | 77 | await delay(ttlMs) 78 | 79 | const receiver = createReceiver(topic.name!, subscription.name!) 80 | 81 | const messages = await receiver.receiveMessages(1, { maxWaitTimeInMs: 0 }) 82 | expect(messages).toHaveLength(0) 83 | }, 84 | ) 85 | -------------------------------------------------------------------------------- /test/service-bus/auto-delete-on-idle.test.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from "@js-temporal/polyfill" 2 | import delay from "delay" 3 | import { fixturedTest } from "test/fixtured-test.js" 4 | 5 | fixturedTest( 6 | "supports auto-delete on idle", 7 | async ({ onTestFinished, azure_queue, expect }) => { 8 | const { 9 | sb_management_client, 10 | createQueue, 11 | rg_name, 12 | sb: { namespace_name }, 13 | } = azure_queue 14 | 15 | process.env["LOCALSANDBOX_NO_ENFORCE_SB_AUTO_DELETE_IDLE_MINIMUM"] = "true" 16 | onTestFinished(() => { 17 | delete process.env["LOCALSANDBOX_NO_ENFORCE_SB_AUTO_DELETE_IDLE_MINIMUM"] 18 | }) 19 | 20 | const queue = await createQueue({ 21 | autoDeleteOnIdle: Temporal.Duration.from({ 22 | milliseconds: 100, 23 | }).toString(), 24 | }) 25 | 26 | await expect( 27 | sb_management_client.queues.get(rg_name, namespace_name, queue.name!), 28 | ).resolves.not.toThrow() 29 | 30 | await delay(150) 31 | 32 | await expect( 33 | sb_management_client.queues.get(rg_name, namespace_name, queue.name!), 34 | ).rejects.toThrow() 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /test/service-bus/sequence-number.test.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "@azure/core-amqp" 2 | import Long from "long" 3 | import { fixturedTest } from "test/fixtured-test.js" 4 | import { BrokerConstants } from "lib/broker/constants.js" 5 | import { unserializedLongToRheaParsable } from "lib/util/long.js" 6 | 7 | fixturedTest( 8 | "sends message with sequence number", 9 | async ({ azure_queue, expect }) => { 10 | const { createSender, createReceiver, createQueue } = azure_queue 11 | 12 | const queue = await createQueue({}) 13 | 14 | const sender = createSender(queue.name!) 15 | 16 | await sender.sendMessages({ 17 | body: "hello world!", 18 | }) 19 | 20 | const receiver = createReceiver(queue.name!) 21 | 22 | const [message] = await receiver.receiveMessages(1) 23 | expect(message!.sequenceNumber).toEqual(new Long(1)) 24 | }, 25 | ) 26 | 27 | fixturedTest( 28 | "can send message with large sequence number", 29 | async ({ azure_queue, expect }) => { 30 | const { createSender, createReceiver, createQueue } = azure_queue 31 | 32 | const queue = await createQueue({}) 33 | 34 | const sender = createSender(queue.name!) 35 | 36 | await sender.sendMessages({ 37 | applicationProperties: { 38 | operation: BrokerConstants.debug.operations.setSequenceNumber, 39 | }, 40 | body: { 41 | [Constants.sequenceNumber]: unserializedLongToRheaParsable.parse( 42 | new Long(0, 1), 43 | ), 44 | }, 45 | bodyType: "value", 46 | }) 47 | 48 | await sender.sendMessages({ 49 | body: "hello world!", 50 | }) 51 | 52 | const receiver = createReceiver(queue.name!) 53 | 54 | const [message] = await receiver.receiveMessages(1) 55 | expect(message!.sequenceNumber).toEqual(new Long(0, 1)) 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import getPort from "get-port" 2 | import { afterAll, beforeAll } from "vitest" 3 | import { serve } from "lib/api/serve.js" 4 | import type http from "node:http" 5 | import util from "node:util" 6 | import type { AzureServiceBusBroker } from "lib/broker/broker.js" 7 | import { createApiBundle } from "lib/api/index.js" 8 | import { DefaultConfigCertificateStore } from "lib/cert/certificate-store.js" 9 | import { getTestLogger } from "lib/logger/get-test-logger.js" 10 | 11 | let server: http.Server | undefined 12 | let broker: AzureServiceBusBroker | undefined 13 | export let testPort: number | undefined 14 | export let testServiceBusPort: number | undefined 15 | const loggerBundle = getTestLogger("test") 16 | 17 | beforeAll(async () => { 18 | const { amqp_server, bundle } = createApiBundle({ 19 | logger: loggerBundle.logger, 20 | }) 21 | 22 | testPort = await getPort() 23 | server = await serve(bundle, testPort, new DefaultConfigCertificateStore()) 24 | 25 | testServiceBusPort = await getPort() 26 | amqp_server.port = testServiceBusPort 27 | await amqp_server.open() 28 | }) 29 | 30 | afterAll(async () => { 31 | testPort = undefined 32 | 33 | if (broker) { 34 | await broker.close() 35 | } 36 | 37 | if (server) { 38 | await util.promisify(server.close.bind(server))() 39 | } 40 | 41 | await loggerBundle.cleanup() 42 | }) 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowUnreachableCode": false, 5 | "noFallthroughCasesInSwitch": true, 6 | "noImplicitOverride": true, 7 | "noImplicitReturns": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noUncheckedIndexedAccess": true, 10 | 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | 14 | "checkJs": false, 15 | 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "allowSyntheticDefaultImports": true, 19 | 20 | "resolveJsonModule": true, 21 | 22 | "target": "ES2022", 23 | "module": "Node16", 24 | 25 | "paths": { 26 | "edgespec": ["./src/lib/edgespec/index.ts"], 27 | "edgespec/*": ["./src/lib/edgespec/*"], 28 | "test/*": ["./test/*"], 29 | "lib/*": ["./src/lib/*"], 30 | "generated/*": ["./src/generated/*"] 31 | } 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "test/**/*.ts", 36 | "./vitest.config.ts", 37 | "./conf/tsup.*.ts", 38 | "./scripts/**/*.ts" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from "vite" 3 | import tsconfigPaths from "vite-tsconfig-paths" 4 | 5 | export default defineConfig({ 6 | plugins: [tsconfigPaths()], 7 | test: { 8 | setupFiles: ["dotenv/config", "./test/setup.ts"], 9 | exclude: [ 10 | "**/node_modules/**", 11 | "**/dist/**", 12 | "**/cypress/**", 13 | "**/.{idea,git,cache,output,temp}/**", 14 | "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*", 15 | "./src/lib/openapi-zod/**", 16 | ], 17 | }, 18 | }) 19 | --------------------------------------------------------------------------------