Compare commits
336 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d79a10e910 | |||
| 48f0ec0bbe | |||
| b89b124a0e | |||
| d008303068 | |||
|
e328ef5a64
|
|||
| b2506188f3 | |||
| 16ce04ea86 | |||
| 1d0f82a851 | |||
| 7be533dc6c | |||
|
73eae98929
|
|||
| aa41f48b0e | |||
| bb0c020812 | |||
| f0c7415d88 | |||
| fbe180020c | |||
| 3eba214a72 | |||
| 134b571baa | |||
| a1a23d69cf | |||
| 6f1dda7be5 | |||
| d50827018a | |||
| 8a6163a921 | |||
| 054cfa1e52 | |||
| 7d25af472a | |||
| ec0d5dff74 | |||
| e3d19384a0 | |||
| 42adcb1df5 | |||
| e072ad685b | |||
| 4222eb1268 | |||
| ab3eafa331 | |||
| 86612d9e25 | |||
| abc5668d33 | |||
| 892fc29331 | |||
| bdb6e37b22 | |||
| 8d512e0290 | |||
| 3ab0a8e701 | |||
| 3bd3daccaf | |||
| 3d6016d7e2 | |||
| 31cc2e10b6 | |||
| 136623c04b | |||
| 12ff2fa1ba | |||
| 1aef6a7a31 | |||
| c02034d6b0 | |||
| dc4555a168 | |||
| 0571f4d986 | |||
| 3c194dc127 | |||
| f7ba0e3dc7 | |||
| 33667d4de1 | |||
| bbf82399e1 | |||
| f927d1c750 | |||
| ef273520b5 | |||
| c8e6287761 | |||
| 7052f0bfb1 | |||
| c6f35f324e | |||
| 08f0ee3374 | |||
| d3c7941de8 | |||
| b1a9021fee | |||
| ab32fddd2b | |||
| ded73a3065 | |||
| 4c571197b8 | |||
| 9f276d7420 | |||
| f56dfeafa7 | |||
| f6f3810ffb | |||
| 9d32174ae4 | |||
| 26073e744d | |||
| 6e38f3d5b5 | |||
| 9199be9da5 | |||
| e3dcb013b6 | |||
| 5982f3ac41 | |||
| eb23f2f62b | |||
| 1effc3937a | |||
|
b69de655e4
|
|||
| 73fdf6c896 | |||
| bf0953b520 | |||
| 95aaea7ecf | |||
| 81c3a7ff13 | |||
| e322f7f7ad | |||
| 4997c53968 | |||
| cfcd023a83 | |||
| 34e7a0b718 | |||
| 7d38c31370 | |||
| 968553b532 | |||
| 0067b42f92 | |||
| 48776b6afc | |||
| 03cf6be52c | |||
| 6b5e66b1be | |||
| 25c3bf2356 | |||
| 7e5bbf3baa | |||
| 8adae23068 | |||
|
0e8d0965b7
|
|||
| cfc5de1bb9 | |||
| 63567eaa8b | |||
| e38e7a2936 | |||
| 718585ebe8 | |||
| 6b3515ed14 | |||
| fee47b271a | |||
| 1da8439372 | |||
| 2de1324458 | |||
| 4673ecdd85 | |||
| d92f24b0a1 | |||
| f80ee9d391 | |||
| 3da293252d | |||
| 6ae6a4d6cf | |||
|
129cd8aad1
|
|||
| 8dd2e57c70 | |||
| 1914211b85 | |||
| ea8eb0a68c | |||
| e9d0e855af | |||
| 8790fd0d82 | |||
| a77a7f3a32 | |||
| fbe962a7b7 | |||
| b5bdcc9dbc | |||
|
fd1685867e
|
|||
|
114cbf89c5
|
|||
| 000ad8b4ad | |||
| 0820fb542f | |||
| f0d4285bee | |||
| e1c10f0537 | |||
| f8c593de3e | |||
| 870f29e59f | |||
| a246c236db | |||
| cebeba4461 | |||
| b77165f6c8 | |||
| 9e85ee1473 | |||
| c113eb920b | |||
| 90cc64ece9 | |||
| ea4df08beb | |||
|
ca7e063888
|
|||
| 7b9dc1456b | |||
|
49af5f0cb1
|
|||
| e347d74a39 | |||
|
ffcf41b85a
|
|||
| 335a9f3b54 | |||
| c0e790b684 | |||
| 3b47365f10 | |||
|
862060875b
|
|||
| 06aeedc3b0 | |||
| fce85782f0 | |||
| 9cd8218eb4 | |||
| 98ef62b144 | |||
| e0cdd2aa58 | |||
| e22e8b339c | |||
|
6404f7a497
|
|||
| 5dc5043d46 | |||
| bcca005256 | |||
|
a9dea19531
|
|||
|
130e92dc5f
|
|||
| c4112a005f | |||
| 549f6617df | |||
| a1b0f49aab | |||
|
4468903535
|
|||
| df054ca451 | |||
| 1e2236dc9e | |||
|
6ccd7f4f25
|
|||
| b1a46f9d4e | |||
|
47dbf827f2
|
|||
|
df44ddbb8e
|
|||
|
9368d77bc8
|
|||
|
4d18cf4175
|
|||
|
bb0c08be06
|
|||
| a9a47c1690 | |||
| de073ce2da | |||
| af045687ae | |||
| c90ee3c9b1 | |||
| 83e99e7d0a | |||
|
80daed081d
|
|||
| f6e4458efa | |||
| 11b9a46802 | |||
| d2324d27df | |||
| c496ed025e | |||
| 4773ada816 | |||
| 6447a299b3 | |||
| b8e9e0d632 | |||
| 3179bb7ae3 | |||
| 73f6fe31d9 | |||
| 87eab3a04e | |||
| e751d35e38 | |||
| 84e30c0771 | |||
| bdb3b80f4a | |||
| d08c9663fe | |||
| a17b8dd122 | |||
| ae8cf15b0a | |||
| 8fd2c1790b | |||
| fe4efa7b97 | |||
| 78599baa5b | |||
| 8ad47c2e54 | |||
| ecce66b579 | |||
| a26e66649a | |||
| b8b5951883 | |||
| f860d80a81 | |||
| d27331cef0 | |||
|
e343ff7538
|
|||
| 4fe45bf125 | |||
| 89e35c4ee7 | |||
| 0f077a53bb | |||
| 51fd889f6a | |||
| e13c9b1a28 | |||
| 576a530886 | |||
| aa9fea580f | |||
| ccb8a10f92 | |||
| d86beb8308 | |||
| 429cf6e66d | |||
| 2dd0ac9392 | |||
| 780244b165 | |||
|
540e90a577
|
|||
| 1b527bab74 | |||
| 5b64cd0165 | |||
| e2fb56f505 | |||
| 6bdbe36c7f | |||
| 2a1415bc35 | |||
| 8e53b1fbf4 | |||
|
2cf992c948
|
|||
| ca9e7c3aa0 | |||
| db762cb496 | |||
| c33885a0ab | |||
| 1135d77c35 | |||
| d1d14c1097 | |||
| 41075e06a3 | |||
| a7d4c01089 | |||
| 7c30a66144 | |||
| 98c17196c8 | |||
| 6635019035 | |||
| 1ae73e7203 | |||
| 91c02eb499 | |||
| 32fc6b2641 | |||
| ce86af4486 | |||
| 5258a68682 | |||
| 1d69be641c | |||
| 3c571a1dc3 | |||
| 4c16e293c0 | |||
| 18fb8da472 | |||
| c9fa0ecc2c | |||
| 35db2fc74e | |||
| e10ab9d75f | |||
| d6100bcb76 | |||
| 47d122aa8d | |||
| b089801216 | |||
| 3c071cb300 | |||
| 8fe56f6ce0 | |||
| c9aa447a9f | |||
| 575f97e8d1 | |||
| dde2a965ec | |||
| b6ae9826b2 | |||
| 787f388168 | |||
| c4b01dbe07 | |||
| 992a95ff8d | |||
| 0c31b8c2b9 | |||
| b75b61f724 | |||
| 0e76377865 | |||
|
52e58f8df8
|
|||
| 35ba7679c9 | |||
| 9d5dc75c2b | |||
| 83cb25a2bb | |||
|
e7a09e8322
|
|||
| 2eae446669 | |||
| 2e86f80727 | |||
| 1311a07dcb | |||
| cd080189a9 | |||
| 2d86c77c39 | |||
| eea7f86bbe | |||
| baccd12e63 | |||
| 7fe90ee9af | |||
| a3b9cae8eb | |||
| 769cbd3f14 | |||
| ecc4da28ff | |||
| 4a14f41324 | |||
| 3f8fdce292 | |||
| 335c419a44 | |||
| 82d2aa1812 | |||
| 24bbb39c3d | |||
| d98a20afff | |||
| 737b133b8b | |||
| 177f923fc2 | |||
| fd0c89dce9 | |||
| 1e58f8e0d5 | |||
| 53349071aa | |||
| efe9e0a5a0 | |||
| 406cb8e4d0 | |||
|
6e7ee0110b
|
|||
| 6eac3a7796 | |||
| 890a6fd50e | |||
| e1cf0d8cb3 | |||
| 63d70d7e35 | |||
| 35a454f8b1 | |||
| 6651553246 | |||
| 311ef3f530 | |||
| d8cce2fb05 | |||
| 309b9423a4 | |||
| 323145a076 | |||
| 28b4dc5572 | |||
| c1173c20ae | |||
| 2d12077016 | |||
| c38db83cd1 | |||
| 25bd438d05 | |||
| f4355d620f | |||
| 887ba69c5e | |||
| 8e6c7aade7 | |||
| 0f0f50111f | |||
| 81a895e5a6 | |||
|
0fab1d5098
|
|||
| 4fbfc0f42e | |||
| 0ba3706c12 | |||
| c44ac87b5e | |||
| deefcd7045 | |||
| 4933857351 | |||
| 18616e8346 | |||
| dd075afb8d | |||
| 003bd3cd50 | |||
| a68fb437dc | |||
| 8aa57e8f68 | |||
| 84e8764054 | |||
| c40bbad892 | |||
| c159c6d20e | |||
| ae0d796e93 | |||
| 1ec0f9a3a7 | |||
| 0dad651959 | |||
| 24fcc4e6a2 | |||
| 4c7406d97b | |||
| 51affc5a55 | |||
| 10871a9b32 | |||
| 411b51f895 | |||
| 6a478209ea | |||
| 043ca65698 | |||
| 8131042a1c | |||
| 2fb2c1947a | |||
| 1cf4faa17f | |||
| 8bb6cb7279 | |||
|
376ae41b4f
|
|||
| dc6e57e815 | |||
| f8c7de447a | |||
| 92050aa31f | |||
| e20829bb2b | |||
| 1918ec3da4 | |||
| 735c387c58 | |||
| 98e2f660a6 | |||
| 7a0159a33f | |||
|
b4447bb15e
|
|||
|
2948905005
|
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
/.gitlab
|
/.gitea
|
||||||
.gitlab-ci.yml
|
|
||||||
.graphqlconfig
|
.graphqlconfig
|
||||||
/exported
|
/exported
|
||||||
/k8s
|
/k8s
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
name: schemas
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
deploy_prod:
|
||||||
|
description: 'Deploy to production'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Generate and format check
|
||||||
|
run: |
|
||||||
|
go install mvdan.cc/gofumpt@latest
|
||||||
|
go install golang.org/x/tools/cmd/goimports@latest
|
||||||
|
go generate ./...
|
||||||
|
git diff --stat --exit-code
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -race -coverprofile=coverage.txt ./...
|
||||||
|
|
||||||
|
vulnerabilities:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Check vulnerabilities
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
govulncheck ./...
|
||||||
|
|
||||||
|
check-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Check goreleaser config
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
version: '~> v2'
|
||||||
|
args: check
|
||||||
|
- name: Test release build
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
version: '~> v2'
|
||||||
|
args: release --snapshot --clean
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: [check, vulnerabilities, check-release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_CONTENT }}
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: buildtool/setup-buildtools-action@v1
|
||||||
|
- name: Build and push
|
||||||
|
run: unset GITEA_TOKEN && build && push
|
||||||
|
|
||||||
|
deploy-prod:
|
||||||
|
needs: build
|
||||||
|
if: gitea.ref == 'refs/heads/main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_CONTENT }}
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
environment: prod
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: buildtool/setup-buildtools-action@v1
|
||||||
|
- name: Deploy to production
|
||||||
|
run: deploy prod
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
RELEASE_TOKEN_FILE: /runner-secrets/release-token
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Install goreleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v6
|
||||||
|
with:
|
||||||
|
version: '~> v2'
|
||||||
|
install-only: true
|
||||||
|
- name: Release
|
||||||
|
run: |
|
||||||
|
GITEA_TOKEN=$(cat "${RELEASE_TOKEN_FILE}")
|
||||||
|
export GITEA_TOKEN
|
||||||
|
goreleaser release --clean
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: pre-commit
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SKIP: no-commit-to-branch
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.14'
|
||||||
|
- name: Install goimports
|
||||||
|
run: go install golang.org/x/tools/cmd/goimports@latest
|
||||||
|
- uses: pre-commit/action@v3.0.1
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
uses: unboundsoftware/shared-workflows/.gitea/workflows/Release.yml@main
|
||||||
|
with:
|
||||||
|
tag_only: true
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
.idea
|
.idea
|
||||||
|
.claude
|
||||||
.testCoverage.txt
|
.testCoverage.txt
|
||||||
.testCoverage.txt.tmp
|
.testCoverage.txt.tmp
|
||||||
coverage.html
|
coverage.html
|
||||||
|
coverage.out
|
||||||
/exported
|
/exported
|
||||||
/release
|
/release
|
||||||
/schemactl
|
/schemactl
|
||||||
|
/service
|
||||||
CHANGES.md
|
CHANGES.md
|
||||||
VERSION
|
VERSION
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
include:
|
|
||||||
- template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'
|
|
||||||
- project: unboundsoftware/ci-templates
|
|
||||||
file: Defaults.gitlab-ci.yml
|
|
||||||
- project: unboundsoftware/ci-templates
|
|
||||||
file: Release.gitlab-ci.yml
|
|
||||||
- project: unboundsoftware/ci-templates
|
|
||||||
file: Pre-Commit-Go.gitlab-ci.yml
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- build
|
|
||||||
- test
|
|
||||||
- deploy-prod
|
|
||||||
- release
|
|
||||||
|
|
||||||
variables:
|
|
||||||
UNBOUND_RELEASE_TAG_ONLY: true
|
|
||||||
|
|
||||||
.buildtools:
|
|
||||||
image: buildtool/build-tools:${BUILDTOOLS_VERSION}
|
|
||||||
|
|
||||||
check:
|
|
||||||
stage: .pre
|
|
||||||
image: amd64/golang:1.24.4@sha256:3494bbe140127d12656113203ec91b8e3ff34e8a2b06a0a22bb0d8a41cc69e53
|
|
||||||
script:
|
|
||||||
- go install mvdan.cc/gofumpt@latest
|
|
||||||
- go install golang.org/x/tools/cmd/goimports@latest
|
|
||||||
- go generate ./...
|
|
||||||
- git diff --stat --exit-code
|
|
||||||
|
|
||||||
build:
|
|
||||||
extends: .buildtools
|
|
||||||
stage: build
|
|
||||||
script:
|
|
||||||
- build
|
|
||||||
- curl -Os https://uploader.codecov.io/latest/linux/codecov
|
|
||||||
- chmod +x codecov
|
|
||||||
- ./codecov -t ${CODECOV_TOKEN} -R $CI_PROJECT_DIR -C $CI_COMMIT_SHA -r $CI_PROJECT_PATH
|
|
||||||
- push
|
|
||||||
|
|
||||||
vulnerabilities:
|
|
||||||
stage: build
|
|
||||||
image: amd64/golang:1.24.4@sha256:3494bbe140127d12656113203ec91b8e3ff34e8a2b06a0a22bb0d8a41cc69e53
|
|
||||||
script:
|
|
||||||
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
||||||
- govulncheck ./...
|
|
||||||
|
|
||||||
deploy-prod:
|
|
||||||
extends: .buildtools
|
|
||||||
stage: deploy-prod
|
|
||||||
before_script:
|
|
||||||
- echo Deploy to prod
|
|
||||||
script:
|
|
||||||
- deploy prod
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == "main"
|
|
||||||
environment:
|
|
||||||
name: prod
|
|
||||||
resource_group: prod
|
|
||||||
|
|
||||||
check_release:
|
|
||||||
stage: test
|
|
||||||
image:
|
|
||||||
name: goreleaser/goreleaser:v2.10.2@sha256:ebc884882268c835edd70c628dbdf830481ae23a569b343cd7d7b178c91573ac
|
|
||||||
entrypoint: [ '' ]
|
|
||||||
variables:
|
|
||||||
GOTOOLCHAIN: auto
|
|
||||||
script: |
|
|
||||||
goreleaser check
|
|
||||||
goreleaser release --snapshot --clean
|
|
||||||
|
|
||||||
release:
|
|
||||||
stage: release
|
|
||||||
needs:
|
|
||||||
- unbound_release_prepare_release
|
|
||||||
image:
|
|
||||||
name: goreleaser/goreleaser:v2.10.2@sha256:ebc884882268c835edd70c628dbdf830481ae23a569b343cd7d7b178c91573ac
|
|
||||||
entrypoint: [ '' ]
|
|
||||||
variables:
|
|
||||||
# Disable shallow cloning so that goreleaser can diff between tags to
|
|
||||||
# generate a changelog.
|
|
||||||
GIT_DEPTH: 0
|
|
||||||
GITLAB_TOKEN: $GITLAB_CI_TOKEN
|
|
||||||
GOTOOLCHAIN: auto
|
|
||||||
# Only run this release job for tags, not every commit (for example).
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
script: |
|
|
||||||
goreleaser release --clean --release-notes=CHANGES.md
|
|
||||||
+5
-8
@@ -1,6 +1,10 @@
|
|||||||
project_name: unbound-schemas
|
project_name: unbound-schemas
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
|
gitea_urls:
|
||||||
|
api: http://gitea-http.gitea.svc.cluster.local:3000/api/v1
|
||||||
|
download: https://gitea.unbound.se
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
|
|
||||||
@@ -21,19 +25,12 @@ homebrew_casks:
|
|||||||
repository:
|
repository:
|
||||||
owner: unboundsoftware
|
owner: unboundsoftware
|
||||||
name: homebrew-taps
|
name: homebrew-taps
|
||||||
binary: schemactl
|
binaries: [schemactl]
|
||||||
directory: Casks
|
directory: Casks
|
||||||
conflicts:
|
|
||||||
- formula: unbound-schemas
|
|
||||||
commit_author:
|
commit_author:
|
||||||
name: "Joakim Olsson"
|
name: "Joakim Olsson"
|
||||||
email: joakim@unbound.se
|
email: joakim@unbound.se
|
||||||
homepage: "https://schemas.unbound.se/"
|
homepage: "https://schemas.unbound.se/"
|
||||||
hooks:
|
|
||||||
post:
|
|
||||||
install: |
|
|
||||||
# replace foo with the actual binary name
|
|
||||||
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/schemactl"]
|
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: unbound-schemas
|
- id: unbound-schemas
|
||||||
|
|||||||
+6
-13
@@ -2,7 +2,7 @@
|
|||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v5.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
@@ -10,15 +10,8 @@ repos:
|
|||||||
args:
|
args:
|
||||||
- --allow-multiple-documents
|
- --allow-multiple-documents
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- repo: https://gitlab.com/devopshq/gitlab-ci-linter
|
|
||||||
rev: v1.0.6
|
|
||||||
hooks:
|
|
||||||
- id: gitlab-ci-linter
|
|
||||||
args:
|
|
||||||
- --project
|
|
||||||
- unboundsoftware/schemas
|
|
||||||
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||||
rev: v9.22.0
|
rev: v9.24.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: commitlint
|
- id: commitlint
|
||||||
stages: [ commit-msg ]
|
stages: [ commit-msg ]
|
||||||
@@ -30,18 +23,18 @@ repos:
|
|||||||
- id: go-imports
|
- id: go-imports
|
||||||
args:
|
args:
|
||||||
- -local
|
- -local
|
||||||
- gitlab.com/unboundsoftware/schemas
|
- git.unbound.se/unboundsoftware/schemas
|
||||||
- repo: https://github.com/lietu/go-pre-commit
|
- repo: https://github.com/lietu/go-pre-commit
|
||||||
rev: v0.1.0
|
rev: v1.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: go-test
|
- id: go-test
|
||||||
- id: gofumpt
|
- id: gofumpt
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.1.6
|
rev: v2.8.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-full
|
- id: golangci-lint-full
|
||||||
- repo: https://github.com/gitleaks/gitleaks
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
rev: v8.27.2
|
rev: v8.30.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: gitleaks
|
- id: gitleaks
|
||||||
exclude: '^ctl/generated.go|graph/generated/.*$|^graph/model/models_gen.go|^tools/.*$$'
|
exclude: '^ctl/generated.go|graph/generated/.*$|^graph/model/models_gen.go|^tools/.*$$'
|
||||||
|
|||||||
+570
@@ -2,6 +2,275 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.9.1] - 2026-01-18
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(ci)* Run build job on tags for Docker images
|
||||||
|
|
||||||
|
## [0.9.0] - 2026-01-18
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add commands for managing organizations and users
|
||||||
|
- Migrate from GitLab CI to Gitea Actions
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.239
|
||||||
|
- *(k8s)* Update ingress class configuration for schema
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.84
|
||||||
|
- *(docker)* Update Node.js version to 24.11.1-alpine
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.240
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo
|
||||||
|
- *(deps)* Update module golang.org/x/crypto to v0.46.0
|
||||||
|
- *(deps)* Update module go.opentelemetry.io/contrib/bridges/otelslog to v0.14.0
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.241
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.85
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.242
|
||||||
|
- *(deps)* Update module golang.org/x/crypto to v0.47.0
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.86
|
||||||
|
|
||||||
|
### 🚜 Refactor
|
||||||
|
|
||||||
|
- *(cache)* Optimize test setup and reduce iterations
|
||||||
|
|
||||||
|
### 🧪 Testing
|
||||||
|
|
||||||
|
- Add validation and event tests for organization and API key
|
||||||
|
- *(cache)* Update tests to use legacy hash for speed
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.30.0
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.13.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.5
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.1
|
||||||
|
- *(deps)* Update node.js to 682368d
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.2
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.13.1
|
||||||
|
- *(deps)* Update golang:1.25.5 docker digest to 0c27bcf
|
||||||
|
- *(deps)* Update node.js to v24.12.0
|
||||||
|
- *(deps)* Update node.js to c921b97
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.13.2
|
||||||
|
- *(deps)* Update golang:1.25.5 docker digest to ad03ba9
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.8.0
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.13.3
|
||||||
|
- *(deps)* Update golang:1.25.5 docker digest to 3a01526
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.24.0
|
||||||
|
- *(deps)* Update node.js to v24.13.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.6
|
||||||
|
|
||||||
|
## [0.8.0] - 2025-11-21
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(tests)* Add unit tests for WebSocket initialization logic
|
||||||
|
- Add latestSchema query for retrieving schema updates
|
||||||
|
- Add CLAUDE.md for project documentation and guidelines
|
||||||
|
- *(cache)* Implement hashed API key storage and retrieval
|
||||||
|
- *(health)* Add health checking endpoints and logic
|
||||||
|
- *(cache)* Add concurrency safety and logging improvements
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Enhance API key handling and logging in middleware
|
||||||
|
- Add command executor interface for better testing
|
||||||
|
- *(deps)* Update module golang.org/x/crypto to v0.45.0
|
||||||
|
- *(deps)* Update module github.com/auth0/go-jwt-middleware/v2 to v2.3.1
|
||||||
|
|
||||||
|
### 🧪 Testing
|
||||||
|
|
||||||
|
- Enhance assertions for version and subscription config
|
||||||
|
- *(cache)* Reduce goroutines for race detector stability
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.29.1
|
||||||
|
- *(deps)* Update node.js to v24
|
||||||
|
|
||||||
|
## [0.7.0] - 2025-11-19
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add Cosmo Router config generation and PubSub support
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.231
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.232
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.233
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.234
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.235
|
||||||
|
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.31
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.236
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.82
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.237
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.83
|
||||||
|
- *(deps)* Update module github.com/alecthomas/kong to v1.13.0
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.238
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Add git-cliff configuration for changelog generation
|
||||||
|
- *(deps)* Update golang:1.25.3 docker digest to 69d1009
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.6
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.7
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.0
|
||||||
|
- *(deps)* Update golang:1.25.3 docker digest to 9ac0edc
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.29.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.4
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.2
|
||||||
|
- *(deps)* Update golang:1.25.4 docker digest to efe81fa
|
||||||
|
|
||||||
|
## [0.6.6] - 2025-10-14
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.227
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.228
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.229
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.81
|
||||||
|
- *(deps)* Update module github.com/pressly/goose/v3 to v3.26.0
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.230
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.5.0
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.3
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.4
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.5
|
||||||
|
- *(deps)* Update golang:1.25.1 docker digest to 12640a4
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.23.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.2
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.3
|
||||||
|
|
||||||
|
## [0.6.5] - 2025-09-18
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.226
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.79
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.80
|
||||||
|
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/eventsourced to v1.19.3
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook lietu/go-pre-commit to v1
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.1
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.2
|
||||||
|
|
||||||
|
## [0.6.4] - 2025-09-11
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/amqp to v1.9.0
|
||||||
|
|
||||||
|
## [0.6.3] - 2025-09-09
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/stretchr/testify to v1.11.0
|
||||||
|
- *(deps)* Update module github.com/pressly/goose/v3 to v3.25.0
|
||||||
|
- *(deps)* Update module github.com/stretchr/testify to v1.11.1
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.222
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.223
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.224
|
||||||
|
- *(deps)* Update module go.opentelemetry.io/contrib/bridges/otelslog to v0.13.0
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.225
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update golang:1.25.0 docker digest to f6b9e1a
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.1
|
||||||
|
- *(deps)* Update golang:1.25.1 docker digest to 53f7808
|
||||||
|
|
||||||
|
## [0.6.2] - 2025-08-22
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Remove conflicts entry from homebrew-taps config
|
||||||
|
|
||||||
|
## [0.6.1] - 2025-08-22
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.195
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.196
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.197
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.198
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.199
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.200
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.202
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.203
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.204
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.207
|
||||||
|
- *(deps)* Update module github.com/golang-jwt/jwt/v5 to v5.2.3
|
||||||
|
- *(deps)* Update module github.com/alecthomas/kong to v1.12.1
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.208
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.210
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.78
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.212
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.213
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.215
|
||||||
|
- *(deps)* Update module github.com/golang-jwt/jwt/v5 to v5.3.0
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.216
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.217
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.218
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.219
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.220
|
||||||
|
- *(deps)* Update module github.com/sparetimecoders/goamqp to v0.3.3
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.221
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.1
|
||||||
|
- *(deps)* Update golang:1.24.4 docker digest to 9f820b6
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.5
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.11.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.2
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.28.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.3.0
|
||||||
|
- *(deps)* Update golang:1.24.5 docker digest to 0a156a4
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.11.1
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.11.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.3.1
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.6
|
||||||
|
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v6
|
||||||
|
- *(deps)* Update golang:1.24.6 docker digest to 958bfd1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.4.0
|
||||||
|
- *(deps)* Update golang:1.24.6 docker digest to cd8f653
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.0
|
||||||
|
|
||||||
|
## [0.6.0] - 2025-06-29
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- *(k8s)* Add OpenTelemetry exporter endpoint to deploy.yaml
|
||||||
|
- Add build version injection via CI_COMMIT argument
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.190
|
||||||
|
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.28
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.75
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.192
|
||||||
|
- *(deps)* Update module github.com/alecthomas/kong to v1.12.0
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo
|
||||||
|
- *(deps)* Update module go.opentelemetry.io/contrib/bridges/otelslog to v0.12.0
|
||||||
|
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.29
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.194
|
||||||
|
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.30
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.76
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.0
|
||||||
|
|
||||||
## [0.5.3] - 2025-06-13
|
## [0.5.3] - 2025-06-13
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
@@ -14,6 +283,10 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- Remove Sentry integration and replace with OpenTelemetry
|
- Remove Sentry integration and replace with OpenTelemetry
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update golang:1.24.4 docker digest to 3494bbe
|
||||||
|
|
||||||
## [0.5.2] - 2025-06-09
|
## [0.5.2] - 2025-06-09
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
@@ -37,6 +310,15 @@ All notable changes to this project will be documented in this file.
|
|||||||
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.185
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.185
|
||||||
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.186
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.186
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update golang:1.24.3 docker digest to f255a7d
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.4
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.2
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.10.2
|
||||||
|
|
||||||
## [0.5.0] - 2025-05-15
|
## [0.5.0] - 2025-05-15
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
@@ -59,7 +341,16 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.4
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.5
|
||||||
|
- *(deps)* Update golang:1.24.2 docker digest to bf7899c
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.25.0
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.9.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.25.1
|
||||||
- *(ci)* Update GitLab CI configuration for templates
|
- *(ci)* Update GitLab CI configuration for templates
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.6
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.3
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.26.0
|
||||||
|
|
||||||
## [0.4.1] - 2025-04-24
|
## [0.4.1] - 2025-04-24
|
||||||
|
|
||||||
@@ -72,6 +363,11 @@ All notable changes to this project will be documented in this file.
|
|||||||
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.171
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.171
|
||||||
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.172
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.172
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.2
|
||||||
|
|
||||||
## [0.4.0] - 2025-04-12
|
## [0.4.0] - 2025-04-12
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
@@ -91,6 +387,10 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- *(deploy)* Remove cpu and memory limits for schemas
|
- *(deploy)* Remove cpu and memory limits for schemas
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.24.3
|
||||||
|
|
||||||
## [0.3.0] - 2025-04-08
|
## [0.3.0] - 2025-04-08
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
@@ -122,6 +422,24 @@ All notable changes to this project will be documented in this file.
|
|||||||
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.168
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.168
|
||||||
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.169
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.169
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.6
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.1
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.22.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.7
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.8.0
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.8.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.8
|
||||||
|
- *(deps)* Update golang:1.24.1 docker digest to 5ecf333
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.24.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.0.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.0.2
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.8.2
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.2
|
||||||
|
- *(deps)* Update golang:1.24.2 docker digest to aebb7df
|
||||||
|
|
||||||
## [0.2.0] - 2025-02-28
|
## [0.2.0] - 2025-02-28
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
@@ -146,7 +464,24 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.3
|
||||||
|
- *(deps)* Update golang:1.23.5 docker digest to e213430
|
||||||
|
- *(deps)* Update dependency go to v1.23.6
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.6
|
||||||
|
- *(deps)* Update golang:1.23.6 docker digest to 958bd2e
|
||||||
|
- *(deps)* Update golang:1.23.6 docker digest to 9271129
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.7.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.3
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.4
|
||||||
- *(go)* Update go version to 1.23.6 and remove toolchain
|
- *(go)* Update go version to 1.23.6 and remove toolchain
|
||||||
|
- *(deps)* Update golang docker tag to v1.24.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.5
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.21.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.24.0
|
||||||
|
- *(deps)* Update golang:1.24.0 docker digest
|
||||||
|
- *(deps)* Update golang:1.24.0 docker digest to a14c5a6
|
||||||
|
- *(deps)* Update golang:1.24.0 docker digest to 58cf31c
|
||||||
- *(docker)* Update base image architecture to amd64
|
- *(docker)* Update base image architecture to amd64
|
||||||
|
|
||||||
## [0.1.1] - 2025-01-24
|
## [0.1.1] - 2025-01-24
|
||||||
@@ -160,6 +495,27 @@ All notable changes to this project will be documented in this file.
|
|||||||
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.63
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.63
|
||||||
- *(k8s)* Standardize app label to app.kubernetes.io/name
|
- *(k8s)* Standardize app label to app.kubernetes.io/name
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.2
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.5.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.3
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.4
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.0
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 3b1a7de
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 08e1417
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 585103a
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.1
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 5305905
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 9820aca
|
||||||
|
- *(deps)* Update dependency go to v1.23.5
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.5
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.6.0
|
||||||
|
- *(deps)* Update golang:1.23.5 docker digest to 8c10f21
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.6.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.2
|
||||||
|
|
||||||
## [0.1.0] - 2025-01-01
|
## [0.1.0] - 2025-01-01
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
@@ -200,8 +556,47 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.2
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.1
|
||||||
|
- *(deps)* Pin dependencies
|
||||||
|
- *(deps)* Pin dependencies
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.2
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.4
|
||||||
|
- *(deps)* Update dependency go to v1.23.3
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.3
|
||||||
|
- *(deps)* Update unbound/pre-commit docker digest to 596abf5
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.62.0
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 8956c08
|
||||||
|
- *(deps)* Update unbound/pre-commit docker digest to e78425c
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 3694e36
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to b2ca381
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 2660218
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to c2d828f
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.5
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 73f06be
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.6
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.7
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.8
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.62.2
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.19.0
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to ee5f0ad
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to b4aabba
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 2b01164
|
||||||
|
- *(deps)* Update golang:1.23.3 docker digest to 017ec6b
|
||||||
|
- *(deps)* Update dependency go to v1.23.4
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.4
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 574185e
|
||||||
- Remove unnecessary Docker variables from configuration
|
- Remove unnecessary Docker variables from configuration
|
||||||
- *(ci)* Remove unused docker service from buildtools
|
- *(ci)* Remove unused docker service from buildtools
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 7003184
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.5.0
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.20.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.3
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.4
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.22.0
|
||||||
|
- *(deps)* Update golang:1.23.4 docker digest to 7ea4c9d
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.22.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.0
|
||||||
|
|
||||||
## [0.0.7] - 2024-10-22
|
## [0.0.7] - 2024-10-22
|
||||||
|
|
||||||
@@ -246,9 +641,49 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v4.6.0
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.15.0
|
||||||
|
- *(deps)* Update dependency go to v1.22.2
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.16.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.58.0
|
||||||
|
- *(deps)* Update dependency go to v1.22.3
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.3
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.58.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.58.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.59.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.18.3
|
||||||
|
- *(deps)* Update dependency go to v1.22.4
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.4
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.59.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.18.4
|
||||||
|
- *(deps)* Update dependency go to v1.22.5
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.5
|
||||||
|
- *(deps)* Update dependency go to v1.22.6
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.6
|
||||||
|
- *(deps)* Update dependency go to v1.23.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.60.1
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.0
|
||||||
- Update golangci-lint hook identifier in pre-commit config
|
- Update golangci-lint hook identifier in pre-commit config
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.60.2
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.60.3
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.17.0
|
||||||
|
- *(deps)* Update dependency go to v1.23.1
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.61.0
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.18.0
|
||||||
- Update goreleaser image to v2.3.1
|
- Update goreleaser image to v2.3.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.19.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.19.2
|
||||||
- Add generate check
|
- Add generate check
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.3.2
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.19.3
|
||||||
|
- *(deps)* Update dependency go to v1.23.2
|
||||||
|
- *(deps)* Update golang docker tag to v1.23.2
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.20.0
|
||||||
|
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v5
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.20.1
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.0
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.1
|
||||||
- Add release notes for goreleaser command in .gitlab-ci.yml
|
- Add release notes for goreleaser command in .gitlab-ci.yml
|
||||||
|
|
||||||
## [0.0.6] - 2024-04-04
|
## [0.0.6] - 2024-04-04
|
||||||
@@ -260,6 +695,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
### ⚙️ Miscellaneous Tasks
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
- Add step for checking release
|
- Add step for checking release
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.2
|
||||||
|
|
||||||
## [0.0.5] - 2024-04-03
|
## [0.0.5] - 2024-04-03
|
||||||
|
|
||||||
@@ -284,15 +720,106 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.8.3 to 1.8.4
|
||||||
- Update golangci-lint
|
- Update golangci-lint
|
||||||
|
- *(deps)* Bump golang from 1.20.4 to 1.20.5
|
||||||
- Update Go version for vulnerabilities
|
- Update Go version for vulnerabilities
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.31 to 0.17.32
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump github.com/vektah/gqlparser/v2
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.32 to 0.17.33
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.21.0 to 0.22.0
|
||||||
|
- *(deps)* Bump github.com/alecthomas/kong from 0.7.1 to 0.8.0
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.33 to 0.17.34
|
||||||
|
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.3 to 2.5.5
|
||||||
|
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.5 to 2.5.6
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.11.2 to 3.13.0
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.4 to 0.1.5
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.13.0 to 3.13.1
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.13.1 to 3.13.4
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump golang from 1.20.5 to 1.20.6
|
||||||
|
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.6 to 2.5.7
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.34 to 0.17.35
|
||||||
|
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.7 to 2.5.8
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.13.4 to 3.14.0
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.35 to 0.17.36
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.22.0 to 0.23.0
|
||||||
|
- *(deps)* Bump golang from 1.20.6 to 1.20.7
|
||||||
- Update to Go 1.20.7
|
- Update to Go 1.20.7
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump golang from 1.20.7 to 1.21.0
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.14.0 to 3.15.0
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
- Update to Golang 1.21.0 for vulnerabilities
|
- Update to Golang 1.21.0 for vulnerabilities
|
||||||
- Update pre-commit versions
|
- Update pre-commit versions
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.23.0 to 0.24.0
|
||||||
|
- *(deps)* Bump github.com/rs/cors from 1.9.0 to 1.10.0
|
||||||
|
- *(deps)* Bump golang from 1.21.0 to 1.21.1
|
||||||
- Update to Go 1.21.1 for vulnerabilities
|
- Update to Go 1.21.1 for vulnerabilities
|
||||||
|
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.8 to 2.5.9
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.36 to 0.17.37
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.24.0 to 0.24.1
|
||||||
|
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.9 to 2.5.10
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.37 to 0.17.38
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump github.com/rs/cors from 1.10.0 to 1.10.1
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.24.1 to 0.25.0
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.5 to 0.2.0
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.38 to 0.17.39
|
||||||
|
- *(deps)* Bump golang from 1.21.1 to 1.21.2
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.15.0 to 3.15.1
|
||||||
|
- *(deps)* Bump golang from 1.21.2 to 1.21.3
|
||||||
|
- *(deps)* Bump github.com/alecthomas/kong from 0.8.0 to 0.8.1
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.39 to 0.17.40
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
|
- *(deps)* Bump golang from 1.21.3 to 1.21.4
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.15.1 to 3.16.0
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.40 to 0.17.41
|
||||||
|
- *(deps)* Bump github.com/auth0/go-jwt-middleware/v2
|
||||||
|
- *(deps)* Bump golang from 1.21.4 to 1.21.5
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.16.0 to 3.17.0
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.2.0 to 0.2.1
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.41 to 0.17.42
|
||||||
|
- *(deps)* Bump golang from 1.21.5 to 1.21.6
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.25.0 to 0.26.0
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.2.1 to 0.3.0
|
||||||
|
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.10 to 2.5.11
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.42 to 0.17.43
|
||||||
|
- *(deps)* Bump github.com/auth0/go-jwt-middleware/v2
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.17.0 to 3.18.0
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
|
||||||
|
- *(deps)* Bump golang from 1.21.6 to 1.22.0
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.26.0 to 0.27.0
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.43 to 0.17.44
|
||||||
|
- *(deps)* Update pre-commit hook devopshq/gitlab-ci-linter to v1.0.6
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.11.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.56.2
|
||||||
|
- *(deps)* Update pre-commit hook lietu/go-pre-commit to v0.1.0
|
||||||
|
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v4.5.0
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.12.0
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.13.0
|
||||||
- Use OrbStack for local dev
|
- Use OrbStack for local dev
|
||||||
|
- *(deps)* Update golang docker tag to v1.22.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.57.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.57.1
|
||||||
- Add gitleaks to pre-commit setup
|
- Add gitleaks to pre-commit setup
|
||||||
- Update resources
|
- Update resources
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.57.2
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.14.0
|
||||||
- Run release on medium instance
|
- Run release on medium instance
|
||||||
- Back to small and upgrade goreleaser
|
- Back to small and upgrade goreleaser
|
||||||
- Remove deprecated replacements
|
- Remove deprecated replacements
|
||||||
@@ -305,8 +832,18 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### ⚙️ Miscellaneous Tasks
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Bump golang from 1.20.3 to 1.20.4
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.10.0 to 3.11.0
|
||||||
- Update Go version for vulnerabilities
|
- Update Go version for vulnerabilities
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.30 to 0.17.31
|
||||||
|
- *(deps)* Bump github.com/Khan/genqlient from 0.5.0 to 0.6.0
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.20.0 to 0.21.0
|
||||||
|
- *(deps)* Bump github.com/pressly/goose/v3 from 3.11.0 to 3.11.2
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
|
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.3 to 0.1.4
|
||||||
|
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
|
||||||
- Update pre-commit and fix golangci-lint
|
- Update pre-commit and fix golangci-lint
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.8.2 to 1.8.3
|
||||||
- Actually validate API key privileges and refs
|
- Actually validate API key privileges and refs
|
||||||
|
|
||||||
## [0.0.3] - 2023-04-27
|
## [0.0.3] - 2023-04-27
|
||||||
@@ -325,19 +862,52 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
- Update schema if URLs have changed
|
- Update schema if URLs have changed
|
||||||
- Add pre-commit and remove those checks from Dockerfile
|
- Add pre-commit and remove those checks from Dockerfile
|
||||||
|
- *(deps)* Bump github.com/alecthomas/kong from 0.6.1 to 0.7.1
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.8.0 to 1.8.1
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.20 to 0.17.22
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.14.0 to 0.16.0
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/amqp
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
- Add context and error handling
|
- Add context and error handling
|
||||||
|
- *(deps)* Bump github.com/rs/cors from 1.8.2 to 1.8.3
|
||||||
- Move to default ingress group
|
- Move to default ingress group
|
||||||
- Decrease trace sample rate
|
- Decrease trace sample rate
|
||||||
- Improve docker caching
|
- Improve docker caching
|
||||||
|
- *(deps)* Bump golang from 1.19.4 to 1.19.5
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.16.0 to 0.17.0
|
||||||
- Add local module to pre-commit config
|
- Add local module to pre-commit config
|
||||||
- Only ignore generated files with do not edit
|
- Only ignore generated files with do not edit
|
||||||
- Default ingress group
|
- Default ingress group
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.22 to 0.17.24
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
|
||||||
|
- *(deps)* Bump golang from 1.19.5 to 1.20.0
|
||||||
- Use Docker DinD version from variable
|
- Use Docker DinD version from variable
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.17.0 to 0.18.0
|
||||||
- Switch to manual rebases for Dependabot
|
- Switch to manual rebases for Dependabot
|
||||||
|
- *(deps)* Bump golang from 1.20.0 to 1.20.1
|
||||||
- Update to golang 1.20.1
|
- Update to golang 1.20.1
|
||||||
|
- *(deps)* Bump github.com/stretchr/testify from 1.8.1 to 1.8.2
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.24 to 0.17.25
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.18.0 to 0.19.0
|
||||||
|
- *(deps)* Bump golang from 1.20.1 to 1.20.2
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.25 to 0.17.26
|
||||||
- Update Go verion for vulnerabilities scan
|
- Update Go verion for vulnerabilities scan
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.26 to 0.17.27
|
||||||
- Reduce sample rate
|
- Reduce sample rate
|
||||||
|
- *(deps)* Bump github.com/getsentry/sentry-go from 0.19.0 to 0.20.0
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.27 to 0.17.28
|
||||||
|
- *(deps)* Bump golang from 1.20.2 to 1.20.3
|
||||||
- Update to Go 1.20.3
|
- Update to Go 1.20.3
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.28 to 0.17.29
|
||||||
|
- *(deps)* Bump github.com/rs/cors from 1.8.3 to 1.9.0
|
||||||
|
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
|
||||||
|
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.29 to 0.17.30
|
||||||
- Fix Gitlab CI lint
|
- Fix Gitlab CI lint
|
||||||
|
|
||||||
## [0.0.2] - 2022-10-14
|
## [0.0.2] - 2022-10-14
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a GraphQL schema registry service that manages federated GraphQL schemas for microservices. It allows services to publish their subgraph schemas and provides merged supergraphs with Cosmo Router configuration for federated GraphQL gateways.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Event Sourcing
|
||||||
|
The system uses event sourcing via `gitlab.com/unboundsoftware/eventsourced`. Key domain aggregates are:
|
||||||
|
- **Organization** (domain/aggregates.go): Manages organizations, users, and API keys
|
||||||
|
- **SubGraph** (domain/aggregates.go): Tracks subgraph schemas with versioning
|
||||||
|
|
||||||
|
All state changes flow through events (domain/events.go) and commands (domain/commands.go). The EventStore persists events to PostgreSQL, and events are published to RabbitMQ for downstream consumers.
|
||||||
|
|
||||||
|
### GraphQL Layer
|
||||||
|
- **Schema**: graph/schema.graphqls defines the API
|
||||||
|
- **Resolvers**: graph/schema.resolvers.go implements mutations/queries
|
||||||
|
- **Generated Code**: graph/generated/ and graph/model/ (auto-generated by gqlgen)
|
||||||
|
|
||||||
|
The resolver (graph/resolver.go) coordinates between the EventStore, Publisher (RabbitMQ), Cache, and PubSub for subscriptions.
|
||||||
|
|
||||||
|
### Schema Merging
|
||||||
|
The sdlmerge/ package handles GraphQL schema federation:
|
||||||
|
- Merges multiple subgraph SDL schemas into a unified supergraph
|
||||||
|
- Uses wundergraph/graphql-go-tools for AST manipulation
|
||||||
|
- Removes duplicates, extends types, and applies federation directives
|
||||||
|
|
||||||
|
### Authentication & Authorization
|
||||||
|
- **Auth0 JWT** (middleware/auth0.go): Validates user tokens from Auth0
|
||||||
|
- **API Keys** (middleware/apikey.go): Validates service API keys
|
||||||
|
- **Auth Middleware** (middleware/auth.go): Routes auth based on context
|
||||||
|
|
||||||
|
The @auth directive controls field-level access (user vs organization API key).
|
||||||
|
|
||||||
|
### Cosmo Router Integration
|
||||||
|
The service generates Cosmo Router configuration (graph/cosmo.go) using the wgc CLI tool installed in the Docker container. This config enables federated query execution across subgraphs.
|
||||||
|
|
||||||
|
### PubSub for Real-time Updates
|
||||||
|
graph/pubsub.go implements subscription support for schemaUpdates, allowing clients to receive real-time notifications when schemas change.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Code Generation
|
||||||
|
```bash
|
||||||
|
# Generate GraphQL server code (gqlgen), format, and organize imports
|
||||||
|
go generate ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Always run this after modifying graph/schema.graphqls. The go:generate directives are in:
|
||||||
|
- graph/resolver.go: runs gqlgen, gofumpt, and goimports
|
||||||
|
- ctl/ctl.go: generates genqlient client code
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./... -v
|
||||||
|
|
||||||
|
# Run tests with race detection and coverage (as used in CI)
|
||||||
|
CGO_ENABLED=1 go test -race -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
|
|
||||||
|
# Run specific package tests
|
||||||
|
go test ./middleware -v
|
||||||
|
go test ./graph -v -run TestGenerateCosmoRouterConfig
|
||||||
|
|
||||||
|
# Run single test
|
||||||
|
go test ./cmd/service -v -run TestWebSocket
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building
|
||||||
|
```bash
|
||||||
|
# Build the service binary
|
||||||
|
go build -o service ./cmd/service/service.go
|
||||||
|
|
||||||
|
# Build the CLI tool
|
||||||
|
go build -o schemactl ./cmd/schemactl/schemactl.go
|
||||||
|
|
||||||
|
# Docker build (multi-stage)
|
||||||
|
docker build -t schemas .
|
||||||
|
```
|
||||||
|
|
||||||
|
The Dockerfile runs tests with coverage before building the production binary.
|
||||||
|
|
||||||
|
### Running the Service
|
||||||
|
```bash
|
||||||
|
# Start the service (requires PostgreSQL and RabbitMQ)
|
||||||
|
go run ./cmd/service/service.go \
|
||||||
|
--postgres-url="postgres://user:pass@localhost:5432/schemas?sslmode=disable" \
|
||||||
|
--amqp-url="amqp://user:pass@localhost:5672/" \
|
||||||
|
--issuer="your-auth0-domain.auth0.com"
|
||||||
|
|
||||||
|
# The service listens on port 8080 by default
|
||||||
|
# GraphQL Playground available at http://localhost:8080/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the schemactl CLI
|
||||||
|
```bash
|
||||||
|
# Publish a subgraph schema
|
||||||
|
schemactl publish \
|
||||||
|
--api-key="your-api-key" \
|
||||||
|
--schema-ref="production" \
|
||||||
|
--service="users" \
|
||||||
|
--url="http://users-service:8080/query" \
|
||||||
|
--sdl=schema.graphql
|
||||||
|
|
||||||
|
# List subgraphs for a ref
|
||||||
|
schemactl list \
|
||||||
|
--api-key="your-api-key" \
|
||||||
|
--schema-ref="production"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. **Schema Changes**: Edit graph/schema.graphqls → run `go generate ./...`
|
||||||
|
2. **Resolver Implementation**: Implement in graph/schema.resolvers.go
|
||||||
|
3. **Testing**: Write tests, run `go test ./...`
|
||||||
|
4. **Pre-commit**: Hooks run go-mod-tidy, goimports, gofumpt, golangci-lint, and tests
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
|
||||||
|
- **gqlgen**: GraphQL server generation
|
||||||
|
- **genqlient**: GraphQL client generation (for ctl package)
|
||||||
|
- **eventsourced**: Event sourcing framework
|
||||||
|
- **wundergraph/graphql-go-tools**: Schema federation and composition
|
||||||
|
- **wgc CLI**: Cosmo Router config generation (Node.js tool)
|
||||||
|
- **Auth0**: JWT authentication
|
||||||
|
- **OpenTelemetry**: Observability (traces, metrics, logs)
|
||||||
|
|
||||||
|
## Important Files
|
||||||
|
|
||||||
|
- gqlgen.yml: gqlgen configuration
|
||||||
|
- graph/tools.go: Declares build-time tool dependencies
|
||||||
|
- .pre-commit-config.yaml: Pre-commit hooks configuration
|
||||||
|
- cliff.toml: Changelog generation config
|
||||||
+12
-3
@@ -1,9 +1,10 @@
|
|||||||
FROM amd64/golang:1.24.4@sha256:3494bbe140127d12656113203ec91b8e3ff34e8a2b06a0a22bb0d8a41cc69e53 as modules
|
FROM amd64/golang:1.25.6@sha256:9860925875ac68a8fb57416cfc5c1ee267a06226730434af677b9406e8ea6ee6 as modules
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
ADD go.* /build
|
ADD go.* /build
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
FROM modules as build
|
FROM modules as build
|
||||||
|
ARG CI_COMMIT
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
ENV CGO_ENABLED=0
|
ENV CGO_ENABLED=0
|
||||||
ADD . /build
|
ADD . /build
|
||||||
@@ -17,15 +18,23 @@ RUN GOOS=linux GOARCH=amd64 go build \
|
|||||||
-a -installsuffix cgo \
|
-a -installsuffix cgo \
|
||||||
-mod=readonly \
|
-mod=readonly \
|
||||||
-o /release/service \
|
-o /release/service \
|
||||||
-ldflags '-w -s' \
|
-ldflags "-w -s -X main.buildVersion=${CI_COMMIT}" \
|
||||||
./cmd/service/service.go
|
./cmd/service/service.go
|
||||||
|
|
||||||
FROM scratch as export
|
FROM scratch as export
|
||||||
COPY --from=build /build/coverage.txt /
|
COPY --from=build /build/coverage.txt /
|
||||||
|
|
||||||
FROM scratch
|
FROM node:24.13.0-alpine@sha256:931d7d57f8c1fd0e2179dbff7cc7da4c9dd100998bc2b32afc85142d8efbc213
|
||||||
ENV TZ Europe/Stockholm
|
ENV TZ Europe/Stockholm
|
||||||
|
|
||||||
|
# Install wgc CLI globally for Cosmo Router composition
|
||||||
|
RUN npm install -g wgc@latest
|
||||||
|
|
||||||
|
# Copy timezone data and certificates
|
||||||
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
|
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
|
||||||
|
# Copy the service binary
|
||||||
COPY --from=build /release/service /
|
COPY --from=build /release/service /
|
||||||
|
|
||||||
CMD ["/service"]
|
CMD ["/service"]
|
||||||
|
|||||||
Vendored
+148
-23
@@ -3,18 +3,21 @@ package cache
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sparetimecoders/goamqp"
|
"github.com/sparetimecoders/goamqp"
|
||||||
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/hash"
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
organizations map[string]domain.Organization
|
organizations map[string]domain.Organization
|
||||||
users map[string][]string
|
users map[string][]string
|
||||||
apiKeys map[string]domain.APIKey
|
apiKeys map[string]domain.APIKey // keyed by organizationId-name
|
||||||
services map[string]map[string]map[string]struct{}
|
services map[string]map[string]map[string]struct{}
|
||||||
subGraphs map[string]string
|
subGraphs map[string]string
|
||||||
lastUpdate map[string]string
|
lastUpdate map[string]string
|
||||||
@@ -22,18 +25,26 @@ type Cache struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) OrganizationByAPIKey(apiKey string) *domain.Organization {
|
func (c *Cache) OrganizationByAPIKey(apiKey string) *domain.Organization {
|
||||||
key, exists := c.apiKeys[apiKey]
|
c.mu.RLock()
|
||||||
if !exists {
|
defer c.mu.RUnlock()
|
||||||
return nil
|
|
||||||
|
// Find the API key by comparing hashes
|
||||||
|
for _, key := range c.apiKeys {
|
||||||
|
if hash.CompareAPIKey(key.Key, apiKey) {
|
||||||
|
org, exists := c.organizations[key.OrganizationId]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &org
|
||||||
|
}
|
||||||
}
|
}
|
||||||
org, exists := c.organizations[key.OrganizationId]
|
return nil
|
||||||
if !exists {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &org
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) OrganizationsByUser(sub string) []domain.Organization {
|
func (c *Cache) OrganizationsByUser(sub string) []domain.Organization {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
orgIds := c.users[sub]
|
orgIds := c.users[sub]
|
||||||
orgs := make([]domain.Organization, len(orgIds))
|
orgs := make([]domain.Organization, len(orgIds))
|
||||||
for i, id := range orgIds {
|
for i, id := range orgIds {
|
||||||
@@ -42,15 +53,34 @@ func (c *Cache) OrganizationsByUser(sub string) []domain.Organization {
|
|||||||
return orgs
|
return orgs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) ApiKeyByKey(key string) *domain.APIKey {
|
func (c *Cache) AllOrganizations() []domain.Organization {
|
||||||
k, exists := c.apiKeys[hash.String(key)]
|
c.mu.RLock()
|
||||||
if !exists {
|
defer c.mu.RUnlock()
|
||||||
return nil
|
|
||||||
|
orgs := make([]domain.Organization, 0, len(c.organizations))
|
||||||
|
for _, org := range c.organizations {
|
||||||
|
orgs = append(orgs, org)
|
||||||
}
|
}
|
||||||
return &k
|
return orgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) ApiKeyByKey(key string) *domain.APIKey {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
// Find the API key by comparing hashes
|
||||||
|
for _, apiKey := range c.apiKeys {
|
||||||
|
if hash.CompareAPIKey(apiKey.Key, key) {
|
||||||
|
return &apiKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Services(orgId, ref, lastUpdate string) ([]string, string) {
|
func (c *Cache) Services(orgId, ref, lastUpdate string) ([]string, string) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
key := refKey(orgId, ref)
|
key := refKey(orgId, ref)
|
||||||
var services []string
|
var services []string
|
||||||
if lastUpdate == "" || c.lastUpdate[key] > lastUpdate {
|
if lastUpdate == "" || c.lastUpdate[key] > lastUpdate {
|
||||||
@@ -62,41 +92,123 @@ func (c *Cache) Services(orgId, ref, lastUpdate string) ([]string, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) SubGraphId(orgId, ref, service string) string {
|
func (c *Cache) SubGraphId(orgId, ref, service string) string {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
return c.subGraphs[subGraphKey(orgId, ref, service)]
|
return c.subGraphs[subGraphKey(orgId, ref, service)]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Update(msg any, _ goamqp.Headers) (any, error) {
|
func (c *Cache) Update(msg any, _ goamqp.Headers) (any, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
switch m := msg.(type) {
|
switch m := msg.(type) {
|
||||||
case *domain.OrganizationAdded:
|
case *domain.OrganizationAdded:
|
||||||
o := domain.Organization{}
|
o := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(m.ID.String()),
|
||||||
|
}
|
||||||
m.UpdateOrganization(&o)
|
m.UpdateOrganization(&o)
|
||||||
c.organizations[m.ID.String()] = o
|
c.organizations[m.ID.String()] = o
|
||||||
c.addUser(m.Initiator, o)
|
c.addUser(m.Initiator, o)
|
||||||
|
c.logger.With("org_id", m.ID.String(), "event", "OrganizationAdded").Debug("cache updated")
|
||||||
|
case *domain.UserAddedToOrganization:
|
||||||
|
org, exists := c.organizations[m.ID.String()]
|
||||||
|
if exists {
|
||||||
|
m.UpdateOrganization(&org)
|
||||||
|
c.organizations[m.ID.String()] = org
|
||||||
|
c.addUser(m.UserId, org)
|
||||||
|
c.logger.With("org_id", m.ID.String(), "user_id", m.UserId, "event", "UserAddedToOrganization").Debug("cache updated")
|
||||||
|
} else {
|
||||||
|
c.logger.With("org_id", m.ID.String(), "event", "UserAddedToOrganization").Warn("organization not found in cache")
|
||||||
|
}
|
||||||
case *domain.APIKeyAdded:
|
case *domain.APIKeyAdded:
|
||||||
key := domain.APIKey{
|
key := domain.APIKey{
|
||||||
Name: m.Name,
|
Name: m.Name,
|
||||||
OrganizationId: m.OrganizationId,
|
OrganizationId: m.OrganizationId,
|
||||||
Key: m.Key,
|
Key: m.Key, // This is now the hashed key
|
||||||
Refs: m.Refs,
|
Refs: m.Refs,
|
||||||
Read: m.Read,
|
Read: m.Read,
|
||||||
Publish: m.Publish,
|
Publish: m.Publish,
|
||||||
CreatedBy: m.Initiator,
|
CreatedBy: m.Initiator,
|
||||||
CreatedAt: m.When(),
|
CreatedAt: m.When(),
|
||||||
}
|
}
|
||||||
c.apiKeys[m.Key] = key
|
// Use composite key: organizationId-name
|
||||||
|
c.apiKeys[apiKeyId(m.OrganizationId, m.Name)] = key
|
||||||
org := c.organizations[m.OrganizationId]
|
org := c.organizations[m.OrganizationId]
|
||||||
org.APIKeys = append(org.APIKeys, key)
|
org.APIKeys = append(org.APIKeys, key)
|
||||||
c.organizations[m.OrganizationId] = org
|
c.organizations[m.OrganizationId] = org
|
||||||
|
c.logger.With("org_id", m.OrganizationId, "key_name", m.Name, "event", "APIKeyAdded").Debug("cache updated")
|
||||||
|
case *domain.APIKeyRemoved:
|
||||||
|
orgId := m.ID.String()
|
||||||
|
org, exists := c.organizations[orgId]
|
||||||
|
if exists {
|
||||||
|
// Remove from organization's API keys list
|
||||||
|
for i, key := range org.APIKeys {
|
||||||
|
if key.Name == m.KeyName {
|
||||||
|
org.APIKeys = append(org.APIKeys[:i], org.APIKeys[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.organizations[orgId] = org
|
||||||
|
// Remove from apiKeys map
|
||||||
|
delete(c.apiKeys, apiKeyId(orgId, m.KeyName))
|
||||||
|
c.logger.With("org_id", orgId, "key_name", m.KeyName, "event", "APIKeyRemoved").Debug("cache updated")
|
||||||
|
} else {
|
||||||
|
c.logger.With("org_id", orgId, "event", "APIKeyRemoved").Warn("organization not found in cache")
|
||||||
|
}
|
||||||
|
case *domain.OrganizationRemoved:
|
||||||
|
orgId := m.ID.String()
|
||||||
|
org, exists := c.organizations[orgId]
|
||||||
|
if exists {
|
||||||
|
// Remove all API keys for this organization
|
||||||
|
for _, key := range org.APIKeys {
|
||||||
|
delete(c.apiKeys, apiKeyId(orgId, key.Name))
|
||||||
|
}
|
||||||
|
// Remove organization from all users
|
||||||
|
for userId, userOrgs := range c.users {
|
||||||
|
for i, userOrgId := range userOrgs {
|
||||||
|
if userOrgId == orgId {
|
||||||
|
c.users[userId] = append(userOrgs[:i], userOrgs[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If user has no more organizations, remove from map
|
||||||
|
if len(c.users[userId]) == 0 {
|
||||||
|
delete(c.users, userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove services for this organization
|
||||||
|
if refs, exists := c.services[orgId]; exists {
|
||||||
|
for ref := range refs {
|
||||||
|
// Remove all subgraphs for this org/ref combination
|
||||||
|
for service := range refs[ref] {
|
||||||
|
delete(c.subGraphs, subGraphKey(orgId, ref, service))
|
||||||
|
}
|
||||||
|
// Remove lastUpdate for this org/ref
|
||||||
|
delete(c.lastUpdate, refKey(orgId, ref))
|
||||||
|
}
|
||||||
|
delete(c.services, orgId)
|
||||||
|
}
|
||||||
|
// Remove organization
|
||||||
|
delete(c.organizations, orgId)
|
||||||
|
c.logger.With("org_id", orgId, "event", "OrganizationRemoved").Debug("cache updated")
|
||||||
|
} else {
|
||||||
|
c.logger.With("org_id", orgId, "event", "OrganizationRemoved").Warn("organization not found in cache")
|
||||||
|
}
|
||||||
case *domain.SubGraphUpdated:
|
case *domain.SubGraphUpdated:
|
||||||
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.Time)
|
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.Time)
|
||||||
|
c.logger.With("org_id", m.OrganizationId, "ref", m.Ref, "service", m.Service, "event", "SubGraphUpdated").Debug("cache updated")
|
||||||
case *domain.Organization:
|
case *domain.Organization:
|
||||||
c.organizations[m.ID.String()] = *m
|
c.organizations[m.ID.String()] = *m
|
||||||
c.addUser(m.CreatedBy, *m)
|
c.addUser(m.CreatedBy, *m)
|
||||||
for _, k := range m.APIKeys {
|
for _, k := range m.APIKeys {
|
||||||
c.apiKeys[k.Key] = k
|
// Use composite key: organizationId-name
|
||||||
|
c.apiKeys[apiKeyId(k.OrganizationId, k.Name)] = k
|
||||||
}
|
}
|
||||||
|
c.logger.With("org_id", m.ID.String(), "event", "Organization aggregate loaded").Debug("cache updated")
|
||||||
case *domain.SubGraph:
|
case *domain.SubGraph:
|
||||||
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.ChangedAt)
|
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.ChangedAt)
|
||||||
|
c.logger.With("org_id", m.OrganizationId, "ref", m.Ref, "service", m.Service, "event", "SubGraph aggregate loaded").Debug("cache updated")
|
||||||
default:
|
default:
|
||||||
c.logger.With("msg", msg).Warn("unexpected message received")
|
c.logger.With("msg", msg).Warn("unexpected message received")
|
||||||
}
|
}
|
||||||
@@ -117,11 +229,20 @@ func (c *Cache) updateSubGraph(orgId string, ref string, subGraphId string, serv
|
|||||||
|
|
||||||
func (c *Cache) addUser(sub string, organization domain.Organization) {
|
func (c *Cache) addUser(sub string, organization domain.Organization) {
|
||||||
user, exists := c.users[sub]
|
user, exists := c.users[sub]
|
||||||
|
orgId := organization.ID.String()
|
||||||
if !exists {
|
if !exists {
|
||||||
c.users[sub] = []string{organization.ID.String()}
|
c.users[sub] = []string{orgId}
|
||||||
} else {
|
return
|
||||||
c.users[sub] = append(user, organization.ID.String())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if organization already exists for this user
|
||||||
|
for _, id := range user {
|
||||||
|
if id == orgId {
|
||||||
|
return // Already exists, no need to add
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.users[sub] = append(user, orgId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger *slog.Logger) *Cache {
|
func New(logger *slog.Logger) *Cache {
|
||||||
@@ -143,3 +264,7 @@ func refKey(orgId string, ref string) string {
|
|||||||
func subGraphKey(orgId string, ref string, service string) string {
|
func subGraphKey(orgId string, ref string, service string) string {
|
||||||
return fmt.Sprintf("%s<->%s<->%s", orgId, ref, service)
|
return fmt.Sprintf("%s<->%s<->%s", orgId, ref, service)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiKeyId(orgId string, name string) string {
|
||||||
|
return fmt.Sprintf("%s<->%s", orgId, name)
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+645
@@ -0,0 +1,645 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCache_OrganizationByAPIKey(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
apiKey := "test-api-key-123" // gitleaks:allow
|
||||||
|
hashedKey, err := hash.APIKey(apiKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Add organization to cache
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
|
||||||
|
// Add API key to cache
|
||||||
|
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
|
||||||
|
Name: "test-key",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: hashedKey,
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test finding organization by plaintext API key
|
||||||
|
foundOrg := c.OrganizationByAPIKey(apiKey)
|
||||||
|
require.NotNil(t, foundOrg)
|
||||||
|
assert.Equal(t, org.Name, foundOrg.Name)
|
||||||
|
assert.Equal(t, orgID, foundOrg.ID.String())
|
||||||
|
|
||||||
|
// Test with wrong API key
|
||||||
|
notFoundOrg := c.OrganizationByAPIKey("wrong-key")
|
||||||
|
assert.Nil(t, notFoundOrg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_OrganizationByAPIKey_Legacy(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
apiKey := "legacy-api-key-456" // gitleaks:allow
|
||||||
|
legacyHash := hash.String(apiKey)
|
||||||
|
|
||||||
|
// Add organization to cache
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Legacy Org",
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
|
||||||
|
// Add API key with legacy SHA256 hash
|
||||||
|
c.apiKeys[apiKeyId(orgID, "legacy-key")] = domain.APIKey{
|
||||||
|
Name: "legacy-key",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: legacyHash,
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test finding organization with legacy hash
|
||||||
|
foundOrg := c.OrganizationByAPIKey(apiKey)
|
||||||
|
require.NotNil(t, foundOrg)
|
||||||
|
assert.Equal(t, org.Name, foundOrg.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_OrganizationsByUser(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
userSub := "user-123"
|
||||||
|
org1ID := uuid.New().String()
|
||||||
|
org2ID := uuid.New().String()
|
||||||
|
|
||||||
|
org1 := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(org1ID),
|
||||||
|
Name: "Org 1",
|
||||||
|
}
|
||||||
|
org2 := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(org2ID),
|
||||||
|
Name: "Org 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.organizations[org1ID] = org1
|
||||||
|
c.organizations[org2ID] = org2
|
||||||
|
c.users[userSub] = []string{org1ID, org2ID}
|
||||||
|
|
||||||
|
orgs := c.OrganizationsByUser(userSub)
|
||||||
|
assert.Len(t, orgs, 2)
|
||||||
|
assert.Contains(t, []string{orgs[0].Name, orgs[1].Name}, "Org 1")
|
||||||
|
assert.Contains(t, []string{orgs[0].Name, orgs[1].Name}, "Org 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_ApiKeyByKey(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
apiKey := "test-api-key-789" // gitleaks:allow
|
||||||
|
hashedKey, err := hash.APIKey(apiKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedKey := domain.APIKey{
|
||||||
|
Name: "test-key",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: hashedKey,
|
||||||
|
Refs: []string{"main", "dev"},
|
||||||
|
Read: true,
|
||||||
|
Publish: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.apiKeys[apiKeyId(orgID, "test-key")] = expectedKey
|
||||||
|
|
||||||
|
foundKey := c.ApiKeyByKey(apiKey)
|
||||||
|
require.NotNil(t, foundKey)
|
||||||
|
assert.Equal(t, expectedKey.Name, foundKey.Name)
|
||||||
|
assert.Equal(t, expectedKey.OrganizationId, foundKey.OrganizationId)
|
||||||
|
assert.Equal(t, expectedKey.Refs, foundKey.Refs)
|
||||||
|
|
||||||
|
// Test with wrong key
|
||||||
|
notFoundKey := c.ApiKeyByKey("wrong-key")
|
||||||
|
assert.Nil(t, notFoundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Services(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
ref := "main"
|
||||||
|
service1 := "service-1"
|
||||||
|
service2 := "service-2"
|
||||||
|
lastUpdate := "2024-01-01T12:00:00Z"
|
||||||
|
|
||||||
|
c.services[orgID] = map[string]map[string]struct{}{
|
||||||
|
ref: {
|
||||||
|
service1: {},
|
||||||
|
service2: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.lastUpdate[refKey(orgID, ref)] = lastUpdate
|
||||||
|
|
||||||
|
// Test getting services with empty lastUpdate
|
||||||
|
services, returnedLastUpdate := c.Services(orgID, ref, "")
|
||||||
|
assert.Len(t, services, 2)
|
||||||
|
assert.Contains(t, services, service1)
|
||||||
|
assert.Contains(t, services, service2)
|
||||||
|
assert.Equal(t, lastUpdate, returnedLastUpdate)
|
||||||
|
|
||||||
|
// Test with older lastUpdate (should return services)
|
||||||
|
services, returnedLastUpdate = c.Services(orgID, ref, "2023-12-31T12:00:00Z")
|
||||||
|
assert.Len(t, services, 2)
|
||||||
|
assert.Equal(t, lastUpdate, returnedLastUpdate)
|
||||||
|
|
||||||
|
// Test with newer lastUpdate (should return empty)
|
||||||
|
services, returnedLastUpdate = c.Services(orgID, ref, "2024-01-02T12:00:00Z")
|
||||||
|
assert.Len(t, services, 0)
|
||||||
|
assert.Equal(t, lastUpdate, returnedLastUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_SubGraphId(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
ref := "main"
|
||||||
|
service := "test-service"
|
||||||
|
subGraphID := uuid.New().String()
|
||||||
|
|
||||||
|
c.subGraphs[subGraphKey(orgID, ref, service)] = subGraphID
|
||||||
|
|
||||||
|
foundID := c.SubGraphId(orgID, ref, service)
|
||||||
|
assert.Equal(t, subGraphID, foundID)
|
||||||
|
|
||||||
|
// Test with non-existent key
|
||||||
|
notFoundID := c.SubGraphId("wrong-org", ref, service)
|
||||||
|
assert.Empty(t, notFoundID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_OrganizationAdded(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
event := &domain.OrganizationAdded{
|
||||||
|
Name: "New Org",
|
||||||
|
Initiator: "user-123",
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(orgID)
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify organization was added
|
||||||
|
org, exists := c.organizations[orgID]
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, "New Org", org.Name)
|
||||||
|
|
||||||
|
// Verify user was added
|
||||||
|
assert.Contains(t, c.users["user-123"], orgID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_APIKeyAdded(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
keyName := "test-key"
|
||||||
|
hashedKey := "hashed-key-value"
|
||||||
|
|
||||||
|
// Add organization first
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
APIKeys: []domain.APIKey{},
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
|
||||||
|
event := &domain.APIKeyAdded{
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Name: keyName,
|
||||||
|
Key: hashedKey,
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
Initiator: "user-123",
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(uuid.New().String())
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify API key was added to cache
|
||||||
|
key, exists := c.apiKeys[apiKeyId(orgID, keyName)]
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, keyName, key.Name)
|
||||||
|
assert.Equal(t, hashedKey, key.Key)
|
||||||
|
assert.Equal(t, []string{"main"}, key.Refs)
|
||||||
|
|
||||||
|
// Verify API key was added to organization
|
||||||
|
updatedOrg := c.organizations[orgID]
|
||||||
|
assert.Len(t, updatedOrg.APIKeys, 1)
|
||||||
|
assert.Equal(t, keyName, updatedOrg.APIKeys[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_SubGraphUpdated(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
ref := "main"
|
||||||
|
service := "test-service"
|
||||||
|
subGraphID := uuid.New().String()
|
||||||
|
|
||||||
|
event := &domain.SubGraphUpdated{
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Ref: ref,
|
||||||
|
Service: service,
|
||||||
|
Initiator: "user-123",
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(subGraphID)
|
||||||
|
event.SetWhen(time.Now())
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify subgraph was added to services
|
||||||
|
assert.Contains(t, c.services[orgID][ref], subGraphID)
|
||||||
|
|
||||||
|
// Verify subgraph ID was stored
|
||||||
|
assert.Equal(t, subGraphID, c.subGraphs[subGraphKey(orgID, ref, service)])
|
||||||
|
|
||||||
|
// Verify lastUpdate was set
|
||||||
|
assert.NotEmpty(t, c.lastUpdate[refKey(orgID, ref)])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_AddUser_NoDuplicates(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
userSub := "user-123"
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user first time
|
||||||
|
c.addUser(userSub, org)
|
||||||
|
assert.Len(t, c.users[userSub], 1)
|
||||||
|
assert.Equal(t, orgID, c.users[userSub][0])
|
||||||
|
|
||||||
|
// Add same user/org again - should not create duplicate
|
||||||
|
c.addUser(userSub, org)
|
||||||
|
assert.Len(t, c.users[userSub], 1, "Should not add duplicate organization")
|
||||||
|
assert.Equal(t, orgID, c.users[userSub][0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_ConcurrentReads(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
// Setup test data - use legacy hash to avoid slow bcrypt
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
userSub := "test-user"
|
||||||
|
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Concurrent Test Org",
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
c.users[userSub] = []string{orgID}
|
||||||
|
|
||||||
|
// Run concurrent reads using fast OrganizationsByUser
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
numGoroutines := 20
|
||||||
|
|
||||||
|
for i := 0; i < numGoroutines; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
orgs := c.OrganizationsByUser(userSub)
|
||||||
|
assert.NotEmpty(t, orgs)
|
||||||
|
assert.Equal(t, "Concurrent Test Org", orgs[0].Name)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_ConcurrentWrites(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
numGoroutines := 10 // Reduced for race detector
|
||||||
|
|
||||||
|
// Concurrent organization additions
|
||||||
|
for i := 0; i < numGoroutines; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(index int) {
|
||||||
|
defer wg.Done()
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
event := &domain.OrganizationAdded{
|
||||||
|
Name: "Org " + string(rune(index)),
|
||||||
|
Initiator: "user-" + string(rune(index)),
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(orgID)
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Verify all organizations were added
|
||||||
|
assert.Equal(t, numGoroutines, len(c.organizations))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_ConcurrentReadsAndWrites(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
// Setup initial data - use legacy hash to avoid slow bcrypt in concurrent test
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
legacyKey := "test-rw-key" // gitleaks:allow
|
||||||
|
legacyHash := hash.String(legacyKey)
|
||||||
|
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "RW Test Org",
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
|
||||||
|
Name: "test-key",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: legacyHash,
|
||||||
|
}
|
||||||
|
c.users["user-initial"] = []string{orgID}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
numReaders := 5
|
||||||
|
numWriters := 3
|
||||||
|
|
||||||
|
// Concurrent readers - use OrganizationsByUser which is fast
|
||||||
|
for i := 0; i < numReaders; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
orgs := c.OrganizationsByUser("user-initial")
|
||||||
|
assert.NotEmpty(t, orgs)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concurrent writers
|
||||||
|
for i := 0; i < numWriters; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(index int) {
|
||||||
|
defer wg.Done()
|
||||||
|
newOrgID := uuid.New().String()
|
||||||
|
event := &domain.OrganizationAdded{
|
||||||
|
Name: "New Org " + string(rune(index)),
|
||||||
|
Initiator: "user-new-" + string(rune(index)),
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(newOrgID)
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Verify cache is in consistent state
|
||||||
|
assert.GreaterOrEqual(t, len(c.organizations), numWriters)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_APIKeyRemoved(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
keyName := "test-key"
|
||||||
|
hashedKey := "hashed-key-value"
|
||||||
|
|
||||||
|
// Add organization with API key
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
APIKeys: []domain.APIKey{
|
||||||
|
{
|
||||||
|
Name: keyName,
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: hashedKey,
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
c.apiKeys[apiKeyId(orgID, keyName)] = org.APIKeys[0]
|
||||||
|
|
||||||
|
// Verify key exists before removal
|
||||||
|
_, exists := c.apiKeys[apiKeyId(orgID, keyName)]
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
// Remove the API key
|
||||||
|
event := &domain.APIKeyRemoved{
|
||||||
|
KeyName: keyName,
|
||||||
|
Initiator: "user-123",
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(orgID)
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify API key was removed from cache
|
||||||
|
_, exists = c.apiKeys[apiKeyId(orgID, keyName)]
|
||||||
|
assert.False(t, exists, "API key should be removed from cache")
|
||||||
|
|
||||||
|
// Verify API key was removed from organization
|
||||||
|
updatedOrg := c.organizations[orgID]
|
||||||
|
assert.Len(t, updatedOrg.APIKeys, 0, "API key should be removed from organization")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_APIKeyRemoved_MultipleKeys(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
|
||||||
|
// Add organization with multiple API keys
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
APIKeys: []domain.APIKey{
|
||||||
|
{
|
||||||
|
Name: "key1",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: "hash1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "key2",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: "hash2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "key3",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: "hash3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
c.apiKeys[apiKeyId(orgID, "key1")] = org.APIKeys[0]
|
||||||
|
c.apiKeys[apiKeyId(orgID, "key2")] = org.APIKeys[1]
|
||||||
|
c.apiKeys[apiKeyId(orgID, "key3")] = org.APIKeys[2]
|
||||||
|
|
||||||
|
// Remove the middle key
|
||||||
|
event := &domain.APIKeyRemoved{
|
||||||
|
KeyName: "key2",
|
||||||
|
Initiator: "user-123",
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(orgID)
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify only key2 was removed
|
||||||
|
_, exists := c.apiKeys[apiKeyId(orgID, "key1")]
|
||||||
|
assert.True(t, exists, "key1 should still exist")
|
||||||
|
|
||||||
|
_, exists = c.apiKeys[apiKeyId(orgID, "key2")]
|
||||||
|
assert.False(t, exists, "key2 should be removed")
|
||||||
|
|
||||||
|
_, exists = c.apiKeys[apiKeyId(orgID, "key3")]
|
||||||
|
assert.True(t, exists, "key3 should still exist")
|
||||||
|
|
||||||
|
// Verify organization has 2 keys remaining
|
||||||
|
updatedOrg := c.organizations[orgID]
|
||||||
|
assert.Len(t, updatedOrg.APIKeys, 2)
|
||||||
|
assert.Equal(t, "key1", updatedOrg.APIKeys[0].Name)
|
||||||
|
assert.Equal(t, "key3", updatedOrg.APIKeys[1].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_OrganizationRemoved(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
userSub := "user-123"
|
||||||
|
|
||||||
|
// Add organization with API keys, users, and subgraphs
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
APIKeys: []domain.APIKey{
|
||||||
|
{
|
||||||
|
Name: "key1",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: "hash1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
c.apiKeys[apiKeyId(orgID, "key1")] = org.APIKeys[0]
|
||||||
|
c.users[userSub] = []string{orgID}
|
||||||
|
c.services[orgID] = map[string]map[string]struct{}{
|
||||||
|
"main": {
|
||||||
|
"service1": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.subGraphs[subGraphKey(orgID, "main", "service1")] = "subgraph-id"
|
||||||
|
c.lastUpdate[refKey(orgID, "main")] = "2024-01-01T12:00:00Z"
|
||||||
|
|
||||||
|
// Remove the organization
|
||||||
|
event := &domain.OrganizationRemoved{
|
||||||
|
Initiator: userSub,
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(orgID)
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify organization was removed
|
||||||
|
_, exists := c.organizations[orgID]
|
||||||
|
assert.False(t, exists, "Organization should be removed from cache")
|
||||||
|
|
||||||
|
// Verify API keys were removed
|
||||||
|
_, exists = c.apiKeys[apiKeyId(orgID, "key1")]
|
||||||
|
assert.False(t, exists, "API keys should be removed from cache")
|
||||||
|
|
||||||
|
// Verify user association was removed
|
||||||
|
userOrgs := c.users[userSub]
|
||||||
|
assert.NotContains(t, userOrgs, orgID, "User should not be associated with removed organization")
|
||||||
|
|
||||||
|
// Verify services were removed
|
||||||
|
_, exists = c.services[orgID]
|
||||||
|
assert.False(t, exists, "Services should be removed from cache")
|
||||||
|
|
||||||
|
// Verify subgraphs were removed
|
||||||
|
_, exists = c.subGraphs[subGraphKey(orgID, "main", "service1")]
|
||||||
|
assert.False(t, exists, "Subgraphs should be removed from cache")
|
||||||
|
|
||||||
|
// Verify lastUpdate was removed
|
||||||
|
_, exists = c.lastUpdate[refKey(orgID, "main")]
|
||||||
|
assert.False(t, exists, "LastUpdate should be removed from cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_OrganizationRemoved_MultipleUsers(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
user1 := "user-1"
|
||||||
|
user2 := "user-2"
|
||||||
|
otherOrgID := uuid.New().String()
|
||||||
|
|
||||||
|
// Add organization
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
|
||||||
|
// Add users with multiple org associations
|
||||||
|
c.users[user1] = []string{orgID, otherOrgID}
|
||||||
|
c.users[user2] = []string{orgID}
|
||||||
|
|
||||||
|
// Remove the organization
|
||||||
|
event := &domain.OrganizationRemoved{
|
||||||
|
Initiator: user1,
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(orgID)
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify user1 still has otherOrgID but not removed orgID
|
||||||
|
assert.Len(t, c.users[user1], 1)
|
||||||
|
assert.Equal(t, otherOrgID, c.users[user1][0])
|
||||||
|
|
||||||
|
// Verify user2 has no organizations
|
||||||
|
assert.Len(t, c.users[user2], 0)
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
# git-cliff ~ default configuration file
|
||||||
|
# https://git-cliff.org/docs/configuration
|
||||||
|
#
|
||||||
|
# Lines starting with "#" are comments.
|
||||||
|
# Configuration options are organized into tables and keys.
|
||||||
|
# See documentation for more information on available options.
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
# template for the changelog header
|
||||||
|
header = """
|
||||||
|
# Changelog\n
|
||||||
|
All notable changes to this project will be documented in this file.\n
|
||||||
|
"""
|
||||||
|
# template for the changelog body
|
||||||
|
# https://keats.github.io/tera/docs/#introduction
|
||||||
|
body = """
|
||||||
|
{% if version %}\
|
||||||
|
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{% else %}\
|
||||||
|
## [unreleased]
|
||||||
|
{% endif %}\
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
### {{ group | striptags | trim | upper_first }}
|
||||||
|
{% for commit in commits %}
|
||||||
|
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||||
|
{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||||
|
{{ commit.message | upper_first }}\
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}\n
|
||||||
|
"""
|
||||||
|
# template for the changelog footer
|
||||||
|
footer = """
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
|
"""
|
||||||
|
# remove the leading and trailing s
|
||||||
|
trim = true
|
||||||
|
# postprocessors
|
||||||
|
postprocessors = [
|
||||||
|
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
|
||||||
|
]
|
||||||
|
# render body even when there are no releases to process
|
||||||
|
# render_always = true
|
||||||
|
# output file path
|
||||||
|
# output = "test.md"
|
||||||
|
|
||||||
|
[git]
|
||||||
|
# parse the commits based on https://www.conventionalcommits.org
|
||||||
|
conventional_commits = true
|
||||||
|
# filter out the commits that are not conventional
|
||||||
|
filter_unconventional = true
|
||||||
|
# process each line of a commit as an individual commit
|
||||||
|
split_commits = false
|
||||||
|
# regex for preprocessing the commit messages
|
||||||
|
commit_preprocessors = [
|
||||||
|
# Replace issue numbers
|
||||||
|
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
||||||
|
# Check spelling of the commit with https://github.com/crate-ci/typos
|
||||||
|
# If the spelling is incorrect, it will be automatically fixed.
|
||||||
|
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
||||||
|
]
|
||||||
|
# regex for parsing and grouping commits
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||||
|
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||||
|
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||||
|
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||||
|
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||||
|
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||||
|
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||||
|
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||||
|
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||||
|
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||||
|
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||||
|
]
|
||||||
|
# filter out the commits that are not matched by commit parsers
|
||||||
|
filter_commits = false
|
||||||
|
# sort the tags topologically
|
||||||
|
topo_order = false
|
||||||
|
# sort the commits inside sections by oldest/newest order
|
||||||
|
sort_commits = "oldest"
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/ctl"
|
"gitea.unbound.se/unboundsoftware/schemas/ctl"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Context struct {
|
type Context struct {
|
||||||
|
|||||||
+42
-13
@@ -26,14 +26,15 @@ import (
|
|||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
"gitlab.com/unboundsoftware/eventsourced/pg"
|
"gitlab.com/unboundsoftware/eventsourced/pg"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/cache"
|
"gitea.unbound.se/unboundsoftware/schemas/cache"
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph"
|
"gitea.unbound.se/unboundsoftware/schemas/graph"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/generated"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/generated"
|
||||||
"gitlab.com/unboundsoftware/schemas/logging"
|
"gitea.unbound.se/unboundsoftware/schemas/health"
|
||||||
"gitlab.com/unboundsoftware/schemas/middleware"
|
"gitea.unbound.se/unboundsoftware/schemas/logging"
|
||||||
"gitlab.com/unboundsoftware/schemas/monitoring"
|
"gitea.unbound.se/unboundsoftware/schemas/middleware"
|
||||||
"gitlab.com/unboundsoftware/schemas/store"
|
"gitea.unbound.se/unboundsoftware/schemas/monitoring"
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
@@ -91,7 +92,10 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
|||||||
pg.WithEventTypes(
|
pg.WithEventTypes(
|
||||||
&domain.SubGraphUpdated{},
|
&domain.SubGraphUpdated{},
|
||||||
&domain.OrganizationAdded{},
|
&domain.OrganizationAdded{},
|
||||||
|
&domain.UserAddedToOrganization{},
|
||||||
&domain.APIKeyAdded{},
|
&domain.APIKeyAdded{},
|
||||||
|
&domain.APIKeyRemoved{},
|
||||||
|
&domain.OrganizationRemoved{},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -126,10 +130,16 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
|||||||
goamqp.EventStreamPublisher(publisher),
|
goamqp.EventStreamPublisher(publisher),
|
||||||
goamqp.TransientEventStreamConsumer("SubGraph.Updated", serviceCache.Update, domain.SubGraphUpdated{}),
|
goamqp.TransientEventStreamConsumer("SubGraph.Updated", serviceCache.Update, domain.SubGraphUpdated{}),
|
||||||
goamqp.TransientEventStreamConsumer("Organization.Added", serviceCache.Update, domain.OrganizationAdded{}),
|
goamqp.TransientEventStreamConsumer("Organization.Added", serviceCache.Update, domain.OrganizationAdded{}),
|
||||||
|
goamqp.TransientEventStreamConsumer("Organization.UserAdded", serviceCache.Update, domain.UserAddedToOrganization{}),
|
||||||
goamqp.TransientEventStreamConsumer("Organization.APIKeyAdded", serviceCache.Update, domain.APIKeyAdded{}),
|
goamqp.TransientEventStreamConsumer("Organization.APIKeyAdded", serviceCache.Update, domain.APIKeyAdded{}),
|
||||||
|
goamqp.TransientEventStreamConsumer("Organization.APIKeyRemoved", serviceCache.Update, domain.APIKeyRemoved{}),
|
||||||
|
goamqp.TransientEventStreamConsumer("Organization.Removed", serviceCache.Update, domain.OrganizationRemoved{}),
|
||||||
goamqp.WithTypeMapping("SubGraph.Updated", domain.SubGraphUpdated{}),
|
goamqp.WithTypeMapping("SubGraph.Updated", domain.SubGraphUpdated{}),
|
||||||
goamqp.WithTypeMapping("Organization.Added", domain.OrganizationAdded{}),
|
goamqp.WithTypeMapping("Organization.Added", domain.OrganizationAdded{}),
|
||||||
|
goamqp.WithTypeMapping("Organization.UserAdded", domain.UserAddedToOrganization{}),
|
||||||
goamqp.WithTypeMapping("Organization.APIKeyAdded", domain.APIKeyAdded{}),
|
goamqp.WithTypeMapping("Organization.APIKeyAdded", domain.APIKeyAdded{}),
|
||||||
|
goamqp.WithTypeMapping("Organization.APIKeyRemoved", domain.APIKeyRemoved{}),
|
||||||
|
goamqp.WithTypeMapping("Organization.Removed", domain.OrganizationRemoved{}),
|
||||||
}
|
}
|
||||||
if err := conn.Start(rootCtx, setups...); err != nil {
|
if err := conn.Start(rootCtx, setups...); err != nil {
|
||||||
return fmt.Errorf("failed to setup AMQP: %v", err)
|
return fmt.Errorf("failed to setup AMQP: %v", err)
|
||||||
@@ -195,6 +205,7 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
|||||||
Publisher: eventPublisher,
|
Publisher: eventPublisher,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Cache: serviceCache,
|
Cache: serviceCache,
|
||||||
|
PubSub: graph.NewPubSub(),
|
||||||
}
|
}
|
||||||
|
|
||||||
config := generated.Config{
|
config := generated.Config{
|
||||||
@@ -209,6 +220,24 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
|||||||
|
|
||||||
srv.AddTransport(transport.Websocket{
|
srv.AddTransport(transport.Websocket{
|
||||||
KeepAlivePingInterval: 10 * time.Second,
|
KeepAlivePingInterval: 10 * time.Second,
|
||||||
|
InitFunc: func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
|
||||||
|
// Extract API key from WebSocket connection_init payload
|
||||||
|
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
|
||||||
|
logger.Info("WebSocket connection with API key", "has_key", true)
|
||||||
|
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
|
||||||
|
|
||||||
|
// Look up organization by API key (cache handles hash comparison)
|
||||||
|
if organization := serviceCache.OrganizationByAPIKey(apiKey); organization != nil {
|
||||||
|
logger.Info("WebSocket: Organization found for API key", "org_id", organization.ID.String())
|
||||||
|
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
|
||||||
|
} else {
|
||||||
|
logger.Warn("WebSocket: No organization found for API key")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Info("WebSocket connection without API key")
|
||||||
|
}
|
||||||
|
return ctx, &initPayload, nil
|
||||||
|
},
|
||||||
})
|
})
|
||||||
srv.AddTransport(transport.Options{})
|
srv.AddTransport(transport.Options{})
|
||||||
srv.AddTransport(transport.GET{})
|
srv.AddTransport(transport.GET{})
|
||||||
@@ -222,8 +251,12 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
|||||||
Cache: lru.New[string](100),
|
Cache: lru.New[string](100),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
healthChecker := health.New(db.DB, logger)
|
||||||
|
|
||||||
mux.Handle("/", monitoring.Handler(playground.Handler("GraphQL playground", "/query")))
|
mux.Handle("/", monitoring.Handler(playground.Handler("GraphQL playground", "/query")))
|
||||||
mux.Handle("/health", http.HandlerFunc(healthFunc))
|
mux.Handle("/health", http.HandlerFunc(healthChecker.LivenessHandler))
|
||||||
|
mux.Handle("/health/live", http.HandlerFunc(healthChecker.LivenessHandler))
|
||||||
|
mux.Handle("/health/ready", http.HandlerFunc(healthChecker.ReadinessHandler))
|
||||||
mux.Handle("/query", cors.AllowAll().Handler(
|
mux.Handle("/query", cors.AllowAll().Handler(
|
||||||
monitoring.Handler(
|
monitoring.Handler(
|
||||||
mw.Middleware().CheckJWT(
|
mw.Middleware().CheckJWT(
|
||||||
@@ -282,10 +315,6 @@ func loadSubGraphs(ctx context.Context, eventStore eventsourced.EventStore, serv
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func healthFunc(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
_, _ = w.Write([]byte("OK"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConnectAMQP(url string) (Connection, error) {
|
func ConnectAMQP(url string) (Connection, error) {
|
||||||
return goamqp.NewFromURL(serviceName, url)
|
return goamqp.NewFromURL(serviceName, url)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockCache is a mock implementation for testing
|
||||||
|
type MockCache struct {
|
||||||
|
organizations map[string]*domain.Organization // keyed by orgId-name composite
|
||||||
|
apiKeys map[string]string // maps orgId-name to hashed key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockCache) OrganizationByAPIKey(plainKey string) *domain.Organization {
|
||||||
|
// Find organization by comparing plaintext key with stored hash
|
||||||
|
for compositeKey, hashedKey := range m.apiKeys {
|
||||||
|
if hash.CompareAPIKey(hashedKey, plainKey) {
|
||||||
|
return m.organizations[compositeKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketInitFunc_WithValidAPIKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
orgID := uuid.New()
|
||||||
|
org := &domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregate{
|
||||||
|
ID: eventsourced.IdFromString(orgID.String()),
|
||||||
|
},
|
||||||
|
Name: "Test Organization",
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey := "test-api-key-123"
|
||||||
|
hashedKey, err := hash.APIKey(apiKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
compositeKey := orgID.String() + "-test-key"
|
||||||
|
|
||||||
|
mockCache := &MockCache{
|
||||||
|
organizations: map[string]*domain.Organization{
|
||||||
|
compositeKey: org,
|
||||||
|
},
|
||||||
|
apiKeys: map[string]string{
|
||||||
|
compositeKey: hashedKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create InitFunc (simulating the WebSocket InitFunc logic)
|
||||||
|
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
|
||||||
|
// Extract API key from WebSocket connection_init payload
|
||||||
|
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
|
||||||
|
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
|
||||||
|
|
||||||
|
// Look up organization by API key (cache handles hash comparison)
|
||||||
|
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
|
||||||
|
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx, &initPayload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
ctx := context.Background()
|
||||||
|
initPayload := transport.InitPayload{
|
||||||
|
"X-Api-Key": apiKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCtx, resultPayload, err := initFunc(ctx, initPayload)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resultPayload)
|
||||||
|
|
||||||
|
// Check API key is in context
|
||||||
|
if value := resultCtx.Value(middleware.ApiKey); value != nil {
|
||||||
|
assert.Equal(t, apiKey, value.(string))
|
||||||
|
} else {
|
||||||
|
t.Fatal("API key not found in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check organization is in context
|
||||||
|
if value := resultCtx.Value(middleware.OrganizationKey); value != nil {
|
||||||
|
capturedOrg, ok := value.(domain.Organization)
|
||||||
|
require.True(t, ok, "Organization should be of correct type")
|
||||||
|
assert.Equal(t, org.Name, capturedOrg.Name)
|
||||||
|
assert.Equal(t, org.ID.String(), capturedOrg.ID.String())
|
||||||
|
} else {
|
||||||
|
t.Fatal("Organization not found in context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketInitFunc_WithInvalidAPIKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := &MockCache{
|
||||||
|
organizations: map[string]*domain.Organization{},
|
||||||
|
apiKeys: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey := "invalid-api-key"
|
||||||
|
|
||||||
|
// Create InitFunc
|
||||||
|
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
|
||||||
|
// Extract API key from WebSocket connection_init payload
|
||||||
|
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
|
||||||
|
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
|
||||||
|
|
||||||
|
// Look up organization by API key (cache handles hash comparison)
|
||||||
|
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
|
||||||
|
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx, &initPayload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
ctx := context.Background()
|
||||||
|
initPayload := transport.InitPayload{
|
||||||
|
"X-Api-Key": apiKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCtx, resultPayload, err := initFunc(ctx, initPayload)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resultPayload)
|
||||||
|
|
||||||
|
// Check API key is in context
|
||||||
|
if value := resultCtx.Value(middleware.ApiKey); value != nil {
|
||||||
|
assert.Equal(t, apiKey, value.(string))
|
||||||
|
} else {
|
||||||
|
t.Fatal("API key not found in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check organization is NOT in context (since API key is invalid)
|
||||||
|
value := resultCtx.Value(middleware.OrganizationKey)
|
||||||
|
assert.Nil(t, value, "Organization should not be set for invalid API key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketInitFunc_WithoutAPIKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := &MockCache{
|
||||||
|
organizations: map[string]*domain.Organization{},
|
||||||
|
apiKeys: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create InitFunc
|
||||||
|
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
|
||||||
|
// Extract API key from WebSocket connection_init payload
|
||||||
|
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
|
||||||
|
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
|
||||||
|
|
||||||
|
// Look up organization by API key (cache handles hash comparison)
|
||||||
|
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
|
||||||
|
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx, &initPayload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
ctx := context.Background()
|
||||||
|
initPayload := transport.InitPayload{}
|
||||||
|
|
||||||
|
resultCtx, resultPayload, err := initFunc(ctx, initPayload)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resultPayload)
|
||||||
|
|
||||||
|
// Check API key is NOT in context
|
||||||
|
value := resultCtx.Value(middleware.ApiKey)
|
||||||
|
assert.Nil(t, value, "API key should not be set when not provided")
|
||||||
|
|
||||||
|
// Check organization is NOT in context
|
||||||
|
value = resultCtx.Value(middleware.OrganizationKey)
|
||||||
|
assert.Nil(t, value, "Organization should not be set when API key is not provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketInitFunc_WithEmptyAPIKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := &MockCache{
|
||||||
|
organizations: map[string]*domain.Organization{},
|
||||||
|
apiKeys: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create InitFunc
|
||||||
|
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
|
||||||
|
// Extract API key from WebSocket connection_init payload
|
||||||
|
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
|
||||||
|
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
|
||||||
|
|
||||||
|
// Look up organization by API key (cache handles hash comparison)
|
||||||
|
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
|
||||||
|
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx, &initPayload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
ctx := context.Background()
|
||||||
|
initPayload := transport.InitPayload{
|
||||||
|
"X-Api-Key": "", // Empty string
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCtx, resultPayload, err := initFunc(ctx, initPayload)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resultPayload)
|
||||||
|
|
||||||
|
// Check API key is NOT in context (because empty string fails the condition)
|
||||||
|
value := resultCtx.Value(middleware.ApiKey)
|
||||||
|
assert.Nil(t, value, "API key should not be set when empty")
|
||||||
|
|
||||||
|
// Check organization is NOT in context
|
||||||
|
value = resultCtx.Value(middleware.OrganizationKey)
|
||||||
|
assert.Nil(t, value, "Organization should not be set when API key is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketInitFunc_WithWrongTypeAPIKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := &MockCache{
|
||||||
|
organizations: map[string]*domain.Organization{},
|
||||||
|
apiKeys: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create InitFunc
|
||||||
|
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
|
||||||
|
// Extract API key from WebSocket connection_init payload
|
||||||
|
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
|
||||||
|
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
|
||||||
|
|
||||||
|
// Look up organization by API key (cache handles hash comparison)
|
||||||
|
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
|
||||||
|
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx, &initPayload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test
|
||||||
|
ctx := context.Background()
|
||||||
|
initPayload := transport.InitPayload{
|
||||||
|
"X-Api-Key": 12345, // Wrong type (int instead of string)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCtx, resultPayload, err := initFunc(ctx, initPayload)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resultPayload)
|
||||||
|
|
||||||
|
// Check API key is NOT in context (type assertion fails)
|
||||||
|
value := resultCtx.Value(middleware.ApiKey)
|
||||||
|
assert.Nil(t, value, "API key should not be set when wrong type")
|
||||||
|
|
||||||
|
// Check organization is NOT in context
|
||||||
|
value = resultCtx.Value(middleware.OrganizationKey)
|
||||||
|
assert.Nil(t, value, "Organization should not be set when API key has wrong type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebSocketInitFunc_WithMultipleOrganizations(t *testing.T) {
|
||||||
|
// Setup - create multiple organizations
|
||||||
|
org1ID := uuid.New()
|
||||||
|
org1 := &domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregate{
|
||||||
|
ID: eventsourced.IdFromString(org1ID.String()),
|
||||||
|
},
|
||||||
|
Name: "Organization 1",
|
||||||
|
}
|
||||||
|
|
||||||
|
org2ID := uuid.New()
|
||||||
|
org2 := &domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregate{
|
||||||
|
ID: eventsourced.IdFromString(org2ID.String()),
|
||||||
|
},
|
||||||
|
Name: "Organization 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey1 := "api-key-org-1"
|
||||||
|
apiKey2 := "api-key-org-2"
|
||||||
|
hashedKey1, err := hash.APIKey(apiKey1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
hashedKey2, err := hash.APIKey(apiKey2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
compositeKey1 := org1ID.String() + "-key1"
|
||||||
|
compositeKey2 := org2ID.String() + "-key2"
|
||||||
|
|
||||||
|
mockCache := &MockCache{
|
||||||
|
organizations: map[string]*domain.Organization{
|
||||||
|
compositeKey1: org1,
|
||||||
|
compositeKey2: org2,
|
||||||
|
},
|
||||||
|
apiKeys: map[string]string{
|
||||||
|
compositeKey1: hashedKey1,
|
||||||
|
compositeKey2: hashedKey2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create InitFunc
|
||||||
|
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
|
||||||
|
// Extract API key from WebSocket connection_init payload
|
||||||
|
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
|
||||||
|
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
|
||||||
|
|
||||||
|
// Look up organization by API key (cache handles hash comparison)
|
||||||
|
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
|
||||||
|
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx, &initPayload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with first API key
|
||||||
|
ctx1 := context.Background()
|
||||||
|
initPayload1 := transport.InitPayload{
|
||||||
|
"X-Api-Key": apiKey1,
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCtx1, _, err := initFunc(ctx1, initPayload1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if value := resultCtx1.Value(middleware.OrganizationKey); value != nil {
|
||||||
|
capturedOrg, ok := value.(domain.Organization)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, org1.Name, capturedOrg.Name)
|
||||||
|
assert.Equal(t, org1.ID.String(), capturedOrg.ID.String())
|
||||||
|
} else {
|
||||||
|
t.Fatal("Organization 1 not found in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with second API key
|
||||||
|
ctx2 := context.Background()
|
||||||
|
initPayload2 := transport.InitPayload{
|
||||||
|
"X-Api-Key": apiKey2,
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCtx2, _, err := initFunc(ctx2, initPayload2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if value := resultCtx2.Value(middleware.OrganizationKey); value != nil {
|
||||||
|
capturedOrg, ok := value.(domain.Organization)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, org2.Name, capturedOrg.Name)
|
||||||
|
assert.Equal(t, org2.ID.String(), capturedOrg.ID.String())
|
||||||
|
} else {
|
||||||
|
t.Fatal("Organization 2 not found in context")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,8 @@ func (o *Organization) Apply(event eventsourced.Event) error {
|
|||||||
switch e := event.(type) {
|
switch e := event.(type) {
|
||||||
case *OrganizationAdded:
|
case *OrganizationAdded:
|
||||||
e.UpdateOrganization(o)
|
e.UpdateOrganization(o)
|
||||||
|
case *UserAddedToOrganization:
|
||||||
|
e.UpdateOrganization(o)
|
||||||
case *APIKeyAdded:
|
case *APIKeyAdded:
|
||||||
o.APIKeys = append(o.APIKeys, APIKey{
|
o.APIKeys = append(o.APIKeys, APIKey{
|
||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
@@ -36,6 +38,10 @@ func (o *Organization) Apply(event eventsourced.Event) error {
|
|||||||
})
|
})
|
||||||
o.ChangedBy = e.Initiator
|
o.ChangedBy = e.Initiator
|
||||||
o.ChangedAt = e.When()
|
o.ChangedAt = e.When()
|
||||||
|
case *APIKeyRemoved:
|
||||||
|
e.UpdateOrganization(o)
|
||||||
|
case *OrganizationRemoved:
|
||||||
|
e.UpdateOrganization(o)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unexpected event type: %+v", event)
|
return fmt.Errorf("unexpected event type: %+v", event)
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-2
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/hash"
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AddOrganization struct {
|
type AddOrganization struct {
|
||||||
@@ -34,6 +34,37 @@ func (a AddOrganization) Event(context.Context) eventsourced.Event {
|
|||||||
|
|
||||||
var _ eventsourced.Command = AddOrganization{}
|
var _ eventsourced.Command = AddOrganization{}
|
||||||
|
|
||||||
|
type AddUserToOrganization struct {
|
||||||
|
UserId string
|
||||||
|
Initiator string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AddUserToOrganization) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
|
||||||
|
if aggregate.Identity() == nil {
|
||||||
|
return fmt.Errorf("organization does not exist")
|
||||||
|
}
|
||||||
|
if len(a.UserId) == 0 {
|
||||||
|
return fmt.Errorf("userId is required")
|
||||||
|
}
|
||||||
|
// Check if user is already in the organization
|
||||||
|
org := aggregate.(*Organization)
|
||||||
|
for _, user := range org.Users {
|
||||||
|
if user == a.UserId {
|
||||||
|
return fmt.Errorf("user is already a member of this organization")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AddUserToOrganization) Event(context.Context) eventsourced.Event {
|
||||||
|
return &UserAddedToOrganization{
|
||||||
|
UserId: a.UserId,
|
||||||
|
Initiator: a.Initiator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ eventsourced.Command = AddUserToOrganization{}
|
||||||
|
|
||||||
type AddAPIKey struct {
|
type AddAPIKey struct {
|
||||||
Name string
|
Name string
|
||||||
Key string
|
Key string
|
||||||
@@ -56,9 +87,20 @@ func (a AddAPIKey) Validate(_ context.Context, aggregate eventsourced.Aggregate)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AddAPIKey) Event(context.Context) eventsourced.Event {
|
func (a AddAPIKey) Event(context.Context) eventsourced.Event {
|
||||||
|
// Hash the API key using bcrypt for secure storage
|
||||||
|
// Note: We can't return an error here, but bcrypt errors are extremely rare
|
||||||
|
// (only if system runs out of memory or bcrypt cost is invalid)
|
||||||
|
// We use a fixed cost of 12 which is always valid
|
||||||
|
hashedKey, err := hash.APIKey(a.Key)
|
||||||
|
if err != nil {
|
||||||
|
// This should never happen with bcrypt cost 12, but if it does,
|
||||||
|
// we'll store an empty hash which will fail validation later
|
||||||
|
hashedKey = ""
|
||||||
|
}
|
||||||
|
|
||||||
return &APIKeyAdded{
|
return &APIKeyAdded{
|
||||||
Name: a.Name,
|
Name: a.Name,
|
||||||
Key: hash.String(a.Key),
|
Key: hashedKey,
|
||||||
Refs: a.Refs,
|
Refs: a.Refs,
|
||||||
Read: a.Read,
|
Read: a.Read,
|
||||||
Publish: a.Publish,
|
Publish: a.Publish,
|
||||||
@@ -68,6 +110,57 @@ func (a AddAPIKey) Event(context.Context) eventsourced.Event {
|
|||||||
|
|
||||||
var _ eventsourced.Command = AddAPIKey{}
|
var _ eventsourced.Command = AddAPIKey{}
|
||||||
|
|
||||||
|
type RemoveAPIKey struct {
|
||||||
|
KeyName string
|
||||||
|
Initiator string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RemoveAPIKey) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
|
||||||
|
if aggregate.Identity() == nil {
|
||||||
|
return fmt.Errorf("organization does not exist")
|
||||||
|
}
|
||||||
|
org := aggregate.(*Organization)
|
||||||
|
found := false
|
||||||
|
for _, k := range org.APIKeys {
|
||||||
|
if k.Name == r.KeyName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("API key '%s' not found", r.KeyName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RemoveAPIKey) Event(context.Context) eventsourced.Event {
|
||||||
|
return &APIKeyRemoved{
|
||||||
|
KeyName: r.KeyName,
|
||||||
|
Initiator: r.Initiator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ eventsourced.Command = RemoveAPIKey{}
|
||||||
|
|
||||||
|
type RemoveOrganization struct {
|
||||||
|
Initiator string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RemoveOrganization) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
|
||||||
|
if aggregate.Identity() == nil {
|
||||||
|
return fmt.Errorf("organization does not exist")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RemoveOrganization) Event(context.Context) eventsourced.Event {
|
||||||
|
return &OrganizationRemoved{
|
||||||
|
Initiator: r.Initiator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ eventsourced.Command = RemoveOrganization{}
|
||||||
|
|
||||||
type UpdateSubGraph struct {
|
type UpdateSubGraph struct {
|
||||||
OrganizationId string
|
OrganizationId string
|
||||||
Ref string
|
Ref string
|
||||||
|
|||||||
+524
-10
@@ -2,12 +2,73 @@ package domain
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AddOrganization tests
|
||||||
|
|
||||||
|
func TestAddOrganization_Validate_Success(t *testing.T) {
|
||||||
|
cmd := AddOrganization{
|
||||||
|
Name: "Test Org",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{} // New organization with no identity
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddOrganization_Validate_AlreadyExists(t *testing.T) {
|
||||||
|
cmd := AddOrganization{
|
||||||
|
Name: "Test Org",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("existing-org-id"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddOrganization_Validate_EmptyName(t *testing.T) {
|
||||||
|
cmd := AddOrganization{
|
||||||
|
Name: "",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{}
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddOrganization_Event(t *testing.T) {
|
||||||
|
cmd := AddOrganization{
|
||||||
|
Name: "Test Org",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
event := cmd.Event(context.Background())
|
||||||
|
require.NotNil(t, event)
|
||||||
|
|
||||||
|
orgEvent, ok := event.(*OrganizationAdded)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "Test Org", orgEvent.Name)
|
||||||
|
assert.Equal(t, "user@example.com", orgEvent.Initiator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAPIKey tests
|
||||||
|
|
||||||
func TestAddAPIKey_Event(t *testing.T) {
|
func TestAddAPIKey_Event(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -24,7 +85,6 @@ func TestAddAPIKey_Event(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
fields fields
|
fields fields
|
||||||
args args
|
args args
|
||||||
want eventsourced.Event
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "event",
|
name: "event",
|
||||||
@@ -37,14 +97,6 @@ func TestAddAPIKey_Event(t *testing.T) {
|
|||||||
Initiator: "jim@example.org",
|
Initiator: "jim@example.org",
|
||||||
},
|
},
|
||||||
args: args{},
|
args: args{},
|
||||||
want: &APIKeyAdded{
|
|
||||||
Name: "test",
|
|
||||||
Key: "dXNfYWtfMTIzNDU2Nzg5MDEyMzQ1NuOwxEKY/BwUmvv0yJlvuSQnrkHkZJuTTKSVmRt4UrhV",
|
|
||||||
Refs: []string{"Example@dev"},
|
|
||||||
Read: true,
|
|
||||||
Publish: true,
|
|
||||||
Initiator: "jim@example.org",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -57,7 +109,469 @@ func TestAddAPIKey_Event(t *testing.T) {
|
|||||||
Publish: tt.fields.Publish,
|
Publish: tt.fields.Publish,
|
||||||
Initiator: tt.fields.Initiator,
|
Initiator: tt.fields.Initiator,
|
||||||
}
|
}
|
||||||
assert.Equalf(t, tt.want, a.Event(tt.args.in0), "Event(%v)", tt.args.in0)
|
event := a.Event(tt.args.in0)
|
||||||
|
require.NotNil(t, event)
|
||||||
|
|
||||||
|
// Cast to APIKeyAdded to verify fields
|
||||||
|
apiKeyEvent, ok := event.(*APIKeyAdded)
|
||||||
|
require.True(t, ok, "Event should be *APIKeyAdded")
|
||||||
|
|
||||||
|
// Verify non-key fields match exactly
|
||||||
|
assert.Equal(t, tt.fields.Name, apiKeyEvent.Name)
|
||||||
|
assert.Equal(t, tt.fields.Refs, apiKeyEvent.Refs)
|
||||||
|
assert.Equal(t, tt.fields.Read, apiKeyEvent.Read)
|
||||||
|
assert.Equal(t, tt.fields.Publish, apiKeyEvent.Publish)
|
||||||
|
assert.Equal(t, tt.fields.Initiator, apiKeyEvent.Initiator)
|
||||||
|
|
||||||
|
// Verify the key is hashed correctly (bcrypt format)
|
||||||
|
assert.True(t, strings.HasPrefix(apiKeyEvent.Key, "$2"), "Key should be bcrypt hashed")
|
||||||
|
assert.NotEqual(t, tt.fields.Key, apiKeyEvent.Key, "Key should be hashed, not plaintext")
|
||||||
|
|
||||||
|
// Verify the hash matches the original key
|
||||||
|
assert.True(t, hash.CompareAPIKey(apiKeyEvent.Key, tt.fields.Key), "Hashed key should match original")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddAPIKey_Validate_Success(t *testing.T) {
|
||||||
|
cmd := AddAPIKey{
|
||||||
|
Name: "production-key",
|
||||||
|
Key: "us_ak_1234567890123456",
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddAPIKey_Validate_OrganizationNotExists(t *testing.T) {
|
||||||
|
cmd := AddAPIKey{
|
||||||
|
Name: "production-key",
|
||||||
|
Key: "us_ak_1234567890123456",
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{} // No identity means it doesn't exist
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddAPIKey_Validate_DuplicateKeyName(t *testing.T) {
|
||||||
|
cmd := AddAPIKey{
|
||||||
|
Name: "existing-key",
|
||||||
|
Key: "us_ak_1234567890123456",
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{
|
||||||
|
Name: "existing-key",
|
||||||
|
Key: "hashed-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "already exist")
|
||||||
|
assert.Contains(t, err.Error(), "existing-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSubGraph tests
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_Success(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_MissingRef(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "ref is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_RefWhitespaceOnly(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: " ",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "ref is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_MissingService(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "service is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_ServiceWhitespaceOnly(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: " ",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "service is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_MissingSDL(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "SDL is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_SDLWhitespaceOnly(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: " \n\t ",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "SDL is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_MissingURL_NoExistingURL(t *testing.T) {
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: nil,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
Url: nil, // No existing URL
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "url is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_MissingURL_HasExistingURL(t *testing.T) {
|
||||||
|
existingURL := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: nil,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
Url: &existingURL, // Has existing URL, so nil is OK
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_EmptyURL_NoExistingURL(t *testing.T) {
|
||||||
|
emptyURL := ""
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &emptyURL,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
Url: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "url is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_URLWhitespaceOnly_NoExistingURL(t *testing.T) {
|
||||||
|
whitespaceURL := " "
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &whitespaceURL,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
Url: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "url is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_WrongAggregateType(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass wrong aggregate type
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not a SubGraph")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Event(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
wsURL := "ws://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
WSUrl: &wsURL,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
event := cmd.Event(context.Background())
|
||||||
|
require.NotNil(t, event)
|
||||||
|
|
||||||
|
subGraphEvent, ok := event.(*SubGraphUpdated)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "org-123", subGraphEvent.OrganizationId)
|
||||||
|
assert.Equal(t, "main", subGraphEvent.Ref)
|
||||||
|
assert.Equal(t, "users", subGraphEvent.Service)
|
||||||
|
assert.Equal(t, url, *subGraphEvent.Url)
|
||||||
|
assert.Equal(t, wsURL, *subGraphEvent.WSUrl)
|
||||||
|
assert.Equal(t, "type Query { hello: String }", subGraphEvent.Sdl)
|
||||||
|
assert.Equal(t, "user@example.com", subGraphEvent.Initiator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAPIKey tests
|
||||||
|
|
||||||
|
func TestRemoveAPIKey_Validate_Success(t *testing.T) {
|
||||||
|
cmd := RemoveAPIKey{
|
||||||
|
KeyName: "production-key",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{
|
||||||
|
Name: "production-key",
|
||||||
|
Key: "hashed-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAPIKey_Validate_OrganizationNotExists(t *testing.T) {
|
||||||
|
cmd := RemoveAPIKey{
|
||||||
|
KeyName: "production-key",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{} // No identity means it doesn't exist
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAPIKey_Validate_KeyNotFound(t *testing.T) {
|
||||||
|
cmd := RemoveAPIKey{
|
||||||
|
KeyName: "non-existent-key",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{
|
||||||
|
Name: "production-key",
|
||||||
|
Key: "hashed-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not found")
|
||||||
|
assert.Contains(t, err.Error(), "non-existent-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAPIKey_Event(t *testing.T) {
|
||||||
|
cmd := RemoveAPIKey{
|
||||||
|
KeyName: "production-key",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
event := cmd.Event(context.Background())
|
||||||
|
require.NotNil(t, event)
|
||||||
|
|
||||||
|
keyEvent, ok := event.(*APIKeyRemoved)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "production-key", keyEvent.KeyName)
|
||||||
|
assert.Equal(t, "user@example.com", keyEvent.Initiator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOrganization tests
|
||||||
|
|
||||||
|
func TestRemoveOrganization_Validate_Success(t *testing.T) {
|
||||||
|
cmd := RemoveOrganization{
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
Name: "Test Org",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveOrganization_Validate_OrganizationNotExists(t *testing.T) {
|
||||||
|
cmd := RemoveOrganization{
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{} // No identity means it doesn't exist
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveOrganization_Event(t *testing.T) {
|
||||||
|
cmd := RemoveOrganization{
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
event := cmd.Event(context.Background())
|
||||||
|
require.NotNil(t, event)
|
||||||
|
|
||||||
|
orgEvent, ok := event.(*OrganizationRemoved)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "user@example.com", orgEvent.Initiator)
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,24 @@ func (a *OrganizationAdded) UpdateOrganization(o *Organization) {
|
|||||||
o.ChangedAt = a.When()
|
o.ChangedAt = a.When()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserAddedToOrganization struct {
|
||||||
|
eventsourced.BaseEvent
|
||||||
|
UserId string `json:"userId"`
|
||||||
|
Initiator string `json:"initiator"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UserAddedToOrganization) UpdateOrganization(o *Organization) {
|
||||||
|
// Check if user is already in the organization
|
||||||
|
for _, user := range o.Users {
|
||||||
|
if user == a.UserId {
|
||||||
|
return // User already exists, no need to add
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.Users = append(o.Users, a.UserId)
|
||||||
|
o.ChangedBy = a.Initiator
|
||||||
|
o.ChangedAt = a.When()
|
||||||
|
}
|
||||||
|
|
||||||
type APIKeyAdded struct {
|
type APIKeyAdded struct {
|
||||||
eventsourced.BaseEvent
|
eventsourced.BaseEvent
|
||||||
OrganizationId string `json:"organizationId"`
|
OrganizationId string `json:"organizationId"`
|
||||||
@@ -34,6 +52,36 @@ func (a *APIKeyAdded) EnrichFromAggregate(aggregate eventsourced.Aggregate) {
|
|||||||
|
|
||||||
var _ eventsourced.EnrichableEvent = &APIKeyAdded{}
|
var _ eventsourced.EnrichableEvent = &APIKeyAdded{}
|
||||||
|
|
||||||
|
type APIKeyRemoved struct {
|
||||||
|
eventsourced.BaseEvent
|
||||||
|
KeyName string `json:"keyName"`
|
||||||
|
Initiator string `json:"initiator"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIKeyRemoved) UpdateOrganization(o *Organization) {
|
||||||
|
// Remove the API key from the organization
|
||||||
|
for i, key := range o.APIKeys {
|
||||||
|
if key.Name == a.KeyName {
|
||||||
|
o.APIKeys = append(o.APIKeys[:i], o.APIKeys[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.ChangedBy = a.Initiator
|
||||||
|
o.ChangedAt = a.When()
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationRemoved struct {
|
||||||
|
eventsourced.BaseEvent
|
||||||
|
Initiator string `json:"initiator"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *OrganizationRemoved) UpdateOrganization(o *Organization) {
|
||||||
|
// Mark organization as removed by clearing critical fields
|
||||||
|
// The aggregate will still exist in the event store, but it's logically deleted
|
||||||
|
o.ChangedBy = a.Initiator
|
||||||
|
o.ChangedAt = a.When()
|
||||||
|
}
|
||||||
|
|
||||||
type SubGraphUpdated struct {
|
type SubGraphUpdated struct {
|
||||||
eventsourced.BaseEvent
|
eventsourced.BaseEvent
|
||||||
OrganizationId string `json:"organizationId"`
|
OrganizationId string `json:"organizationId"`
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOrganizationAdded_UpdateOrganization(t *testing.T) {
|
||||||
|
event := &OrganizationAdded{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Name: "Test Organization",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
assert.Equal(t, "Test Organization", org.Name)
|
||||||
|
assert.Equal(t, []string{"user@example.com"}, org.Users)
|
||||||
|
assert.Equal(t, "user@example.com", org.CreatedBy)
|
||||||
|
assert.Equal(t, "user@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.CreatedAt)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserAddedToOrganization_UpdateOrganization(t *testing.T) {
|
||||||
|
event := &UserAddedToOrganization{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UserId: "new-user@example.com",
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
Users: []string{"existing-user@example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
assert.Len(t, org.Users, 2)
|
||||||
|
assert.Contains(t, org.Users, "existing-user@example.com")
|
||||||
|
assert.Contains(t, org.Users, "new-user@example.com")
|
||||||
|
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserAddedToOrganization_UpdateOrganization_DuplicateUser(t *testing.T) {
|
||||||
|
event := &UserAddedToOrganization{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UserId: "existing-user@example.com",
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
Users: []string{"existing-user@example.com"},
|
||||||
|
ChangedBy: "previous-admin@example.com",
|
||||||
|
}
|
||||||
|
originalChangedBy := org.ChangedBy
|
||||||
|
originalChangedAt := org.ChangedAt
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
// User should not be added twice
|
||||||
|
assert.Len(t, org.Users, 1)
|
||||||
|
assert.Equal(t, "existing-user@example.com", org.Users[0])
|
||||||
|
|
||||||
|
// ChangedBy and ChangedAt should NOT be updated when user already exists (idempotent)
|
||||||
|
assert.Equal(t, originalChangedBy, org.ChangedBy)
|
||||||
|
assert.Equal(t, originalChangedAt, org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyRemoved_UpdateOrganization(t *testing.T) {
|
||||||
|
event := &APIKeyRemoved{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
KeyName: "production-key",
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{Name: "dev-key", Key: "hashed-key-1"},
|
||||||
|
{Name: "production-key", Key: "hashed-key-2"},
|
||||||
|
{Name: "staging-key", Key: "hashed-key-3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
assert.Len(t, org.APIKeys, 2)
|
||||||
|
assert.Equal(t, "dev-key", org.APIKeys[0].Name)
|
||||||
|
assert.Equal(t, "staging-key", org.APIKeys[1].Name)
|
||||||
|
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyRemoved_UpdateOrganization_KeyNotFound(t *testing.T) {
|
||||||
|
event := &APIKeyRemoved{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
KeyName: "non-existent-key",
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{Name: "dev-key", Key: "hashed-key-1"},
|
||||||
|
{Name: "production-key", Key: "hashed-key-2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
// No keys should be removed
|
||||||
|
assert.Len(t, org.APIKeys, 2)
|
||||||
|
assert.Equal(t, "dev-key", org.APIKeys[0].Name)
|
||||||
|
assert.Equal(t, "production-key", org.APIKeys[1].Name)
|
||||||
|
|
||||||
|
// But metadata should still be updated
|
||||||
|
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyRemoved_UpdateOrganization_OnlyKey(t *testing.T) {
|
||||||
|
event := &APIKeyRemoved{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
KeyName: "only-key",
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{Name: "only-key", Key: "hashed-key"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
// All keys should be removed
|
||||||
|
assert.Len(t, org.APIKeys, 0)
|
||||||
|
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrganizationRemoved_UpdateOrganization(t *testing.T) {
|
||||||
|
event := &OrganizationRemoved{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
Name: "Test Organization",
|
||||||
|
Users: []string{"user1@example.com", "user2@example.com"},
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{Name: "key1", Key: "hashed-key-1"},
|
||||||
|
},
|
||||||
|
CreatedBy: "creator@example.com",
|
||||||
|
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
// Organization data remains (soft delete), but metadata is updated
|
||||||
|
assert.Equal(t, "Test Organization", org.Name)
|
||||||
|
assert.Len(t, org.Users, 2)
|
||||||
|
assert.Len(t, org.APIKeys, 1)
|
||||||
|
|
||||||
|
// Metadata should be updated to reflect removal
|
||||||
|
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyAdded_EnrichFromAggregate(t *testing.T) {
|
||||||
|
orgId := "org-123"
|
||||||
|
aggregate := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgId),
|
||||||
|
}
|
||||||
|
|
||||||
|
event := &APIKeyAdded{
|
||||||
|
Name: "test-key",
|
||||||
|
Key: "hashed-key",
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
event.EnrichFromAggregate(aggregate)
|
||||||
|
|
||||||
|
assert.Equal(t, orgId, event.OrganizationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubGraphUpdated_Event(t *testing.T) {
|
||||||
|
// Verify SubGraphUpdated event structure
|
||||||
|
url := "http://service.example.com"
|
||||||
|
wsUrl := "ws://service.example.com"
|
||||||
|
|
||||||
|
event := &SubGraphUpdated{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users-service",
|
||||||
|
Url: &url,
|
||||||
|
WSUrl: &wsUrl,
|
||||||
|
Sdl: "type Query { user: User }",
|
||||||
|
Initiator: "system",
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotNil(t, event)
|
||||||
|
assert.Equal(t, "org-123", event.OrganizationId)
|
||||||
|
assert.Equal(t, "main", event.Ref)
|
||||||
|
assert.Equal(t, "users-service", event.Service)
|
||||||
|
assert.Equal(t, url, *event.Url)
|
||||||
|
assert.Equal(t, wsUrl, *event.WSUrl)
|
||||||
|
assert.Equal(t, "type Query { user: User }", event.Sdl)
|
||||||
|
assert.Equal(t, "system", event.Initiator)
|
||||||
|
}
|
||||||
@@ -1,79 +1,80 @@
|
|||||||
module gitlab.com/unboundsoftware/schemas
|
module gitea.unbound.se/unboundsoftware/schemas
|
||||||
|
|
||||||
go 1.24.4
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.74
|
github.com/99designs/gqlgen v0.17.86
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/Khan/genqlient v0.8.1
|
github.com/Khan/genqlient v0.8.1
|
||||||
github.com/alecthomas/kong v1.11.0
|
github.com/alecthomas/kong v1.13.0
|
||||||
github.com/apex/log v1.9.0
|
github.com/apex/log v1.9.0
|
||||||
github.com/auth0/go-jwt-middleware/v2 v2.3.0
|
github.com/auth0/go-jwt-middleware/v2 v2.3.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pressly/goose/v3 v3.24.3
|
github.com/pressly/goose/v3 v3.26.0
|
||||||
github.com/rs/cors v1.11.1
|
github.com/rs/cors v1.11.1
|
||||||
github.com/sparetimecoders/goamqp v0.3.2
|
github.com/sparetimecoders/goamqp v0.3.3
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27
|
github.com/vektah/gqlparser/v2 v2.5.31
|
||||||
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.189
|
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242
|
||||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.1
|
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.0
|
||||||
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2
|
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.3
|
||||||
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0
|
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0
|
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0
|
||||||
go.opentelemetry.io/otel v1.36.0
|
go.opentelemetry.io/otel v1.39.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0
|
||||||
go.opentelemetry.io/otel/log v0.12.2
|
go.opentelemetry.io/otel/log v0.15.0
|
||||||
go.opentelemetry.io/otel/sdk v1.36.0
|
go.opentelemetry.io/otel/sdk v1.39.0
|
||||||
go.opentelemetry.io/otel/sdk/log v0.12.2
|
go.opentelemetry.io/otel/sdk/log v0.15.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.36.0
|
go.opentelemetry.io/otel/sdk/metric v1.39.0
|
||||||
go.opentelemetry.io/otel/trace v1.36.0
|
go.opentelemetry.io/otel/trace v1.39.0
|
||||||
|
golang.org/x/crypto v0.47.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||||
github.com/buger/jsonparser v1.1.1 // indirect
|
github.com/buger/jsonparser v1.1.1 // indirect
|
||||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/gorilla/websocket v1.5.1 // indirect
|
github.com/gorilla/websocket v1.5.1 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
github.com/sosodev/duration v1.3.1 // indirect
|
github.com/sosodev/duration v1.3.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/tidwall/gjson v1.17.0 // indirect
|
github.com/tidwall/gjson v1.17.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/urfave/cli/v2 v2.27.6 // indirect
|
github.com/urfave/cli/v3 v3.6.1 // indirect
|
||||||
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 // indirect
|
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
|
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/mod v0.24.0 // indirect
|
golang.org/x/mod v0.31.0 // indirect
|
||||||
golang.org/x/net v0.40.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sync v0.14.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/text v0.33.0 // indirect
|
||||||
golang.org/x/tools v0.33.0 // indirect
|
golang.org/x/tools v0.40.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
google.golang.org/grpc v1.72.1 // indirect
|
google.golang.org/grpc v1.77.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/99designs/gqlgen v0.17.74 h1:1FuVtkXxOc87xpKio3f6sohREmec+Jvy86PcYOuwgWo=
|
github.com/99designs/gqlgen v0.17.86 h1:C8N3UTa5heXX6twl+b0AJyGkTwYL6dNmFrgZNLRcU6w=
|
||||||
github.com/99designs/gqlgen v0.17.74/go.mod h1:a+iR6mfRLNRp++kDpooFHiPWYiWX3Yu1BIilQRHgh10=
|
github.com/99designs/gqlgen v0.17.86/go.mod h1:KTrPl+vHA1IUzNlh4EYkl7+tcErL3MgKnhHrBcV74Fw=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
|
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
|
||||||
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
|
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/kong v1.11.0 h1:y++1gI7jf8O7G7l4LZo5ASFhrhJvzc+WgF/arranEmM=
|
github.com/alecthomas/kong v1.13.0 h1:5e/7XC3ugvhP1DQBmTS+WuHtCbcv44hsohMgcvVxSrA=
|
||||||
github.com/alecthomas/kong v1.11.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
|
github.com/alecthomas/kong v1.13.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
|
||||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||||
@@ -27,18 +27,16 @@ github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy
|
|||||||
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
|
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||||
github.com/auth0/go-jwt-middleware/v2 v2.3.0 h1:4QREj6cS3d8dS05bEm443jhnqQF97FX9sMBeWqnNRzE=
|
github.com/auth0/go-jwt-middleware/v2 v2.3.1 h1:lbDyWE9aLydb3zrank+Gufb9qGJN9u//7EbJK07pRrw=
|
||||||
github.com/auth0/go-jwt-middleware/v2 v2.3.0/go.mod h1:dL4ObBs1/dj4/W4cYxd8rqAdDGXYyd5rqbpMIxcbVrU=
|
github.com/auth0/go-jwt-middleware/v2 v2.3.1/go.mod h1:mqVr0gdB5zuaFyQFWMJH/c/2hehNjbYUD4i8Dpyf+Hc=
|
||||||
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
||||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -50,17 +48,19 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
|
|||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
@@ -72,8 +72,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
@@ -85,6 +85,7 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht
|
|||||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||||
|
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -115,8 +116,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM=
|
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
|
||||||
github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E=
|
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
@@ -126,12 +127,10 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
|||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
|
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
|
||||||
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||||
github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
|
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
||||||
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
@@ -142,13 +141,15 @@ github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h
|
|||||||
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
|
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
|
||||||
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
|
||||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||||
github.com/sparetimecoders/goamqp v0.3.2 h1:XdlyUBAJS5RcURw+SnnPjPJJuofddZwQsjAf05VPXvI=
|
github.com/sparetimecoders/goamqp v0.3.3 h1:z/nfTPmrjeU/rIVuNOgsVLCimp3WFoNFvS3ZzXRJ6HE=
|
||||||
github.com/sparetimecoders/goamqp v0.3.2/go.mod h1:W9NRCpWLE+Vruv2dcRSbszNil2O826d2Nv6kAkETW5o=
|
github.com/sparetimecoders/goamqp v0.3.3/go.mod h1:W9NRCpWLE+Vruv2dcRSbszNil2O826d2Nv6kAkETW5o=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
|
||||||
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
@@ -166,93 +167,93 @@ github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj
|
|||||||
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
||||||
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||||
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
||||||
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
|
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
|
||||||
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
|
||||||
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk=
|
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk=
|
||||||
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE=
|
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE=
|
||||||
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.189 h1:vcMuks7xty+2LbNpBYuUtBhUUA8jI1/Y2n24fGMRSCg=
|
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242 h1:0ieQmRxYz0nbJEbaaA4Cx2RPcxlomhQ8KI31uuevWx0=
|
||||||
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.189/go.mod h1:DaBrBCMgKGd3t7zg7z11jKm+0mVJiesr/IQCRG9qgP0=
|
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.242/go.mod h1:mX25ASEQiKamxaFSK6NZihh0oDCigIuzro30up4mFH4=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.0 h1:TdBJnrnrxJrPhC4i6KTFUElZa3k/fFXiGwg0sds5aAo=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.0/go.mod h1:VauAph7uCvEakYNdHkkSAoOOGKvEuUA/uhsR376ThbI=
|
||||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.1 h1:MGHH2Uxp68J9i4V3/3vApB6gjBUjn6RjiPHhbc8Wsno=
|
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.3 h1:0HbDHF4sHfoyDrbPLMFWvsQLbTl2ITrpI9PjDIZsV1Y=
|
||||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.1/go.mod h1:clGBkdpFWb5/27aLOhJ6+DB15enJf+T4J5lR6X0lqAs=
|
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.3/go.mod h1:LrA7I7etRmhIC1PjO8c26BHm+gWsy2rC3eSMe5+XUWE=
|
||||||
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2 h1:8sCnThNHEPB3BQomcJ7u6fmc2t043fAZSMmVPDDbQOs=
|
|
||||||
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2/go.mod h1:KeLn3U67hxbdFLfeXd0c0LI/r1C5rijbWrfNdARWe98=
|
|
||||||
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0 h1:pUJzMpNPX0GVsffRZXlpKR1d7Ws96KTxJwbLFPpASSc=
|
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0 h1:pUJzMpNPX0GVsffRZXlpKR1d7Ws96KTxJwbLFPpASSc=
|
||||||
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0/go.mod h1:WgPrZhyCbsZ3TG2tPUbh2MUjOEaANJjsWi/0hlIwRVU=
|
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0/go.mod h1:WgPrZhyCbsZ3TG2tPUbh2MUjOEaANJjsWi/0hlIwRVU=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0 h1:EMIiYTms4Z4m3bBuKp1VmMNRLZcl6j4YbvOPL1IhlWo=
|
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0 h1:eypSOd+0txRKCXPNyqLPsbSfA0jULgJcGmSAdFAnrCM=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.11.0/go.mod h1:DIEZmUR7tzuOOVUTDKvkGWtYWSHFV18Qg8+GMb8wPJw=
|
go.opentelemetry.io/contrib/bridges/otelslog v0.14.0/go.mod h1:CRGvIBL/aAxpQU34ZxyQVFlovVcp67s4cAmQu8Jh9mc=
|
||||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 h1:gAU726w9J8fwr4qRDqu1GYMNNs4gXrU+Pv20/N1UpB4=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0 h1:nKP4Z2ejtHn3yShBb+2KawiXgpn8In5cT7aO2wXuOTE=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.39.0/go.mod h1:NwjeBbNigsO4Aj9WgM0C+cKIrxsZUaRmZUO7A8I7u8o=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 h1:12vMqzLLNZtXuXbJhSENRg+Vvx+ynNilV8twBLBsXMY=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0 h1:0BSddrtQqLEylcErkeFrJBmwFzcqfQq9+/uxfTZq+HE=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2/go.mod h1:ZccPZoPOoq8x3Trik/fCsba7DEYDUnN6yX79pgp2BUQ=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.15.0/go.mod h1:87sjYuAPzaRCtdd09GU5gM1U9wQLrrcYrm77mh5EBoc=
|
||||||
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
|
go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY=
|
||||||
go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E=
|
go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4=
|
||||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.12.2 h1:yNoETvTByVKi7wHvYS6HMcZrN5hFLD7I++1xIZ/k6W0=
|
go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.12.2/go.mod h1:DcpdmUXHJgSqN/dh+XMWa7Vf89u9ap0/AAk/XGLnEzY=
|
go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc h1:uqxdywfHqqCl6LmZzI3pUnXT1RGFYyUgxj0AkWPFxi0=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.0.0-20250521073539-a85ae98dcedc/go.mod h1:TY/N/FT7dmFrP/r5ym3g0yysP1DefqGpAZr4f82P0dE=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
|
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
@@ -267,11 +268,11 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y=
|
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||||
modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs=
|
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
|
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
|
||||||
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
|
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
package graph
|
package graph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/model"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ToGqlOrganizations(orgs []domain.Organization) []*model.Organization {
|
func ToGqlOrganizations(orgs []domain.Organization) []*model.Organization {
|
||||||
@@ -38,7 +38,7 @@ func ToGqlAPIKeys(keys []domain.APIKey) []*model.APIKey {
|
|||||||
result[i] = &model.APIKey{
|
result[i] = &model.APIKey{
|
||||||
ID: apiKeyId(k.OrganizationId, k.Name),
|
ID: apiKeyId(k.OrganizationId, k.Name),
|
||||||
Name: k.Name,
|
Name: k.Name,
|
||||||
Key: &k.Key,
|
Key: nil, // Never return the hashed key - only return plaintext on creation
|
||||||
Organization: nil,
|
Organization: nil,
|
||||||
Refs: k.Refs,
|
Refs: k.Refs,
|
||||||
Read: k.Read,
|
Read: k.Read,
|
||||||
|
|||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandExecutor is an interface for executing external commands
|
||||||
|
// This allows for mocking in tests
|
||||||
|
type CommandExecutor interface {
|
||||||
|
Execute(name string, args ...string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultCommandExecutor implements CommandExecutor using os/exec
|
||||||
|
type DefaultCommandExecutor struct{}
|
||||||
|
|
||||||
|
// Execute runs a command and returns its combined output
|
||||||
|
func (e *DefaultCommandExecutor) Execute(name string, args ...string) ([]byte, error) {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
return cmd.CombinedOutput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCosmoRouterConfig generates a Cosmo Router execution config from subgraphs
|
||||||
|
// using the official wgc CLI tool via npx
|
||||||
|
func GenerateCosmoRouterConfig(subGraphs []*model.SubGraph) (string, error) {
|
||||||
|
return GenerateCosmoRouterConfigWithExecutor(subGraphs, &DefaultCommandExecutor{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCosmoRouterConfigWithExecutor generates a Cosmo Router execution config from subgraphs
|
||||||
|
// using the provided command executor (useful for testing)
|
||||||
|
func GenerateCosmoRouterConfigWithExecutor(subGraphs []*model.SubGraph, executor CommandExecutor) (string, error) {
|
||||||
|
if len(subGraphs) == 0 {
|
||||||
|
return "", fmt.Errorf("no subgraphs provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temporary directory for composition
|
||||||
|
tmpDir, err := os.MkdirTemp("", "cosmo-compose-*")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create temp dir: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
// Write each subgraph SDL to a file
|
||||||
|
type SubgraphConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
RoutingURL string `yaml:"routing_url,omitempty"`
|
||||||
|
Schema map[string]string `yaml:"schema"`
|
||||||
|
Subscription map[string]interface{} `yaml:"subscription,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputConfig struct {
|
||||||
|
Version int `yaml:"version"`
|
||||||
|
Subgraphs []SubgraphConfig `yaml:"subgraphs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
inputConfig := InputConfig{
|
||||||
|
Version: 1,
|
||||||
|
Subgraphs: make([]SubgraphConfig, 0, len(subGraphs)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sg := range subGraphs {
|
||||||
|
// Write SDL to a temp file
|
||||||
|
schemaFile := filepath.Join(tmpDir, fmt.Sprintf("%s.graphql", sg.Service))
|
||||||
|
if err := os.WriteFile(schemaFile, []byte(sg.Sdl), 0o644); err != nil {
|
||||||
|
return "", fmt.Errorf("write schema file for %s: %w", sg.Service, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
subgraphCfg := SubgraphConfig{
|
||||||
|
Name: sg.Service,
|
||||||
|
Schema: map[string]string{
|
||||||
|
"file": schemaFile,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if sg.URL != nil {
|
||||||
|
subgraphCfg.RoutingURL = *sg.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
if sg.WsURL != nil {
|
||||||
|
subgraphCfg.Subscription = map[string]interface{}{
|
||||||
|
"url": *sg.WsURL,
|
||||||
|
"protocol": "ws",
|
||||||
|
"websocket_subprotocol": "graphql-ws",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputConfig.Subgraphs = append(inputConfig.Subgraphs, subgraphCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write input config YAML
|
||||||
|
inputFile := filepath.Join(tmpDir, "input.yaml")
|
||||||
|
inputYAML, err := yaml.Marshal(inputConfig)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("marshal input config: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(inputFile, inputYAML, 0o644); err != nil {
|
||||||
|
return "", fmt.Errorf("write input config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute wgc router compose
|
||||||
|
// wgc is installed globally in the Docker image
|
||||||
|
outputFile := filepath.Join(tmpDir, "config.json")
|
||||||
|
output, err := executor.Execute("wgc", "router", "compose",
|
||||||
|
"--input", inputFile,
|
||||||
|
"--out", outputFile,
|
||||||
|
"--suppress-warnings",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("wgc router compose failed: %w\nOutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the generated config
|
||||||
|
configJSON, err := os.ReadFile(outputFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read output config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(configJSON), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockCommandExecutor implements CommandExecutor for testing
|
||||||
|
type MockCommandExecutor struct {
|
||||||
|
// CallCount tracks how many times Execute was called
|
||||||
|
CallCount int
|
||||||
|
// LastArgs stores the arguments from the last call
|
||||||
|
LastArgs []string
|
||||||
|
// Error can be set to simulate command failures
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute mocks the wgc command by generating a realistic config.json file
|
||||||
|
func (m *MockCommandExecutor) Execute(name string, args ...string) ([]byte, error) {
|
||||||
|
m.CallCount++
|
||||||
|
m.LastArgs = append([]string{name}, args...)
|
||||||
|
|
||||||
|
if m.Error != nil {
|
||||||
|
return nil, m.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the input file to understand what subgraphs we're composing
|
||||||
|
var inputFile, outputFile string
|
||||||
|
for i, arg := range args {
|
||||||
|
if arg == "--input" && i+1 < len(args) {
|
||||||
|
inputFile = args[i+1]
|
||||||
|
}
|
||||||
|
if arg == "--out" && i+1 < len(args) {
|
||||||
|
outputFile = args[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if inputFile == "" || outputFile == "" {
|
||||||
|
return nil, fmt.Errorf("missing required arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the input YAML to get subgraph information
|
||||||
|
inputData, err := os.ReadFile(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read input file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var input struct {
|
||||||
|
Version int `yaml:"version"`
|
||||||
|
Subgraphs []struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
RoutingURL string `yaml:"routing_url,omitempty"`
|
||||||
|
Schema map[string]string `yaml:"schema"`
|
||||||
|
Subscription map[string]interface{} `yaml:"subscription,omitempty"`
|
||||||
|
} `yaml:"subgraphs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(inputData, &input); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse input YAML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a realistic Cosmo Router config based on the input
|
||||||
|
config := map[string]interface{}{
|
||||||
|
"version": "mock-version-uuid",
|
||||||
|
"subgraphs": func() []map[string]interface{} {
|
||||||
|
subgraphs := make([]map[string]interface{}, len(input.Subgraphs))
|
||||||
|
for i, sg := range input.Subgraphs {
|
||||||
|
subgraph := map[string]interface{}{
|
||||||
|
"id": fmt.Sprintf("mock-id-%d", i),
|
||||||
|
"name": sg.Name,
|
||||||
|
}
|
||||||
|
if sg.RoutingURL != "" {
|
||||||
|
subgraph["routingUrl"] = sg.RoutingURL
|
||||||
|
}
|
||||||
|
subgraphs[i] = subgraph
|
||||||
|
}
|
||||||
|
return subgraphs
|
||||||
|
}(),
|
||||||
|
"engineConfig": map[string]interface{}{
|
||||||
|
"graphqlSchema": generateMockSchema(input.Subgraphs),
|
||||||
|
"datasourceConfigurations": func() []map[string]interface{} {
|
||||||
|
dsConfigs := make([]map[string]interface{}, len(input.Subgraphs))
|
||||||
|
for i, sg := range input.Subgraphs {
|
||||||
|
// Read SDL from file
|
||||||
|
sdl := ""
|
||||||
|
if schemaFile, ok := sg.Schema["file"]; ok {
|
||||||
|
if sdlData, err := os.ReadFile(schemaFile); err == nil {
|
||||||
|
sdl = string(sdlData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dsConfig := map[string]interface{}{
|
||||||
|
"id": fmt.Sprintf("datasource-%d", i),
|
||||||
|
"kind": "GRAPHQL",
|
||||||
|
"customGraphql": map[string]interface{}{
|
||||||
|
"federation": map[string]interface{}{
|
||||||
|
"enabled": true,
|
||||||
|
"serviceSdl": sdl,
|
||||||
|
},
|
||||||
|
"subscription": func() map[string]interface{} {
|
||||||
|
if len(sg.Subscription) > 0 {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"enabled": true,
|
||||||
|
"url": map[string]interface{}{
|
||||||
|
"staticVariableContent": sg.Subscription["url"],
|
||||||
|
},
|
||||||
|
"protocol": sg.Subscription["protocol"],
|
||||||
|
"websocketSubprotocol": sg.Subscription["websocket_subprotocol"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"enabled": false,
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dsConfigs[i] = dsConfig
|
||||||
|
}
|
||||||
|
return dsConfigs
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the config to the output file
|
||||||
|
configJSON, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(outputFile, configJSON, 0o644); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write output file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte("Success"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateMockSchema creates a simple merged schema from subgraphs
|
||||||
|
func generateMockSchema(subgraphs []struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
RoutingURL string `yaml:"routing_url,omitempty"`
|
||||||
|
Schema map[string]string `yaml:"schema"`
|
||||||
|
Subscription map[string]interface{} `yaml:"subscription,omitempty"`
|
||||||
|
},
|
||||||
|
) string {
|
||||||
|
schema := strings.Builder{}
|
||||||
|
schema.WriteString("schema {\n query: Query\n")
|
||||||
|
|
||||||
|
// Check if any subgraph has subscriptions
|
||||||
|
hasSubscriptions := false
|
||||||
|
for _, sg := range subgraphs {
|
||||||
|
if len(sg.Subscription) > 0 {
|
||||||
|
hasSubscriptions = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasSubscriptions {
|
||||||
|
schema.WriteString(" subscription: Subscription\n")
|
||||||
|
}
|
||||||
|
schema.WriteString("}\n\n")
|
||||||
|
|
||||||
|
// Add types by reading SDL files
|
||||||
|
for _, sg := range subgraphs {
|
||||||
|
if schemaFile, ok := sg.Schema["file"]; ok {
|
||||||
|
if sdlData, err := os.ReadFile(schemaFile); err == nil {
|
||||||
|
schema.WriteString(string(sdlData))
|
||||||
|
schema.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateCosmoRouterConfig(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
subGraphs []*model.SubGraph
|
||||||
|
wantErr bool
|
||||||
|
validate func(t *testing.T, config string)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single subgraph with all fields",
|
||||||
|
subGraphs: []*model.SubGraph{
|
||||||
|
{
|
||||||
|
Service: "test-service",
|
||||||
|
URL: stringPtr("http://localhost:4001/query"),
|
||||||
|
WsURL: stringPtr("ws://localhost:4001/query"),
|
||||||
|
Sdl: "type Query { test: String }",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
validate: func(t *testing.T, config string) {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(config), &result)
|
||||||
|
require.NoError(t, err, "Config should be valid JSON")
|
||||||
|
|
||||||
|
// Version is a UUID string from wgc
|
||||||
|
version, ok := result["version"].(string)
|
||||||
|
require.True(t, ok, "Version should be a string")
|
||||||
|
assert.NotEmpty(t, version, "Version should not be empty")
|
||||||
|
|
||||||
|
subgraphs, ok := result["subgraphs"].([]interface{})
|
||||||
|
require.True(t, ok, "subgraphs should be an array")
|
||||||
|
require.Len(t, subgraphs, 1, "Should have 1 subgraph")
|
||||||
|
|
||||||
|
sg := subgraphs[0].(map[string]interface{})
|
||||||
|
assert.Equal(t, "test-service", sg["name"])
|
||||||
|
assert.Equal(t, "http://localhost:4001/query", sg["routingUrl"])
|
||||||
|
|
||||||
|
// Check that datasource configurations include subscription settings
|
||||||
|
engineConfig, ok := result["engineConfig"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Should have engineConfig")
|
||||||
|
|
||||||
|
dsConfigs, ok := engineConfig["datasourceConfigurations"].([]interface{})
|
||||||
|
require.True(t, ok && len(dsConfigs) > 0, "Should have datasource configurations")
|
||||||
|
|
||||||
|
ds := dsConfigs[0].(map[string]interface{})
|
||||||
|
customGraphql, ok := ds["customGraphql"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Should have customGraphql config")
|
||||||
|
|
||||||
|
subscription, ok := customGraphql["subscription"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Should have subscription config")
|
||||||
|
assert.True(t, subscription["enabled"].(bool), "Subscription should be enabled")
|
||||||
|
|
||||||
|
subUrl, ok := subscription["url"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Should have subscription URL")
|
||||||
|
assert.Equal(t, "ws://localhost:4001/query", subUrl["staticVariableContent"])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple subgraphs",
|
||||||
|
subGraphs: []*model.SubGraph{
|
||||||
|
{
|
||||||
|
Service: "service-1",
|
||||||
|
URL: stringPtr("http://localhost:4001/query"),
|
||||||
|
Sdl: "type Query { field1: String }",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Service: "service-2",
|
||||||
|
URL: stringPtr("http://localhost:4002/query"),
|
||||||
|
Sdl: "type Query { field2: String }",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Service: "service-3",
|
||||||
|
URL: stringPtr("http://localhost:4003/query"),
|
||||||
|
WsURL: stringPtr("ws://localhost:4003/query"),
|
||||||
|
Sdl: "type Subscription { updates: String }",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
validate: func(t *testing.T, config string) {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(config), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
subgraphs := result["subgraphs"].([]interface{})
|
||||||
|
assert.Len(t, subgraphs, 3, "Should have 3 subgraphs")
|
||||||
|
|
||||||
|
// Check service names
|
||||||
|
sg1 := subgraphs[0].(map[string]interface{})
|
||||||
|
assert.Equal(t, "service-1", sg1["name"])
|
||||||
|
|
||||||
|
sg3 := subgraphs[2].(map[string]interface{})
|
||||||
|
assert.Equal(t, "service-3", sg3["name"])
|
||||||
|
|
||||||
|
// Check that datasource configurations include subscription for service-3
|
||||||
|
engineConfig, ok := result["engineConfig"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Should have engineConfig")
|
||||||
|
|
||||||
|
dsConfigs, ok := engineConfig["datasourceConfigurations"].([]interface{})
|
||||||
|
require.True(t, ok && len(dsConfigs) == 3, "Should have 3 datasource configurations")
|
||||||
|
|
||||||
|
// Find service-3's datasource config (should have subscription enabled)
|
||||||
|
ds3 := dsConfigs[2].(map[string]interface{})
|
||||||
|
customGraphql, ok := ds3["customGraphql"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Service-3 should have customGraphql config")
|
||||||
|
|
||||||
|
subscription, ok := customGraphql["subscription"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Service-3 should have subscription config")
|
||||||
|
assert.True(t, subscription["enabled"].(bool), "Service-3 subscription should be enabled")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subgraph with no URL",
|
||||||
|
subGraphs: []*model.SubGraph{
|
||||||
|
{
|
||||||
|
Service: "test-service",
|
||||||
|
URL: nil,
|
||||||
|
WsURL: nil,
|
||||||
|
Sdl: "type Query { test: String }",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
validate: func(t *testing.T, config string) {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(config), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
subgraphs := result["subgraphs"].([]interface{})
|
||||||
|
sg := subgraphs[0].(map[string]interface{})
|
||||||
|
|
||||||
|
// Should not have routing URL when URL is nil
|
||||||
|
_, hasRoutingURL := sg["routingUrl"]
|
||||||
|
assert.False(t, hasRoutingURL, "Should not have routingUrl when URL is nil")
|
||||||
|
|
||||||
|
// Check datasource configurations don't have subscription enabled
|
||||||
|
engineConfig, ok := result["engineConfig"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Should have engineConfig")
|
||||||
|
|
||||||
|
dsConfigs, ok := engineConfig["datasourceConfigurations"].([]interface{})
|
||||||
|
require.True(t, ok && len(dsConfigs) > 0, "Should have datasource configurations")
|
||||||
|
|
||||||
|
ds := dsConfigs[0].(map[string]interface{})
|
||||||
|
customGraphql, ok := ds["customGraphql"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Should have customGraphql config")
|
||||||
|
|
||||||
|
subscription, ok := customGraphql["subscription"].(map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
// wgc always enables subscription but URL should be empty when WsURL is nil
|
||||||
|
subUrl, hasUrl := subscription["url"].(map[string]interface{})
|
||||||
|
if hasUrl {
|
||||||
|
_, hasStaticContent := subUrl["staticVariableContent"]
|
||||||
|
assert.False(t, hasStaticContent, "Subscription URL should be empty when WsURL is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty subgraphs",
|
||||||
|
subGraphs: []*model.SubGraph{},
|
||||||
|
wantErr: true,
|
||||||
|
validate: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil subgraphs",
|
||||||
|
subGraphs: nil,
|
||||||
|
wantErr: true,
|
||||||
|
validate: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex SDL with multiple types",
|
||||||
|
subGraphs: []*model.SubGraph{
|
||||||
|
{
|
||||||
|
Service: "complex-service",
|
||||||
|
URL: stringPtr("http://localhost:4001/query"),
|
||||||
|
Sdl: `
|
||||||
|
type Query {
|
||||||
|
user(id: ID!): User
|
||||||
|
users: [User!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
validate: func(t *testing.T, config string) {
|
||||||
|
var result map[string]interface{}
|
||||||
|
err := json.Unmarshal([]byte(config), &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check the composed graphqlSchema contains the types
|
||||||
|
engineConfig, ok := result["engineConfig"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Should have engineConfig")
|
||||||
|
|
||||||
|
graphqlSchema, ok := engineConfig["graphqlSchema"].(string)
|
||||||
|
require.True(t, ok, "Should have graphqlSchema")
|
||||||
|
|
||||||
|
assert.Contains(t, graphqlSchema, "Query", "Schema should contain Query type")
|
||||||
|
assert.Contains(t, graphqlSchema, "User", "Schema should contain User type")
|
||||||
|
|
||||||
|
// Check datasource has the original SDL
|
||||||
|
dsConfigs, ok := engineConfig["datasourceConfigurations"].([]interface{})
|
||||||
|
require.True(t, ok && len(dsConfigs) > 0, "Should have datasource configurations")
|
||||||
|
|
||||||
|
ds := dsConfigs[0].(map[string]interface{})
|
||||||
|
customGraphql, ok := ds["customGraphql"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Should have customGraphql config")
|
||||||
|
|
||||||
|
federation, ok := customGraphql["federation"].(map[string]interface{})
|
||||||
|
require.True(t, ok, "Should have federation config")
|
||||||
|
|
||||||
|
serviceSdl, ok := federation["serviceSdl"].(string)
|
||||||
|
require.True(t, ok, "Should have serviceSdl")
|
||||||
|
assert.Contains(t, serviceSdl, "email: String!", "SDL should contain email field")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Use mock executor for all tests
|
||||||
|
mockExecutor := &MockCommandExecutor{}
|
||||||
|
config, err := GenerateCosmoRouterConfigWithExecutor(tt.subGraphs, mockExecutor)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
// Verify executor was not called for error cases
|
||||||
|
if len(tt.subGraphs) == 0 {
|
||||||
|
assert.Equal(t, 0, mockExecutor.CallCount, "Should not call executor for empty subgraphs")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, config, "Config should not be empty")
|
||||||
|
|
||||||
|
// Verify executor was called correctly
|
||||||
|
assert.Equal(t, 1, mockExecutor.CallCount, "Should call executor once")
|
||||||
|
assert.Equal(t, "wgc", mockExecutor.LastArgs[0], "Should call wgc command")
|
||||||
|
assert.Contains(t, mockExecutor.LastArgs, "router", "Should include 'router' arg")
|
||||||
|
assert.Contains(t, mockExecutor.LastArgs, "compose", "Should include 'compose' arg")
|
||||||
|
|
||||||
|
if tt.validate != nil {
|
||||||
|
tt.validate(t, config)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGenerateCosmoRouterConfig_MockError tests error handling with mock executor
|
||||||
|
func TestGenerateCosmoRouterConfig_MockError(t *testing.T) {
|
||||||
|
subGraphs := []*model.SubGraph{
|
||||||
|
{
|
||||||
|
Service: "test-service",
|
||||||
|
URL: stringPtr("http://localhost:4001/query"),
|
||||||
|
Sdl: "type Query { test: String }",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a mock executor that returns an error
|
||||||
|
mockExecutor := &MockCommandExecutor{
|
||||||
|
Error: fmt.Errorf("simulated wgc failure"),
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := GenerateCosmoRouterConfigWithExecutor(subGraphs, mockExecutor)
|
||||||
|
|
||||||
|
// Verify error is propagated
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "wgc router compose failed")
|
||||||
|
assert.Contains(t, err.Error(), "simulated wgc failure")
|
||||||
|
assert.Empty(t, config)
|
||||||
|
|
||||||
|
// Verify executor was called
|
||||||
|
assert.Equal(t, 1, mockExecutor.CallCount, "Should have attempted to call executor")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for tests
|
||||||
|
func stringPtr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
+1911
-2251
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,13 @@ type Organization struct {
|
|||||||
type Query struct {
|
type Query struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SchemaUpdate struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
SubGraphs []*SubGraph `json:"subGraphs"`
|
||||||
|
CosmoRouterConfig *string `json:"cosmoRouterConfig,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type SubGraph struct {
|
type SubGraph struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Service string `json:"service"`
|
Service string `json:"service"`
|
||||||
@@ -68,6 +75,9 @@ type SubGraphs struct {
|
|||||||
|
|
||||||
func (SubGraphs) IsSupergraph() {}
|
func (SubGraphs) IsSupergraph() {}
|
||||||
|
|
||||||
|
type Subscription struct {
|
||||||
|
}
|
||||||
|
|
||||||
type Unchanged struct {
|
type Unchanged struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
MinDelaySeconds int `json:"minDelaySeconds"`
|
MinDelaySeconds int `json:"minDelaySeconds"`
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PubSub handles publishing schema updates to subscribers
|
||||||
|
type PubSub struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
subscribers map[string][]chan *model.SchemaUpdate
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPubSub() *PubSub {
|
||||||
|
return &PubSub{
|
||||||
|
subscribers: make(map[string][]chan *model.SchemaUpdate),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe creates a new subscription channel for a given schema ref
|
||||||
|
func (ps *PubSub) Subscribe(ref string) chan *model.SchemaUpdate {
|
||||||
|
ps.mu.Lock()
|
||||||
|
defer ps.mu.Unlock()
|
||||||
|
|
||||||
|
ch := make(chan *model.SchemaUpdate, 10)
|
||||||
|
ps.subscribers[ref] = append(ps.subscribers[ref], ch)
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe removes a subscription channel
|
||||||
|
func (ps *PubSub) Unsubscribe(ref string, ch chan *model.SchemaUpdate) {
|
||||||
|
ps.mu.Lock()
|
||||||
|
defer ps.mu.Unlock()
|
||||||
|
|
||||||
|
subs := ps.subscribers[ref]
|
||||||
|
for i, sub := range subs {
|
||||||
|
if sub == ch {
|
||||||
|
// Remove this subscriber
|
||||||
|
ps.subscribers[ref] = append(subs[:i], subs[i+1:]...)
|
||||||
|
close(sub)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up empty subscriber lists
|
||||||
|
if len(ps.subscribers[ref]) == 0 {
|
||||||
|
delete(ps.subscribers, ref)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish sends a schema update to all subscribers of a given ref
|
||||||
|
func (ps *PubSub) Publish(ref string, update *model.SchemaUpdate) {
|
||||||
|
ps.mu.RLock()
|
||||||
|
defer ps.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, ch := range ps.subscribers[ref] {
|
||||||
|
// Non-blocking send - if subscriber is slow, skip
|
||||||
|
select {
|
||||||
|
case ch <- update:
|
||||||
|
default:
|
||||||
|
// Channel full, subscriber is too slow - skip this update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPubSub_SubscribeAndPublish(t *testing.T) {
|
||||||
|
ps := NewPubSub()
|
||||||
|
ref := "Test@dev"
|
||||||
|
|
||||||
|
// Subscribe
|
||||||
|
ch := ps.Subscribe(ref)
|
||||||
|
require.NotNil(t, ch, "Subscribe should return a channel")
|
||||||
|
|
||||||
|
// Publish
|
||||||
|
update := &model.SchemaUpdate{
|
||||||
|
Ref: ref,
|
||||||
|
ID: "test-id-1",
|
||||||
|
SubGraphs: []*model.SubGraph{
|
||||||
|
{
|
||||||
|
ID: "sg1",
|
||||||
|
Service: "test-service",
|
||||||
|
Sdl: "type Query { test: String }",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
go ps.Publish(ref, update)
|
||||||
|
|
||||||
|
// Receive
|
||||||
|
select {
|
||||||
|
case received := <-ch:
|
||||||
|
assert.Equal(t, update.Ref, received.Ref, "Ref should match")
|
||||||
|
assert.Equal(t, update.ID, received.ID, "ID should match")
|
||||||
|
assert.Equal(t, len(update.SubGraphs), len(received.SubGraphs), "SubGraphs count should match")
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
t.Fatal("Timeout waiting for published update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPubSub_MultipleSubscribers(t *testing.T) {
|
||||||
|
ps := NewPubSub()
|
||||||
|
ref := "Test@dev"
|
||||||
|
|
||||||
|
// Create multiple subscribers
|
||||||
|
ch1 := ps.Subscribe(ref)
|
||||||
|
ch2 := ps.Subscribe(ref)
|
||||||
|
ch3 := ps.Subscribe(ref)
|
||||||
|
|
||||||
|
update := &model.SchemaUpdate{
|
||||||
|
Ref: ref,
|
||||||
|
ID: "test-id-2",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish once
|
||||||
|
ps.Publish(ref, update)
|
||||||
|
|
||||||
|
// All subscribers should receive the update
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(3)
|
||||||
|
|
||||||
|
checkReceived := func(ch <-chan *model.SchemaUpdate, name string) {
|
||||||
|
defer wg.Done()
|
||||||
|
select {
|
||||||
|
case received := <-ch:
|
||||||
|
assert.Equal(t, update.ID, received.ID, "%s should receive correct update", name)
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
t.Errorf("%s: Timeout waiting for update", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go checkReceived(ch1, "Subscriber 1")
|
||||||
|
go checkReceived(ch2, "Subscriber 2")
|
||||||
|
go checkReceived(ch3, "Subscriber 3")
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPubSub_DifferentRefs(t *testing.T) {
|
||||||
|
ps := NewPubSub()
|
||||||
|
|
||||||
|
ref1 := "Test1@dev"
|
||||||
|
ref2 := "Test2@dev"
|
||||||
|
|
||||||
|
ch1 := ps.Subscribe(ref1)
|
||||||
|
ch2 := ps.Subscribe(ref2)
|
||||||
|
|
||||||
|
update1 := &model.SchemaUpdate{Ref: ref1, ID: "id1"}
|
||||||
|
update2 := &model.SchemaUpdate{Ref: ref2, ID: "id2"}
|
||||||
|
|
||||||
|
// Publish to ref1
|
||||||
|
ps.Publish(ref1, update1)
|
||||||
|
|
||||||
|
// Only ch1 should receive
|
||||||
|
select {
|
||||||
|
case received := <-ch1:
|
||||||
|
assert.Equal(t, "id1", received.ID)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("ch1 should have received update")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ch2 should not receive ref1's update
|
||||||
|
select {
|
||||||
|
case <-ch2:
|
||||||
|
t.Fatal("ch2 should not receive ref1's update")
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
// Expected - no update
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish to ref2
|
||||||
|
ps.Publish(ref2, update2)
|
||||||
|
|
||||||
|
// Now ch2 should receive
|
||||||
|
select {
|
||||||
|
case received := <-ch2:
|
||||||
|
assert.Equal(t, "id2", received.ID)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("ch2 should have received update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPubSub_Unsubscribe(t *testing.T) {
|
||||||
|
ps := NewPubSub()
|
||||||
|
ref := "Test@dev"
|
||||||
|
|
||||||
|
ch := ps.Subscribe(ref)
|
||||||
|
|
||||||
|
// Unsubscribe
|
||||||
|
ps.Unsubscribe(ref, ch)
|
||||||
|
|
||||||
|
// Channel should be closed
|
||||||
|
_, ok := <-ch
|
||||||
|
assert.False(t, ok, "Channel should be closed after unsubscribe")
|
||||||
|
|
||||||
|
// Publishing after unsubscribe should not panic
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
ps.Publish(ref, &model.SchemaUpdate{Ref: ref})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPubSub_BufferedChannel(t *testing.T) {
|
||||||
|
ps := NewPubSub()
|
||||||
|
ref := "Test@dev"
|
||||||
|
|
||||||
|
ch := ps.Subscribe(ref)
|
||||||
|
|
||||||
|
// Publish multiple updates quickly (up to buffer size of 10)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
update := &model.SchemaUpdate{
|
||||||
|
Ref: ref,
|
||||||
|
ID: string(rune('a' + i)),
|
||||||
|
}
|
||||||
|
ps.Publish(ref, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All 10 should be buffered and receivable
|
||||||
|
received := 0
|
||||||
|
timeout := time.After(1 * time.Second)
|
||||||
|
|
||||||
|
for received < 10 {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
received++
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatalf("Only received %d out of 10 updates", received)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, 10, received, "Should receive all buffered updates")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPubSub_SlowSubscriber(t *testing.T) {
|
||||||
|
ps := NewPubSub()
|
||||||
|
ref := "Test@dev"
|
||||||
|
|
||||||
|
ch := ps.Subscribe(ref)
|
||||||
|
|
||||||
|
// Fill the buffer (10 items)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
ps.Publish(ref, &model.SchemaUpdate{Ref: ref})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish one more - this should be dropped (channel full, non-blocking send)
|
||||||
|
ps.Publish(ref, &model.SchemaUpdate{Ref: ref, ID: "should-be-dropped"})
|
||||||
|
|
||||||
|
// Drain the channel
|
||||||
|
count := 0
|
||||||
|
timeout := time.After(500 * time.Millisecond)
|
||||||
|
|
||||||
|
drainLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case update := <-ch:
|
||||||
|
count++
|
||||||
|
// Should not receive the dropped update
|
||||||
|
assert.NotEqual(t, "should-be-dropped", update.ID, "Should not receive dropped update")
|
||||||
|
case <-timeout:
|
||||||
|
break drainLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have received exactly 10 (the buffer size), not 11
|
||||||
|
assert.Equal(t, 10, count, "Should only receive buffered updates, not the dropped one")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPubSub_ConcurrentPublish(t *testing.T) {
|
||||||
|
ps := NewPubSub()
|
||||||
|
ref := "Test@dev"
|
||||||
|
|
||||||
|
ch := ps.Subscribe(ref)
|
||||||
|
|
||||||
|
numPublishers := 10
|
||||||
|
updatesPerPublisher := 10
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(numPublishers)
|
||||||
|
|
||||||
|
// Multiple goroutines publishing concurrently
|
||||||
|
for i := 0; i < numPublishers; i++ {
|
||||||
|
go func(publisherID int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := 0; j < updatesPerPublisher; j++ {
|
||||||
|
ps.Publish(ref, &model.SchemaUpdate{
|
||||||
|
Ref: ref,
|
||||||
|
ID: string(rune('a' + publisherID)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Should not panic and subscriber should receive updates
|
||||||
|
// (exact count may vary due to buffer and timing)
|
||||||
|
timeout := time.After(1 * time.Second)
|
||||||
|
received := 0
|
||||||
|
|
||||||
|
receiveLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
received++
|
||||||
|
case <-timeout:
|
||||||
|
break receiveLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Greater(t, received, 0, "Should have received some updates")
|
||||||
|
}
|
||||||
+4
-3
@@ -7,13 +7,13 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/cache"
|
"gitea.unbound.se/unboundsoftware/schemas/cache"
|
||||||
"gitlab.com/unboundsoftware/schemas/middleware"
|
"gitea.unbound.se/unboundsoftware/schemas/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate go run github.com/99designs/gqlgen
|
//go:generate go run github.com/99designs/gqlgen
|
||||||
//go:generate gofumpt -w .
|
//go:generate gofumpt -w .
|
||||||
//go:generate goimports -w -local gitlab.com/unboundsoftware/schemas .
|
//go:generate goimports -w -local gitea.unbound.se/unboundsoftware/schemas .
|
||||||
|
|
||||||
// This file will not be regenerated automatically.
|
// This file will not be regenerated automatically.
|
||||||
//
|
//
|
||||||
@@ -28,6 +28,7 @@ type Resolver struct {
|
|||||||
Publisher Publisher
|
Publisher Publisher
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
Cache *cache.Cache
|
Cache *cache.Cache
|
||||||
|
PubSub *PubSub
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) apiKeyCanAccessRef(ctx context.Context, ref string, publish bool) (string, error) {
|
func (r *Resolver) apiKeyCanAccessRef(ctx context.Context, ref string, publish bool) (string, error) {
|
||||||
|
|||||||
+17
-1
@@ -1,14 +1,23 @@
|
|||||||
type Query {
|
type Query {
|
||||||
organizations: [Organization!]! @auth(user: true)
|
organizations: [Organization!]! @auth(user: true)
|
||||||
supergraph(ref: String!, isAfter: String): Supergraph! @auth(organization: true)
|
allOrganizations: [Organization!]! @auth(user: true)
|
||||||
|
supergraph(ref: String!, isAfter: String): Supergraph! @auth(user: true, organization: true)
|
||||||
|
latestSchema(ref: String!): SchemaUpdate! @auth(user: true, organization: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
addOrganization(name: String!): Organization! @auth(user: true)
|
addOrganization(name: String!): Organization! @auth(user: true)
|
||||||
|
addUserToOrganization(organizationId: ID!, userId: String!): Organization! @auth(user: true)
|
||||||
addAPIKey(input: InputAPIKey): APIKey! @auth(user: true)
|
addAPIKey(input: InputAPIKey): APIKey! @auth(user: true)
|
||||||
|
removeAPIKey(organizationId: ID!, keyName: String!): Organization! @auth(user: true)
|
||||||
|
removeOrganization(organizationId: ID!): Boolean! @auth(user: true)
|
||||||
updateSubGraph(input: InputSubGraph!): SubGraph! @auth(organization: true)
|
updateSubGraph(input: InputSubGraph!): SubGraph! @auth(organization: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Subscription {
|
||||||
|
schemaUpdates(ref: String!): SchemaUpdate! @auth(organization: true)
|
||||||
|
}
|
||||||
|
|
||||||
type Organization {
|
type Organization {
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String!
|
name: String!
|
||||||
@@ -54,6 +63,13 @@ type SubGraph {
|
|||||||
changedAt: Time!
|
changedAt: Time!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SchemaUpdate {
|
||||||
|
ref: String!
|
||||||
|
id: ID!
|
||||||
|
subGraphs: [SubGraph!]!
|
||||||
|
cosmoRouterConfig: String
|
||||||
|
}
|
||||||
|
|
||||||
input InputAPIKey {
|
input InputAPIKey {
|
||||||
name: String!
|
name: String!
|
||||||
organizationId: ID!
|
organizationId: ID!
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/model"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *Resolver) fetchSubGraph(ctx context.Context, subGraphId string) (*domain.SubGraph, error) {
|
func (r *Resolver) fetchSubGraph(ctx context.Context, subGraphId string) (*domain.SubGraph, error) {
|
||||||
|
|||||||
+327
-12
@@ -1,6 +1,7 @@
|
|||||||
package graph
|
package graph
|
||||||
|
|
||||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
// This file will be automatically regenerated based on the schema, any resolver
|
||||||
|
// implementations
|
||||||
// will be copied through when generating and any unknown code will be moved to the end.
|
// will be copied through when generating and any unknown code will be moved to the end.
|
||||||
// Code generated by github.com/99designs/gqlgen
|
// Code generated by github.com/99designs/gqlgen
|
||||||
|
|
||||||
@@ -11,12 +12,12 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/generated"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/generated"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/model"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
"gitlab.com/unboundsoftware/schemas/middleware"
|
"gitea.unbound.se/unboundsoftware/schemas/middleware"
|
||||||
"gitlab.com/unboundsoftware/schemas/rand"
|
"gitea.unbound.se/unboundsoftware/schemas/rand"
|
||||||
"gitlab.com/unboundsoftware/schemas/sdlmerge"
|
"gitea.unbound.se/unboundsoftware/schemas/sdlmerge"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddOrganization is the resolver for the addOrganization field.
|
// AddOrganization is the resolver for the addOrganization field.
|
||||||
@@ -37,6 +38,24 @@ func (r *mutationResolver) AddOrganization(ctx context.Context, name string) (*m
|
|||||||
return ToGqlOrganization(*org), nil
|
return ToGqlOrganization(*org), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddUserToOrganization is the resolver for the addUserToOrganization field.
|
||||||
|
func (r *mutationResolver) AddUserToOrganization(ctx context.Context, organizationID string, userID string) (*model.Organization, error) {
|
||||||
|
sub := middleware.UserFromContext(ctx)
|
||||||
|
org := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(organizationID)}
|
||||||
|
h, err := r.handler(ctx, org)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = h.Handle(ctx, &domain.AddUserToOrganization{
|
||||||
|
UserId: userID,
|
||||||
|
Initiator: sub,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ToGqlOrganization(*org), nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddAPIKey is the resolver for the addAPIKey field.
|
// AddAPIKey is the resolver for the addAPIKey field.
|
||||||
func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIKey) (*model.APIKey, error) {
|
func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIKey) (*model.APIKey, error) {
|
||||||
sub := middleware.UserFromContext(ctx)
|
sub := middleware.UserFromContext(ctx)
|
||||||
@@ -71,6 +90,41 @@ func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIK
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveAPIKey is the resolver for the removeAPIKey field.
|
||||||
|
func (r *mutationResolver) RemoveAPIKey(ctx context.Context, organizationID string, keyName string) (*model.Organization, error) {
|
||||||
|
sub := middleware.UserFromContext(ctx)
|
||||||
|
org := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(organizationID)}
|
||||||
|
h, err := r.handler(ctx, org)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = h.Handle(ctx, &domain.RemoveAPIKey{
|
||||||
|
KeyName: keyName,
|
||||||
|
Initiator: sub,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ToGqlOrganization(*org), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOrganization is the resolver for the removeOrganization field.
|
||||||
|
func (r *mutationResolver) RemoveOrganization(ctx context.Context, organizationID string) (bool, error) {
|
||||||
|
sub := middleware.UserFromContext(ctx)
|
||||||
|
org := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(organizationID)}
|
||||||
|
h, err := r.handler(ctx, org)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
_, err = h.Handle(ctx, &domain.RemoveOrganization{
|
||||||
|
Initiator: sub,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateSubGraph is the resolver for the updateSubGraph field.
|
// UpdateSubGraph is the resolver for the updateSubGraph field.
|
||||||
func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.InputSubGraph) (*model.SubGraph, error) {
|
func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.InputSubGraph) (*model.SubGraph, error) {
|
||||||
orgId := middleware.OrganizationFromContext(ctx)
|
orgId := middleware.OrganizationFromContext(ctx)
|
||||||
@@ -119,6 +173,60 @@ func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.Input
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish schema update to subscribers
|
||||||
|
go func() {
|
||||||
|
services, lastUpdate := r.Cache.Services(orgId, input.Ref, "")
|
||||||
|
r.Logger.Info("Publishing schema update after subgraph change",
|
||||||
|
"ref", input.Ref,
|
||||||
|
"orgId", orgId,
|
||||||
|
"lastUpdate", lastUpdate,
|
||||||
|
"servicesCount", len(services),
|
||||||
|
)
|
||||||
|
|
||||||
|
subGraphs := make([]*model.SubGraph, len(services))
|
||||||
|
for i, id := range services {
|
||||||
|
sg, err := r.fetchSubGraph(context.Background(), id)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("fetch subgraph for update notification", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subGraphs[i] = &model.SubGraph{
|
||||||
|
ID: sg.ID.String(),
|
||||||
|
Service: sg.Service,
|
||||||
|
URL: sg.Url,
|
||||||
|
WsURL: sg.WSUrl,
|
||||||
|
Sdl: sg.Sdl,
|
||||||
|
ChangedBy: sg.ChangedBy,
|
||||||
|
ChangedAt: sg.ChangedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Cosmo router config
|
||||||
|
cosmoConfig, err := GenerateCosmoRouterConfig(subGraphs)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("generate cosmo config for update", "error", err)
|
||||||
|
cosmoConfig = "" // Send empty if generation fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish to all subscribers of this ref
|
||||||
|
update := &model.SchemaUpdate{
|
||||||
|
Ref: input.Ref,
|
||||||
|
ID: lastUpdate,
|
||||||
|
SubGraphs: subGraphs,
|
||||||
|
CosmoRouterConfig: &cosmoConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Logger.Info("Publishing schema update to subscribers",
|
||||||
|
"ref", update.Ref,
|
||||||
|
"id", update.ID,
|
||||||
|
"subGraphsCount", len(update.SubGraphs),
|
||||||
|
"cosmoConfigLength", len(cosmoConfig),
|
||||||
|
)
|
||||||
|
|
||||||
|
r.PubSub.Publish(input.Ref, update)
|
||||||
|
}()
|
||||||
|
|
||||||
return r.toGqlSubGraph(subGraph), nil
|
return r.toGqlSubGraph(subGraph), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,13 +237,49 @@ func (r *queryResolver) Organizations(ctx context.Context) ([]*model.Organizatio
|
|||||||
return ToGqlOrganizations(orgs), nil
|
return ToGqlOrganizations(orgs), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllOrganizations is the resolver for the allOrganizations field.
|
||||||
|
func (r *queryResolver) AllOrganizations(ctx context.Context) ([]*model.Organization, error) {
|
||||||
|
// Check if user has admin role
|
||||||
|
if !middleware.UserHasRole(ctx, "admin") {
|
||||||
|
return nil, fmt.Errorf("unauthorized: admin role required")
|
||||||
|
}
|
||||||
|
|
||||||
|
orgs := r.Cache.AllOrganizations()
|
||||||
|
return ToGqlOrganizations(orgs), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Supergraph is the resolver for the supergraph field.
|
// Supergraph is the resolver for the supergraph field.
|
||||||
func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *string) (model.Supergraph, error) {
|
func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *string) (model.Supergraph, error) {
|
||||||
orgId := middleware.OrganizationFromContext(ctx)
|
orgId := middleware.OrganizationFromContext(ctx)
|
||||||
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
userId := middleware.UserFromContext(ctx)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
r.Logger.Info("Supergraph query",
|
||||||
|
"ref", ref,
|
||||||
|
"orgId", orgId,
|
||||||
|
"userId", userId,
|
||||||
|
)
|
||||||
|
|
||||||
|
// If authenticated with API key (organization), check access
|
||||||
|
if orgId != "" {
|
||||||
|
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if userId != "" {
|
||||||
|
// For user authentication, check if user has access to ref through their organizations
|
||||||
|
userOrgs := r.Cache.OrganizationsByUser(userId)
|
||||||
|
if len(userOrgs) == 0 {
|
||||||
|
r.Logger.Error("User has no organizations", "userId", userId)
|
||||||
|
return nil, fmt.Errorf("user has no access to any organizations")
|
||||||
|
}
|
||||||
|
// Use the first organization's ID for querying
|
||||||
|
orgId = userOrgs[0].ID.String()
|
||||||
|
r.Logger.Info("Using organization from user context", "orgId", orgId)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("no authentication provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
after := ""
|
after := ""
|
||||||
if isAfter != nil {
|
if isAfter != nil {
|
||||||
after = *isAfter
|
after = *isAfter
|
||||||
@@ -184,13 +328,184 @@ func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *str
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LatestSchema is the resolver for the latestSchema field.
|
||||||
|
func (r *queryResolver) LatestSchema(ctx context.Context, ref string) (*model.SchemaUpdate, error) {
|
||||||
|
orgId := middleware.OrganizationFromContext(ctx)
|
||||||
|
userId := middleware.UserFromContext(ctx)
|
||||||
|
|
||||||
|
r.Logger.Info("LatestSchema query",
|
||||||
|
"ref", ref,
|
||||||
|
"orgId", orgId,
|
||||||
|
"userId", userId,
|
||||||
|
)
|
||||||
|
|
||||||
|
// If authenticated with API key (organization), check access
|
||||||
|
if orgId != "" {
|
||||||
|
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if userId != "" {
|
||||||
|
// For user authentication, check if user has access to ref through their organizations
|
||||||
|
userOrgs := r.Cache.OrganizationsByUser(userId)
|
||||||
|
if len(userOrgs) == 0 {
|
||||||
|
r.Logger.Error("User has no organizations", "userId", userId)
|
||||||
|
return nil, fmt.Errorf("user has no access to any organizations")
|
||||||
|
}
|
||||||
|
// Use the first organization's ID for querying
|
||||||
|
// In a real-world scenario, you might want to check which org has access to this ref
|
||||||
|
orgId = userOrgs[0].ID.String()
|
||||||
|
r.Logger.Info("Using organization from user context", "orgId", orgId)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("no authentication provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current services and schema
|
||||||
|
services, lastUpdate := r.Cache.Services(orgId, ref, "")
|
||||||
|
r.Logger.Info("Fetching latest schema",
|
||||||
|
"ref", ref,
|
||||||
|
"orgId", orgId,
|
||||||
|
"lastUpdate", lastUpdate,
|
||||||
|
"servicesCount", len(services),
|
||||||
|
)
|
||||||
|
|
||||||
|
subGraphs := make([]*model.SubGraph, len(services))
|
||||||
|
for i, id := range services {
|
||||||
|
sg, err := r.fetchSubGraph(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("fetch subgraph", "error", err, "id", id)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
subGraphs[i] = &model.SubGraph{
|
||||||
|
ID: sg.ID.String(),
|
||||||
|
Service: sg.Service,
|
||||||
|
URL: sg.Url,
|
||||||
|
WsURL: sg.WSUrl,
|
||||||
|
Sdl: sg.Sdl,
|
||||||
|
ChangedBy: sg.ChangedBy,
|
||||||
|
ChangedAt: sg.ChangedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Cosmo router config
|
||||||
|
cosmoConfig, err := GenerateCosmoRouterConfig(subGraphs)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("generate cosmo config", "error", err)
|
||||||
|
cosmoConfig = "" // Return empty if generation fails
|
||||||
|
}
|
||||||
|
|
||||||
|
update := &model.SchemaUpdate{
|
||||||
|
Ref: ref,
|
||||||
|
ID: lastUpdate,
|
||||||
|
SubGraphs: subGraphs,
|
||||||
|
CosmoRouterConfig: &cosmoConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Logger.Info("Latest schema fetched",
|
||||||
|
"ref", update.Ref,
|
||||||
|
"id", update.ID,
|
||||||
|
"subGraphsCount", len(update.SubGraphs),
|
||||||
|
"cosmoConfigLength", len(cosmoConfig),
|
||||||
|
)
|
||||||
|
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaUpdates is the resolver for the schemaUpdates field.
|
||||||
|
func (r *subscriptionResolver) SchemaUpdates(ctx context.Context, ref string) (<-chan *model.SchemaUpdate, error) {
|
||||||
|
orgId := middleware.OrganizationFromContext(ctx)
|
||||||
|
|
||||||
|
r.Logger.Info("SchemaUpdates subscription started",
|
||||||
|
"ref", ref,
|
||||||
|
"orgId", orgId,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to updates for this ref
|
||||||
|
ch := r.PubSub.Subscribe(ref)
|
||||||
|
|
||||||
|
// Send initial state immediately
|
||||||
|
go func() {
|
||||||
|
// Use background context for async operation
|
||||||
|
bgCtx := context.Background()
|
||||||
|
|
||||||
|
services, lastUpdate := r.Cache.Services(orgId, ref, "")
|
||||||
|
r.Logger.Info("Preparing initial schema update",
|
||||||
|
"ref", ref,
|
||||||
|
"orgId", orgId,
|
||||||
|
"lastUpdate", lastUpdate,
|
||||||
|
"servicesCount", len(services),
|
||||||
|
)
|
||||||
|
|
||||||
|
subGraphs := make([]*model.SubGraph, len(services))
|
||||||
|
for i, id := range services {
|
||||||
|
sg, err := r.fetchSubGraph(bgCtx, id)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("fetch subgraph for initial update", "error", err, "id", id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subGraphs[i] = &model.SubGraph{
|
||||||
|
ID: sg.ID.String(),
|
||||||
|
Service: sg.Service,
|
||||||
|
URL: sg.Url,
|
||||||
|
WsURL: sg.WSUrl,
|
||||||
|
Sdl: sg.Sdl,
|
||||||
|
ChangedBy: sg.ChangedBy,
|
||||||
|
ChangedAt: sg.ChangedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Cosmo router config
|
||||||
|
cosmoConfig, err := GenerateCosmoRouterConfig(subGraphs)
|
||||||
|
if err != nil {
|
||||||
|
r.Logger.Error("generate cosmo config", "error", err)
|
||||||
|
cosmoConfig = "" // Send empty if generation fails
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send initial update
|
||||||
|
update := &model.SchemaUpdate{
|
||||||
|
Ref: ref,
|
||||||
|
ID: lastUpdate,
|
||||||
|
SubGraphs: subGraphs,
|
||||||
|
CosmoRouterConfig: &cosmoConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Logger.Info("Sending initial schema update",
|
||||||
|
"ref", update.Ref,
|
||||||
|
"id", update.ID,
|
||||||
|
"subGraphsCount", len(update.SubGraphs),
|
||||||
|
"cosmoConfigLength", len(cosmoConfig),
|
||||||
|
)
|
||||||
|
|
||||||
|
ch <- update
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Clean up subscription when context is done
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
r.PubSub.Unsubscribe(ref, ch)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Mutation returns generated.MutationResolver implementation.
|
// Mutation returns generated.MutationResolver implementation.
|
||||||
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
|
||||||
|
|
||||||
// Query returns generated.QueryResolver implementation.
|
// Query returns generated.QueryResolver implementation.
|
||||||
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||||
|
|
||||||
|
// Subscription returns generated.SubscriptionResolver implementation.
|
||||||
|
func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} }
|
||||||
|
|
||||||
type (
|
type (
|
||||||
mutationResolver struct{ *Resolver }
|
mutationResolver struct{ *Resolver }
|
||||||
queryResolver struct{ *Resolver }
|
queryResolver struct{ *Resolver }
|
||||||
|
subscriptionResolver struct{ *Resolver }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build tools
|
//go:build tools
|
||||||
// +build tools
|
|
||||||
|
|
||||||
package graph
|
package graph
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,72 @@ package hash
|
|||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// String creates a SHA256 hash of a string (legacy, for non-sensitive data)
|
||||||
func String(s string) string {
|
func String(s string) string {
|
||||||
encoded := sha256.New().Sum([]byte(s))
|
encoded := sha256.New().Sum([]byte(s))
|
||||||
return base64.StdEncoding.EncodeToString(encoded)
|
return base64.StdEncoding.EncodeToString(encoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// APIKey hashes an API key using bcrypt for secure storage
|
||||||
|
// Cost of 12 provides a good balance between security and performance
|
||||||
|
func APIKey(key string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(key), 12)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareAPIKey compares a plaintext API key with a hash
|
||||||
|
// Supports both bcrypt (new) and SHA256 (legacy) hashes for backwards compatibility
|
||||||
|
// Returns true if they match, false otherwise
|
||||||
|
//
|
||||||
|
// Migration Strategy:
|
||||||
|
// Old API keys stored with SHA256 will continue to work. To upgrade them to bcrypt:
|
||||||
|
// 1. Keys are automatically upgraded when users re-authenticate (if implemented)
|
||||||
|
// 2. Or, run a one-time migration using MigrateAPIKeyHash when convenient
|
||||||
|
func CompareAPIKey(hashedKey, plainKey string) bool {
|
||||||
|
// Bcrypt hashes start with $2a$, $2b$, or $2y$
|
||||||
|
// If the hash starts with $2, it's a bcrypt hash
|
||||||
|
if len(hashedKey) > 2 && hashedKey[0] == '$' && hashedKey[1] == '2' {
|
||||||
|
// New bcrypt hash
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hashedKey), []byte(plainKey))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy SHA256 hash - compare using the old method
|
||||||
|
legacyHash := String(plainKey)
|
||||||
|
return hashedKey == legacyHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLegacyHash returns true if the hash is a legacy SHA256 hash (not bcrypt)
|
||||||
|
func IsLegacyHash(hashedKey string) bool {
|
||||||
|
return len(hashedKey) <= 2 || hashedKey[0] != '$' || hashedKey[1] != '2'
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateAPIKeyHash can be used to upgrade a legacy SHA256 hash to bcrypt
|
||||||
|
// This is useful for one-time migrations of existing keys
|
||||||
|
// Returns the new bcrypt hash if the key is legacy, otherwise returns the original
|
||||||
|
func MigrateAPIKeyHash(currentHash, plainKey string) (string, bool, error) {
|
||||||
|
// If already bcrypt, no migration needed
|
||||||
|
if !IsLegacyHash(currentHash) {
|
||||||
|
return currentHash, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the legacy hash is correct before migrating
|
||||||
|
if !CompareAPIKey(currentHash, plainKey) {
|
||||||
|
return "", false, nil // Invalid key, don't migrate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new bcrypt hash
|
||||||
|
newHash, err := APIKey(plainKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newHash, true, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package hash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAPIKey(t *testing.T) {
|
||||||
|
key := "test_api_key_12345" // gitleaks:allow
|
||||||
|
|
||||||
|
hash1, err := APIKey(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, hash1)
|
||||||
|
assert.NotEqual(t, key, hash1, "Hash should not equal plaintext")
|
||||||
|
|
||||||
|
// Bcrypt hashes should start with $2
|
||||||
|
assert.True(t, strings.HasPrefix(hash1, "$2"), "Should be a bcrypt hash")
|
||||||
|
|
||||||
|
// Same key should produce different hashes (due to salt)
|
||||||
|
hash2, err := APIKey(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, hash1, hash2, "Bcrypt should produce different hashes with different salts")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareAPIKey_Bcrypt(t *testing.T) {
|
||||||
|
key := "test_api_key_12345" // gitleaks:allow
|
||||||
|
|
||||||
|
hash, err := APIKey(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Correct key should match
|
||||||
|
assert.True(t, CompareAPIKey(hash, key))
|
||||||
|
|
||||||
|
// Wrong key should not match
|
||||||
|
assert.False(t, CompareAPIKey(hash, "wrong_key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareAPIKey_Legacy(t *testing.T) {
|
||||||
|
key := "test_api_key_12345" // gitleaks:allow
|
||||||
|
|
||||||
|
// Create a legacy SHA256 hash
|
||||||
|
legacyHash := String(key)
|
||||||
|
|
||||||
|
// Should still work with legacy hashes
|
||||||
|
assert.True(t, CompareAPIKey(legacyHash, key))
|
||||||
|
|
||||||
|
// Wrong key should not match
|
||||||
|
assert.False(t, CompareAPIKey(legacyHash, "wrong_key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompareAPIKey_BackwardCompatibility(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
hashFunc func(string) string
|
||||||
|
expectOK bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "bcrypt hash",
|
||||||
|
hashFunc: func(k string) string {
|
||||||
|
h, _ := APIKey(k)
|
||||||
|
return h
|
||||||
|
},
|
||||||
|
expectOK: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "legacy SHA256 hash",
|
||||||
|
hashFunc: func(k string) string {
|
||||||
|
return String(k)
|
||||||
|
},
|
||||||
|
expectOK: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
key := "test_key_123"
|
||||||
|
hash := tt.hashFunc(key)
|
||||||
|
|
||||||
|
result := CompareAPIKey(hash, key)
|
||||||
|
assert.Equal(t, tt.expectOK, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestString(t *testing.T) {
|
||||||
|
// Test that String function still works (for non-sensitive data)
|
||||||
|
input := "test_string"
|
||||||
|
hash1 := String(input)
|
||||||
|
hash2 := String(input)
|
||||||
|
|
||||||
|
// SHA256 should be deterministic
|
||||||
|
assert.Equal(t, hash1, hash2)
|
||||||
|
assert.NotEmpty(t, hash1)
|
||||||
|
assert.NotEqual(t, input, hash1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsLegacyHash(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
hash string
|
||||||
|
isLegacy bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "bcrypt hash",
|
||||||
|
hash: "$2a$12$abcdefghijklmnopqrstuv",
|
||||||
|
isLegacy: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SHA256 hash",
|
||||||
|
hash: "dXNfYWtfMTIzNDU2Nzg5MDEyMzQ1NuOwxEKY",
|
||||||
|
isLegacy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
hash: "",
|
||||||
|
isLegacy: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.isLegacy, IsLegacyHash(tt.hash))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigrateAPIKeyHash(t *testing.T) {
|
||||||
|
plainKey := "test_api_key_123"
|
||||||
|
|
||||||
|
t.Run("migrate legacy hash", func(t *testing.T) {
|
||||||
|
// Create a legacy SHA256 hash
|
||||||
|
legacyHash := String(plainKey)
|
||||||
|
|
||||||
|
// Migrate it
|
||||||
|
newHash, migrated, err := MigrateAPIKeyHash(legacyHash, plainKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, migrated, "Should indicate migration occurred")
|
||||||
|
assert.NotEqual(t, legacyHash, newHash, "New hash should differ from legacy")
|
||||||
|
assert.True(t, strings.HasPrefix(newHash, "$2"), "New hash should be bcrypt")
|
||||||
|
|
||||||
|
// Verify new hash works
|
||||||
|
assert.True(t, CompareAPIKey(newHash, plainKey))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no migration needed for bcrypt", func(t *testing.T) {
|
||||||
|
// Create a bcrypt hash
|
||||||
|
bcryptHash, err := APIKey(plainKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Try to migrate it
|
||||||
|
newHash, migrated, err := MigrateAPIKeyHash(bcryptHash, plainKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, migrated, "Should not migrate bcrypt hash")
|
||||||
|
assert.Equal(t, bcryptHash, newHash, "Hash should remain unchanged")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid key does not migrate", func(t *testing.T) {
|
||||||
|
legacyHash := String("correct_key")
|
||||||
|
|
||||||
|
// Try to migrate with wrong plaintext
|
||||||
|
newHash, migrated, err := MigrateAPIKeyHash(legacyHash, "wrong_key")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, migrated, "Should not migrate invalid key")
|
||||||
|
assert.Empty(t, newHash, "Should return empty for invalid key")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Checker struct {
|
||||||
|
db *sql.DB
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB, logger *slog.Logger) *Checker {
|
||||||
|
return &Checker{
|
||||||
|
db: db,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type HealthStatus struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Checks map[string]string `json:"checks,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LivenessHandler checks if the application is running
|
||||||
|
// This is a simple check that always returns OK if the handler is reached
|
||||||
|
func (h *Checker) LivenessHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(HealthStatus{
|
||||||
|
Status: "UP",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadinessHandler checks if the application is ready to accept traffic
|
||||||
|
// This checks database connectivity and other critical dependencies
|
||||||
|
func (h *Checker) ReadinessHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
checks := make(map[string]string)
|
||||||
|
allHealthy := true
|
||||||
|
|
||||||
|
// Check database connectivity
|
||||||
|
if err := h.db.PingContext(ctx); err != nil {
|
||||||
|
h.logger.With("error", err).Warn("database health check failed")
|
||||||
|
checks["database"] = "DOWN"
|
||||||
|
allHealthy = false
|
||||||
|
} else {
|
||||||
|
checks["database"] = "UP"
|
||||||
|
}
|
||||||
|
|
||||||
|
status := HealthStatus{
|
||||||
|
Status: "UP",
|
||||||
|
Checks: checks,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allHealthy {
|
||||||
|
status.Status = "DOWN"
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
_ = json.NewEncoder(w).Encode(status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(status)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLivenessHandler(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
db, _, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
checker := New(db, logger)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/health/live", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
checker.LivenessHandler(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Contains(t, rec.Body.String(), `"status":"UP"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadinessHandler_Healthy(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Expect a ping and return success
|
||||||
|
mock.ExpectPing().WillReturnError(nil)
|
||||||
|
|
||||||
|
checker := New(db, logger)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/health/ready", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
checker.ReadinessHandler(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Contains(t, rec.Body.String(), `"status":"UP"`)
|
||||||
|
assert.Contains(t, rec.Body.String(), `"database":"UP"`)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadinessHandler_DatabaseDown(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Expect a ping and return error
|
||||||
|
mock.ExpectPing().WillReturnError(sql.ErrConnDone)
|
||||||
|
|
||||||
|
checker := New(db, logger)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/health/ready", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
checker.ReadinessHandler(rec, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusServiceUnavailable, rec.Code)
|
||||||
|
assert.Contains(t, rec.Body.String(), `"status":"DOWN"`)
|
||||||
|
assert.Contains(t, rec.Body.String(), `"database":"DOWN"`)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
}
|
||||||
+14
-2
@@ -44,18 +44,30 @@ spec:
|
|||||||
requests:
|
requests:
|
||||||
cpu: "20m"
|
cpu: "20m"
|
||||||
memory: "20Mi"
|
memory: "20Mi"
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health/live
|
||||||
|
port: 8080
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health/ready
|
||||||
port: 8080
|
port: 8080
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
image: registry.gitlab.com/unboundsoftware/schemas:${COMMIT}
|
image: oci.unbound.se/unboundsoftware/schemas:${COMMIT}
|
||||||
ports:
|
ports:
|
||||||
- name: api
|
- name: api
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
|
env:
|
||||||
|
- name: OTEL_EXPORTER_OTLP_ENDPOINT
|
||||||
|
value: http://k8s-monitoring-alloy-receiver.monitoring.svc.cluster.local:4318
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: schemas
|
name: schemas
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: schemas-ingress
|
name: schemas-ingress
|
||||||
annotations:
|
annotations:
|
||||||
kubernetes.io/ingress.class: "alb"
|
|
||||||
alb.ingress.kubernetes.io/group.name: "default"
|
alb.ingress.kubernetes.io/group.name: "default"
|
||||||
alb.ingress.kubernetes.io/scheme: internet-facing
|
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||||
alb.ingress.kubernetes.io/target-type: instance
|
alb.ingress.kubernetes.io/target-type: instance
|
||||||
@@ -11,6 +10,7 @@ metadata:
|
|||||||
alb.ingress.kubernetes.io/ssl-redirect: "443"
|
alb.ingress.kubernetes.io/ssl-redirect: "443"
|
||||||
alb.ingress.kubernetes.io/healthcheck-path: '/health'
|
alb.ingress.kubernetes.io/healthcheck-path: '/health'
|
||||||
spec:
|
spec:
|
||||||
|
ingressClassName: "alb"
|
||||||
rules:
|
rules:
|
||||||
- host: "schemas.unbound.se"
|
- host: "schemas.unbound.se"
|
||||||
http:
|
http:
|
||||||
|
|||||||
+66
-7
@@ -8,8 +8,7 @@ import (
|
|||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/hash"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -49,7 +48,9 @@ func (m *AuthMiddleware) Handler(next http.Handler) http.Handler {
|
|||||||
_, _ = w.Write([]byte("Invalid API Key format"))
|
_, _ = w.Write([]byte("Invalid API Key format"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if organization := m.cache.OrganizationByAPIKey(hash.String(apiKey)); organization != nil {
|
// Cache handles hash comparison internally
|
||||||
|
organization := m.cache.OrganizationByAPIKey(apiKey)
|
||||||
|
if organization != nil {
|
||||||
ctx = context.WithValue(ctx, OrganizationKey, *organization)
|
ctx = context.WithValue(ctx, OrganizationKey, *organization)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +67,37 @@ func UserFromContext(ctx context.Context) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UserHasRole(ctx context.Context, role string) bool {
|
||||||
|
token, err := TokenFromContext(ctx)
|
||||||
|
if err != nil || token == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the custom roles claim
|
||||||
|
rolesInterface, ok := claims["https://unbound.se/roles"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, ok := rolesInterface.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range roles {
|
||||||
|
if roleStr, ok := r.(string); ok && roleStr == role {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func OrganizationFromContext(ctx context.Context) string {
|
func OrganizationFromContext(ctx context.Context) string {
|
||||||
if value := ctx.Value(OrganizationKey); value != nil {
|
if value := ctx.Value(OrganizationKey); value != nil {
|
||||||
if u, ok := value.(domain.Organization); ok {
|
if u, ok := value.(domain.Organization); ok {
|
||||||
@@ -76,15 +108,42 @@ func OrganizationFromContext(ctx context.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *AuthMiddleware) Directive(ctx context.Context, _ interface{}, next graphql.Resolver, user *bool, organization *bool) (res interface{}, err error) {
|
func (m *AuthMiddleware) Directive(ctx context.Context, _ interface{}, next graphql.Resolver, user *bool, organization *bool) (res interface{}, err error) {
|
||||||
if user != nil && *user {
|
userRequired := user != nil && *user
|
||||||
if u := UserFromContext(ctx); u == "" {
|
orgRequired := organization != nil && *organization
|
||||||
|
|
||||||
|
u := UserFromContext(ctx)
|
||||||
|
orgId := OrganizationFromContext(ctx)
|
||||||
|
|
||||||
|
fmt.Printf("[Auth Directive] userRequired=%v, orgRequired=%v, hasUser=%v, hasOrg=%v\n",
|
||||||
|
userRequired, orgRequired, u != "", orgId != "")
|
||||||
|
|
||||||
|
// If both are required, it means EITHER is acceptable (OR logic)
|
||||||
|
if userRequired && orgRequired {
|
||||||
|
if u == "" && orgId == "" {
|
||||||
|
fmt.Printf("[Auth Directive] REJECTED: Neither user nor organization available\n")
|
||||||
|
return nil, fmt.Errorf("authentication required: provide either user token or organization API key")
|
||||||
|
}
|
||||||
|
fmt.Printf("[Auth Directive] ACCEPTED: Has user=%v OR organization=%v\n", u != "", orgId != "")
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only user required
|
||||||
|
if userRequired {
|
||||||
|
if u == "" {
|
||||||
|
fmt.Printf("[Auth Directive] REJECTED: No user available\n")
|
||||||
return nil, fmt.Errorf("no user available in request")
|
return nil, fmt.Errorf("no user available in request")
|
||||||
}
|
}
|
||||||
|
fmt.Printf("[Auth Directive] ACCEPTED: User authenticated\n")
|
||||||
}
|
}
|
||||||
if organization != nil && *organization {
|
|
||||||
if orgId := OrganizationFromContext(ctx); orgId == "" {
|
// Only organization required
|
||||||
|
if orgRequired {
|
||||||
|
if orgId == "" {
|
||||||
|
fmt.Printf("[Auth Directive] REJECTED: No organization available\n")
|
||||||
return nil, fmt.Errorf("no organization available in request")
|
return nil, fmt.Errorf("no organization available in request")
|
||||||
}
|
}
|
||||||
|
fmt.Printf("[Auth Directive] ACCEPTED: Organization authenticated\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,564 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
mw "github.com/auth0/go-jwt-middleware/v2"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockCache is a mock implementation of the Cache interface
|
||||||
|
type MockCache struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockCache) OrganizationByAPIKey(apiKey string) *domain.Organization {
|
||||||
|
args := m.Called(apiKey)
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return args.Get(0).(*domain.Organization)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Handler_WithValidAPIKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
orgID := uuid.New()
|
||||||
|
expectedOrg := &domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregate{
|
||||||
|
ID: eventsourced.IdFromString(orgID.String()),
|
||||||
|
},
|
||||||
|
Name: "Test Organization",
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey := "test-api-key-123"
|
||||||
|
|
||||||
|
// Mock expects plaintext key (cache handles hashing internally)
|
||||||
|
mockCache.On("OrganizationByAPIKey", apiKey).Return(expectedOrg)
|
||||||
|
|
||||||
|
// Create a test handler that checks the context
|
||||||
|
var capturedOrg *domain.Organization
|
||||||
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if org := r.Context().Value(OrganizationKey); org != nil {
|
||||||
|
if o, ok := org.(domain.Organization); ok {
|
||||||
|
capturedOrg = &o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create request with API key in context
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
ctx := context.WithValue(req.Context(), ApiKey, apiKey)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
require.NotNil(t, capturedOrg)
|
||||||
|
assert.Equal(t, expectedOrg.Name, capturedOrg.Name)
|
||||||
|
assert.Equal(t, expectedOrg.ID.String(), capturedOrg.ID.String())
|
||||||
|
mockCache.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Handler_WithInvalidAPIKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
apiKey := "invalid-api-key"
|
||||||
|
|
||||||
|
// Mock expects plaintext key (cache handles hashing internally)
|
||||||
|
mockCache.On("OrganizationByAPIKey", apiKey).Return(nil)
|
||||||
|
|
||||||
|
// Create a test handler that checks the context
|
||||||
|
var capturedOrg *domain.Organization
|
||||||
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if org := r.Context().Value(OrganizationKey); org != nil {
|
||||||
|
if o, ok := org.(domain.Organization); ok {
|
||||||
|
capturedOrg = &o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create request with API key in context
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
ctx := context.WithValue(req.Context(), ApiKey, apiKey)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Nil(t, capturedOrg, "Organization should not be set for invalid API key")
|
||||||
|
mockCache.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Handler_WithoutAPIKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
// The middleware passes the plaintext API key (cache handles hashing)
|
||||||
|
mockCache.On("OrganizationByAPIKey", "").Return(nil)
|
||||||
|
|
||||||
|
// Create a test handler that checks the context
|
||||||
|
var capturedOrg *domain.Organization
|
||||||
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if org := r.Context().Value(OrganizationKey); org != nil {
|
||||||
|
if o, ok := org.(domain.Organization); ok {
|
||||||
|
capturedOrg = &o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create request without API key
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Nil(t, capturedOrg, "Organization should not be set without API key")
|
||||||
|
mockCache.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Handler_WithValidJWT(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
// The middleware passes the plaintext API key (cache handles hashing)
|
||||||
|
mockCache.On("OrganizationByAPIKey", "").Return(nil)
|
||||||
|
|
||||||
|
userID := "user-123"
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"sub": userID,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a test handler that checks the context
|
||||||
|
var capturedUser string
|
||||||
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if user := r.Context().Value(UserKey); user != nil {
|
||||||
|
if u, ok := user.(string); ok {
|
||||||
|
capturedUser = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create request with JWT token in context
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
ctx := context.WithValue(req.Context(), mw.ContextKey{}, token)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Equal(t, userID, capturedUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Handler_APIKeyErrorHandling(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create request with invalid API key type in context
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
ctx := context.WithValue(req.Context(), ApiKey, 12345) // Invalid type
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, rec.Code)
|
||||||
|
assert.Contains(t, rec.Body.String(), "Invalid API Key format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Handler_JWTErrorHandling(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create request with invalid JWT token type in context
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
ctx := context.WithValue(req.Context(), mw.ContextKey{}, "not-a-token") // Invalid type
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, rec.Code)
|
||||||
|
assert.Contains(t, rec.Body.String(), "Invalid JWT token format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Handler_BothJWTAndAPIKey(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
orgID := uuid.New()
|
||||||
|
expectedOrg := &domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregate{
|
||||||
|
ID: eventsourced.IdFromString(orgID.String()),
|
||||||
|
},
|
||||||
|
Name: "Test Organization",
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := "user-123"
|
||||||
|
apiKey := "test-api-key-123"
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"sub": userID,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock expects plaintext key (cache handles hashing internally)
|
||||||
|
mockCache.On("OrganizationByAPIKey", apiKey).Return(expectedOrg)
|
||||||
|
|
||||||
|
// Create a test handler that checks both user and organization in context
|
||||||
|
var capturedUser string
|
||||||
|
var capturedOrg *domain.Organization
|
||||||
|
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if user := r.Context().Value(UserKey); user != nil {
|
||||||
|
if u, ok := user.(string); ok {
|
||||||
|
capturedUser = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if org := r.Context().Value(OrganizationKey); org != nil {
|
||||||
|
if o, ok := org.(domain.Organization); ok {
|
||||||
|
capturedOrg = &o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create request with both JWT and API key in context
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
|
ctx := context.WithValue(req.Context(), mw.ContextKey{}, token)
|
||||||
|
ctx = context.WithValue(ctx, ApiKey, apiKey)
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Equal(t, userID, capturedUser)
|
||||||
|
require.NotNil(t, capturedOrg)
|
||||||
|
assert.Equal(t, expectedOrg.Name, capturedOrg.Name)
|
||||||
|
mockCache.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserFromContext(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with valid user",
|
||||||
|
ctx: context.WithValue(context.Background(), UserKey, "user-123"),
|
||||||
|
expected: "user-123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without user",
|
||||||
|
ctx: context.Background(),
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with invalid type",
|
||||||
|
ctx: context.WithValue(context.Background(), UserKey, 123),
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := UserFromContext(tt.ctx)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrganizationFromContext(t *testing.T) {
|
||||||
|
orgID := uuid.New()
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregate{
|
||||||
|
ID: eventsourced.IdFromString(orgID.String()),
|
||||||
|
},
|
||||||
|
Name: "Test Org",
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ctx context.Context
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with valid organization",
|
||||||
|
ctx: context.WithValue(context.Background(), OrganizationKey, org),
|
||||||
|
expected: orgID.String(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without organization",
|
||||||
|
ctx: context.Background(),
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with invalid type",
|
||||||
|
ctx: context.WithValue(context.Background(), OrganizationKey, "not-an-org"),
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := OrganizationFromContext(tt.ctx)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Directive_RequiresUser(t *testing.T) {
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
requireUser := true
|
||||||
|
|
||||||
|
// Test with user present
|
||||||
|
ctx := context.WithValue(context.Background(), UserKey, "user-123")
|
||||||
|
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
}, &requireUser, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test without user
|
||||||
|
ctx = context.Background()
|
||||||
|
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
}, &requireUser, nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "no user available in request")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Directive_RequiresOrganization(t *testing.T) {
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
requireOrg := true
|
||||||
|
orgID := uuid.New()
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregate{
|
||||||
|
ID: eventsourced.IdFromString(orgID.String()),
|
||||||
|
},
|
||||||
|
Name: "Test Org",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with organization present
|
||||||
|
ctx := context.WithValue(context.Background(), OrganizationKey, org)
|
||||||
|
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
}, nil, &requireOrg)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test without organization
|
||||||
|
ctx = context.Background()
|
||||||
|
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
}, nil, &requireOrg)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "no organization available in request")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Directive_RequiresBoth(t *testing.T) {
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
requireUser := true
|
||||||
|
requireOrg := true
|
||||||
|
orgID := uuid.New()
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregate{
|
||||||
|
ID: eventsourced.IdFromString(orgID.String()),
|
||||||
|
},
|
||||||
|
Name: "Test Org",
|
||||||
|
}
|
||||||
|
|
||||||
|
// When both user and organization are marked as acceptable,
|
||||||
|
// the directive uses OR logic - either one is sufficient
|
||||||
|
|
||||||
|
// Test with both present - should succeed
|
||||||
|
ctx := context.WithValue(context.Background(), UserKey, "user-123")
|
||||||
|
ctx = context.WithValue(ctx, OrganizationKey, org)
|
||||||
|
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
}, &requireUser, &requireOrg)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test with only user - should succeed (OR logic)
|
||||||
|
ctx = context.WithValue(context.Background(), UserKey, "user-123")
|
||||||
|
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
}, &requireUser, &requireOrg)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test with only organization - should succeed (OR logic)
|
||||||
|
ctx = context.WithValue(context.Background(), OrganizationKey, org)
|
||||||
|
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
}, &requireUser, &requireOrg)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test with neither - should fail
|
||||||
|
ctx = context.Background()
|
||||||
|
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
}, &requireUser, &requireOrg)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "authentication required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) {
|
||||||
|
mockCache := new(MockCache)
|
||||||
|
authMiddleware := NewAuth(mockCache)
|
||||||
|
|
||||||
|
// Test with no requirements
|
||||||
|
ctx := context.Background()
|
||||||
|
result, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
}, nil, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "success", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithValidRole(t *testing.T) {
|
||||||
|
// Create token with roles claim
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"sub": "user-123",
|
||||||
|
"https://unbound.se/roles": []interface{}{"admin", "user"},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), mw.ContextKey{}, token)
|
||||||
|
|
||||||
|
// Test for existing role
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.True(t, hasRole)
|
||||||
|
|
||||||
|
hasRole = UserHasRole(ctx, "user")
|
||||||
|
assert.True(t, hasRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithoutRole(t *testing.T) {
|
||||||
|
// Create token with roles claim
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"sub": "user-123",
|
||||||
|
"https://unbound.se/roles": []interface{}{"user"},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), mw.ContextKey{}, token)
|
||||||
|
|
||||||
|
// Test for non-existing role
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.False(t, hasRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithoutRolesClaim(t *testing.T) {
|
||||||
|
// Create token without roles claim
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"sub": "user-123",
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), mw.ContextKey{}, token)
|
||||||
|
|
||||||
|
// Test should return false when roles claim is missing
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.False(t, hasRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithoutToken(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Test should return false when no token in context
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.False(t, hasRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithInvalidTokenType(t *testing.T) {
|
||||||
|
// Put invalid token type in context
|
||||||
|
ctx := context.WithValue(context.Background(), mw.ContextKey{}, "not-a-token")
|
||||||
|
|
||||||
|
// Test should return false when token type is invalid
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.False(t, hasRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithInvalidRolesType(t *testing.T) {
|
||||||
|
// Create token with invalid roles type
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"sub": "user-123",
|
||||||
|
"https://unbound.se/roles": "not-an-array",
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), mw.ContextKey{}, token)
|
||||||
|
|
||||||
|
// Test should return false when roles type is invalid
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.False(t, hasRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithInvalidRoleElementType(t *testing.T) {
|
||||||
|
// Create token with invalid role element types
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"sub": "user-123",
|
||||||
|
"https://unbound.se/roles": []interface{}{123, 456}, // Numbers instead of strings
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), mw.ContextKey{}, token)
|
||||||
|
|
||||||
|
// Test should return false when role elements are not strings
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.False(t, hasRole)
|
||||||
|
}
|
||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
"kubernetes"
|
"kubernetes"
|
||||||
],
|
],
|
||||||
"matchPackageNames": [
|
"matchPackageNames": [
|
||||||
"registry.gitlab.com/unboundsoftware/schemas"
|
"oci.unbound.se/unboundsoftware/schemas"
|
||||||
],
|
],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package sdlmerge
|
package sdlmerge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -61,12 +62,13 @@ func MergeSDLs(SDLs ...string) (string, error) {
|
|||||||
return "", fmt.Errorf("merge ast: %w", err)
|
return "", fmt.Errorf("merge ast: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := astprinter.PrintString(&doc)
|
// Format with indentation for better readability
|
||||||
if err != nil {
|
buf := &bytes.Buffer{}
|
||||||
|
if err := astprinter.PrintIndent(&doc, []byte(" "), buf); err != nil {
|
||||||
return "", fmt.Errorf("stringify schema: %w", err)
|
return "", fmt.Errorf("stringify schema: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateSubgraphs(subgraphs []string) error {
|
func validateSubgraphs(subgraphs []string) error {
|
||||||
|
|||||||
@@ -0,0 +1,434 @@
|
|||||||
|
package sdlmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMergeSDLs_Success(t *testing.T) {
|
||||||
|
// Both types need to be in the same subgraph or properly federated
|
||||||
|
sdl1 := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "Post")
|
||||||
|
assert.Contains(t, result, "id")
|
||||||
|
assert.Contains(t, result, "name")
|
||||||
|
assert.Contains(t, result, "title")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_SingleSchema(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type Query {
|
||||||
|
hello: String
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "Query")
|
||||||
|
assert.Contains(t, result, "hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_EmptySchemas(t *testing.T) {
|
||||||
|
result, err := MergeSDLs()
|
||||||
|
require.NoError(t, err)
|
||||||
|
// With no schemas, result will be empty after processing
|
||||||
|
// This is valid - just verifies no crash
|
||||||
|
_ = result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_InvalidSyntax(t *testing.T) {
|
||||||
|
invalidSDL := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
// Missing closing brace
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := MergeSDLs(invalidSDL)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "parse graphql document string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_UnknownType(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
profile: UnknownType
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := MergeSDLs(sdl)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "validate schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_DuplicateTypes_DifferentFields(t *testing.T) {
|
||||||
|
// Same type with different fields in different subgraphs - should fail
|
||||||
|
// In federation, shared types must be identical
|
||||||
|
sdl1 := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
sdl2 := `
|
||||||
|
type User {
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := MergeSDLs(sdl1, sdl2)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "shared type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_ExtendType(t *testing.T) {
|
||||||
|
sdl1 := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
sdl2 := `
|
||||||
|
extend type User {
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl1, sdl2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "id")
|
||||||
|
assert.Contains(t, result, "email")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Scalars(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
scalar DateTime
|
||||||
|
|
||||||
|
type Event {
|
||||||
|
id: ID!
|
||||||
|
createdAt: DateTime!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "DateTime")
|
||||||
|
assert.Contains(t, result, "Event")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Enums(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
enum Role {
|
||||||
|
ADMIN
|
||||||
|
USER
|
||||||
|
GUEST
|
||||||
|
}
|
||||||
|
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
role: Role!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "Role")
|
||||||
|
assert.Contains(t, result, "ADMIN")
|
||||||
|
assert.Contains(t, result, "USER")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Interfaces(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
interface Node {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type User implements Node {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "Node")
|
||||||
|
assert.Contains(t, result, "implements")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Unions(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bot {
|
||||||
|
id: ID!
|
||||||
|
version: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
union Actor = User | Bot
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "Actor")
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "Bot")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_InputTypes(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
input CreateUserInput {
|
||||||
|
name: String!
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createUser(input: CreateUserInput!): User
|
||||||
|
}
|
||||||
|
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "CreateUserInput")
|
||||||
|
assert.Contains(t, result, "createUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Directives(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String! @deprecated(reason: "Use fullName instead")
|
||||||
|
fullName: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "name")
|
||||||
|
assert.Contains(t, result, "fullName")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_FederationKeys(t *testing.T) {
|
||||||
|
// Federation @key directive
|
||||||
|
sdl := `
|
||||||
|
type User @key(fields: "id") {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
// @key directive should be removed during merge
|
||||||
|
assert.NotContains(t, result, "@key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_ExternalFields(t *testing.T) {
|
||||||
|
// Federation @external directive
|
||||||
|
sdl1 := `
|
||||||
|
type User @key(fields: "id") {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
sdl2 := `
|
||||||
|
extend type User @key(fields: "id") {
|
||||||
|
id: ID! @external
|
||||||
|
posts: [Post!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl1, sdl2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "Post")
|
||||||
|
// @external fields should be removed
|
||||||
|
assert.NotContains(t, result, "@external")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_ComplexSchema(t *testing.T) {
|
||||||
|
// Multiple subgraphs with various types - simplified to avoid cross-references
|
||||||
|
users := `
|
||||||
|
type User @key(fields: "id") {
|
||||||
|
id: ID!
|
||||||
|
username: String!
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
user(id: ID!): User
|
||||||
|
users: [User!]!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
posts := `
|
||||||
|
extend type User @key(fields: "id") {
|
||||||
|
id: ID! @external
|
||||||
|
posts: [Post!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post @key(fields: "id") {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
content: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Query {
|
||||||
|
post(id: ID!): Post
|
||||||
|
posts: [Post!]!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
comments := `
|
||||||
|
extend type Post @key(fields: "id") {
|
||||||
|
id: ID! @external
|
||||||
|
comments: [Comment!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Comment {
|
||||||
|
id: ID!
|
||||||
|
text: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Query {
|
||||||
|
comment(id: ID!): Comment
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(users, posts, comments)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify all types are present
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "Post")
|
||||||
|
assert.Contains(t, result, "Comment")
|
||||||
|
assert.Contains(t, result, "Query")
|
||||||
|
|
||||||
|
// Verify fields from all subgraphs
|
||||||
|
assert.Contains(t, result, "username")
|
||||||
|
assert.Contains(t, result, "posts")
|
||||||
|
assert.Contains(t, result, "comments")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_EmptyTypeDefinition(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type Empty {}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := MergeSDLs(sdl)
|
||||||
|
require.Error(t, err)
|
||||||
|
// Empty types are invalid in GraphQL
|
||||||
|
assert.Contains(t, err.Error(), "empty body")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_MultipleValidationErrors(t *testing.T) {
|
||||||
|
// Schema with multiple errors
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
profile: NonExistentType1
|
||||||
|
settings: NonExistentType2
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := MergeSDLs(sdl)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_ListTypes(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
tags: [String!]!
|
||||||
|
friends: [User!]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "tags")
|
||||||
|
assert.Contains(t, result, "friends")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_NonNullTypes(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
email: String
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "id")
|
||||||
|
assert.Contains(t, result, "name")
|
||||||
|
assert.Contains(t, result, "email")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Comments(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
# This is a user type
|
||||||
|
type User {
|
||||||
|
# User ID
|
||||||
|
id: ID!
|
||||||
|
# User name
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_LargeSchema(t *testing.T) {
|
||||||
|
// Test with a reasonably large schema to ensure performance
|
||||||
|
var sdlBuilder strings.Builder
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
sdlBuilder.WriteString("type Type")
|
||||||
|
sdlBuilder.WriteString(strings.Repeat(string(rune('A'+i%26)), 1))
|
||||||
|
sdlBuilder.WriteString(string(rune('0' + i/26)))
|
||||||
|
sdlBuilder.WriteString(" { id: ID }\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdlBuilder.String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify some types are present
|
||||||
|
assert.Contains(t, result, "TypeA0")
|
||||||
|
assert.Contains(t, result, "TypeB0")
|
||||||
|
assert.Contains(t, result, "TypeC0")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user