diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 322e8a1..d38ce63 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,10 +8,11 @@ jobs: linters: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 with: go-version: 1.18.x - uses: golangci/golangci-lint-action@v3 with: + version: v1.55.2 args: --timeout 3m --verbose diff --git a/.github/workflows/publiccode-yml-validation.yml b/.github/workflows/publiccode-yml-validation.yml new file mode 100644 index 0000000..41b97a4 --- /dev/null +++ b/.github/workflows/publiccode-yml-validation.yml @@ -0,0 +1,14 @@ +on: [pull_request] + +jobs: + publiccode_yml_validation: + runs-on: ubuntu-latest + name: publiccode.yml validation + steps: + - uses: actions/checkout@v2 + - uses: italia/publiccode-parser-action@v1 + with: + publiccode: 'publiccode.yml' + comment-on-pr: true + env: + REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-staging-docker-image.yml b/.github/workflows/publish-staging-docker-image.yml new file mode 100644 index 0000000..d8f1b18 --- /dev/null +++ b/.github/workflows/publish-staging-docker-image.yml @@ -0,0 +1,41 @@ +# Push the staging Docker image (ghcr.italia/developers-italia-api:main) +# on updates to main. + +on: + push: + branches: [ main ] + +permissions: + # To push Docker images to GitHub + packages: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + uses: actions/setup-go@v4 + with: + go-version: 1.18.x + - + uses: actions/checkout@v3 + with: + # All history, required for goreleaser + fetch-depth: 0 + - + # FIXME: goreleaser should already take care of the login + # (see https://github.com/goreleaser/goreleaser/blame/02a3486d4ba59505113a57b438ae567351ed3dab/scripts/entrypoint.sh#L17) + # but it doesn't work for some reason. + run: echo "$GITHUB_TOKEN" | docker login ghcr.io -u docker --password-stdin + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - + # Tag with a temporary valid semantic version. This is required by goreleaser. + run: git tag v0-main-$(git rev-parse --short HEAD) + + - + uses: goreleaser/goreleaser-action@v2 + with: + version: v1.9.2 + args: release --config .goreleaser.staging.yaml diff --git a/.github/workflows/release-chart.yml b/.github/workflows/release-chart.yml index 9cf8175..ae0304d 100644 --- a/.github/workflows/release-chart.yml +++ b/.github/workflows/release-chart.yml @@ -10,7 +10,7 @@ jobs: release_chart: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get version id: get_version run: echo "::set-output name=version::${GITHUB_REF_NAME#helm/}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a69a3df..f7cc67c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,11 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: go-version: 1.18.x - - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # All history, required for goreleaser fetch-depth: 0 diff --git a/.github/workflows/spectral.yaml b/.github/workflows/spectral.yaml index df4617e..1a1805c 100644 --- a/.github/workflows/spectral.yaml +++ b/.github/workflows/spectral.yaml @@ -11,7 +11,7 @@ jobs: spectral: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - run: curl https://italia.github.io/api-oas-checker/spectral-full.yml > .spectral.yml # Get additional module required by spectral-full diff --git a/.github/workflows/tests-postgresql.yml b/.github/workflows/tests-postgresql.yml new file mode 100644 index 0000000..41500a7 --- /dev/null +++ b/.github/workflows/tests-postgresql.yml @@ -0,0 +1,32 @@ +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + tests-postgresql: + runs-on: ubuntu-latest + + services: + db: + image: postgres:14 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: 1.18.x + - run: go test -race ./... + env: + DATABASE_DSN: "postgres://postgres:postgres@localhost:5432/test?sslmode=disable" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests-sqlite.yml similarity index 57% rename from .github/workflows/tests.yml rename to .github/workflows/tests-sqlite.yml index cdcbc9c..7ecf571 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests-sqlite.yml @@ -5,11 +5,13 @@ on: branches: [main] jobs: - tests: + tests-sqlite: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 with: go-version: 1.18.x - run: go test -race ./... + env: + DATABASE_DSN: "file:/tmp/test.db" diff --git a/.golangci.yaml b/.golangci.yaml index 158610a..39ebaa0 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -74,11 +74,16 @@ linters: - interfacer - maligned - scopelint + - structcheck + # We don't want to limit dependencies + - depguard # Not terribly useful and ends up in too much boilerplate - exhaustruct # False positives (https://github.com/daixiang0/gci/issues/54) - gci + # Seems excessive + - tagalign # Run only fast linters from enabled linters set (first run won't be fast) # Default: false diff --git a/.goreleaser.staging.yaml b/.goreleaser.staging.yaml new file mode 100644 index 0000000..56c9650 --- /dev/null +++ b/.goreleaser.staging.yaml @@ -0,0 +1,21 @@ +# Goreleases configuration for staging builds, just builds and pushes +# the Docker image for the main branch. +# +# Make sure to check the documentation at https://goreleaser.com + +# Don't create a GitHub release +release: + disable: true + +builds: + - env: + - CGO_ENABLED=0 + ldflags: + - -s -w + goos: + - linux +dockers: + - + dockerfile: Dockerfile.goreleaser + image_templates: + - "ghcr.io/italia/developers-italia-api:main" diff --git a/Dockerfile b/Dockerfile index e30d58e..cfeb813 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # # This is for local development only. -# See Dockerfile.goreleaser for the image published on release. +# See Dockerfile.goreleaser for the image published on release or staging. # FROM golang:1.18 as base diff --git a/README.md b/README.md index 4d9a038..3a18965 100644 --- a/README.md +++ b/README.md @@ -24,25 +24,25 @@

- + API documentation

- Developers Italia API is the RESTful API of the Free and Open Source software catalog + Developers Italia API is the RESTful API of the Free and Open Source software catalog aimed at Italian Public Administrations.

-# Requirements +## Requirements -* Golang 1.18 +* Golang 1.18+ * [PostgreSQL](https://https://www.postgresql.org/) -# Development +## Development -The application uses [https://github.com/cosmtrek/air](Air) for live-reloading +The application uses [Air](https://github.com/cosmtrek/air) for live-reloading in the development environment. To start developing: @@ -61,17 +61,22 @@ endpoints at `http://localhost:3000/v1/`. The application will automatically reload when a change is made. -# Configuration +## Configuration You can configure the API with environment variables: -* `DATABASE_DSN`: the data source used to connect to the database, - fe `host=db user=postgres password=postgres dbname=postgres port=5432`. +* `DATABASE_DSN`: the URI used to connect to the database, + fe `postgres://user:password@host:5432/dbname`. Supports PostgreSQL and SQLite. * `PASETO_KEY` (optional): Base64 encoded 32 bytes key used to check the - [PASETO](https://paseto.io/) authentication tokens. - If not set the API will run in read only mode. + [PASETO](https://paseto.io/) authentication tokens. You can generate it with + + ```console + head -c 32 /dev/urandom | base64 + ``` + + If not set, the API will run in read only mode. * `ENVIRONMENT` (optional): possible values `test`, `development`, `production`. Default `production`. @@ -80,7 +85,7 @@ You can configure the API with environment variables: will be ratelimited. Default: no limit. -# Contributing +## Contributing This project exists also thanks to your contributions! Here is a list of people who already contributed to this repository: @@ -91,8 +96,8 @@ who already contributed to this repository: /> -# License +## License -Copyright © 2022 - Presidenza del Consiglio dei Ministri +Copyright © 2022-present Presidenza del Consiglio dei Ministri The source code is released under the AGPL version 3. diff --git a/charts/developers-italia-api/Chart.yaml b/charts/developers-italia-api/Chart.yaml index a0bcdec..2e59a2a 100644 --- a/charts/developers-italia-api/Chart.yaml +++ b/charts/developers-italia-api/Chart.yaml @@ -15,12 +15,12 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: v0.1.0 +version: v0.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.3.0" +appVersion: "v0.7.3" icon: https://avatars.githubusercontent.com/u/15377824?s=96&v=4 diff --git a/charts/developers-italia-api/README.md b/charts/developers-italia-api/README.md index 7dda4d7..52bfb95 100644 --- a/charts/developers-italia-api/README.md +++ b/charts/developers-italia-api/README.md @@ -1,6 +1,6 @@ # developers-italia-api -![Version: v0.1.0](https://img.shields.io/badge/Version-v0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.3.0](https://img.shields.io/badge/AppVersion-v0.3.0-informational?style=flat-square) +![Version: v0.3.0](https://img.shields.io/badge/Version-v0.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.7.3](https://img.shields.io/badge/AppVersion-v0.7.3-informational?style=flat-square) The API of Developers Italia @@ -15,6 +15,8 @@ The API of Developers Italia | autoscaling.targetCPUUtilizationPercentage | int | `80` | | | databaseDSN | string | `""` | Database connection string, e.g. "host= port=5432 dbname= user= password= sslmode=require". | | deploymentAnnotations | object | `{}` | | +| extraVolumeMounts | list | `[]` | | +| extraVolumes | list | `[]` | | | fullnameOverride | string | `""` | | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"ghcr.io/italia/developers-italia-api"` | | @@ -41,8 +43,14 @@ The API of Developers Italia | serviceAccount.annotations | object | `{}` | | | serviceAccount.create | bool | `true` | | | serviceAccount.name | string | `""` | | +| serviceMonitor.additionalLabels | object | `{}` | | +| serviceMonitor.enabled | bool | `false` | Create ServiceMonitor resource (requires corresponding Prometheus Operator CRD installed). | +| serviceMonitor.interval | string | `nil` | Interval at which metrics should be scraped (uses Prometheus default if unspecified, details [here](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.Endpoint)) | +| serviceMonitor.jobLabel | string | `nil` | Defaults to the name of the Kubernetes service (details [here](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.ServiceMonitor)). | +| serviceMonitor.scrapeTimeout | string | `nil` | Timeout after which the scrape is ended (uses Prometheus default if unspecified, details [here](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.Endpoint)). | +| serviceMonitor.targetLabels | list | `[]` | | | tolerations | list | `[]` | | | useExistingSecret | string | `nil` | Name of existing Kubernetes secret containing keys 'databaseDSN' and 'pasetoKey'. If not provided, a secret will be generated using values from 'databaseDSN' and 'pasetoKey'. | ---------------------------------------------- -Autogenerated from chart metadata using [helm-docs vv1.11.0](https://github.com/norwoodj/helm-docs/releases/vv1.11.0) +Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0) diff --git a/charts/developers-italia-api/templates/deployment.yaml b/charts/developers-italia-api/templates/deployment.yaml index 4130bb0..c6217f6 100644 --- a/charts/developers-italia-api/templates/deployment.yaml +++ b/charts/developers-italia-api/templates/deployment.yaml @@ -6,6 +6,8 @@ metadata: {{- include "developers-italia-api.labels" . | nindent 4 }} {{- with .Values.deploymentAnnotations }} annotations: + timestamp: {{ now | date "20060102150405" | quote }} + {{- toYaml . | nindent 4 }} {{- end }} spec: @@ -66,6 +68,10 @@ spec: port: http resources: {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.extraVolumeMounts }} + volumeMounts: + {{- toYaml .Values.extraVolumeMounts | nindent 12 }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -78,3 +84,7 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{- if .Values.extraVolumes }} + volumes: + {{- toYaml .Values.extraVolumes | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/charts/developers-italia-api/templates/servicemonitor.yaml b/charts/developers-italia-api/templates/servicemonitor.yaml new file mode 100644 index 0000000..6c29a56 --- /dev/null +++ b/charts/developers-italia-api/templates/servicemonitor.yaml @@ -0,0 +1,35 @@ +{{- if .Values.serviceMonitor.enabled -}} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "developers-italia-api.fullname" . }} + labels: + {{- include "developers-italia-api.labels" . | nindent 4 }} + {{- if .Values.serviceMonitor.additionalLabels }} + {{- toYaml .Values.serviceMonitor.additionalLabels | nindent 4 }} + {{- end }} +spec: + endpoints: + - port: http + {{- if .Values.serviceMonitor.interval }} + interval: {{ .Values.serviceMonitor.interval }} + {{- end }} + {{- if .Values.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} + {{- end }} +{{- if .Values.serviceMonitor.jobLabel }} + jobLabel: {{ .Values.serviceMonitor.jobLabel | quote }} +{{- end }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} +{{- if .Values.serviceMonitor.targetLabels }} + targetLabels: + {{- range .Values.serviceMonitor.targetLabels }} + - {{ . }} + {{- end }} +{{- end }} + selector: + matchLabels: + {{- include "developers-italia-api.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/charts/developers-italia-api/values.yaml b/charts/developers-italia-api/values.yaml index 3e97147..79ef8dd 100644 --- a/charts/developers-italia-api/values.yaml +++ b/charts/developers-italia-api/values.yaml @@ -100,3 +100,19 @@ databaseDSN: "" # -- Base64 encoded Paseto Key. pasetoKey: "" + +serviceMonitor: + # -- Create ServiceMonitor resource (requires corresponding Prometheus Operator CRD installed). + enabled: false + additionalLabels: {} + # -- (string) Defaults to the name of the Kubernetes service (details [here](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.ServiceMonitor)). + jobLabel: + # -- (string) Interval at which metrics should be scraped (uses Prometheus default if unspecified, details [here](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.Endpoint)) + interval: + # -- (string) Timeout after which the scrape is ended (uses Prometheus default if unspecified, details [here](https://prometheus-operator.dev/docs/operator/api/#monitoring.coreos.com/v1.Endpoint)). + scrapeTimeout: + targetLabels: [] + +extraVolumes: [] + +extraVolumeMounts: [] diff --git a/developers-italia.oas.yaml b/developers-italia.oas.yaml index 9c3b1ee..0531c81 100644 --- a/developers-italia.oas.yaml +++ b/developers-italia.oas.yaml @@ -28,11 +28,11 @@ info: servers: - url: 'https://api.developers.italia.it/v1' description: The production server + - url: 'https://api-staging.developers.italia.it/v1' + description: The staging server tags: - name: software description: Operations on software - - name: bundles - description: Operations on bundles - name: logs description: Operations on logs - name: publishers @@ -576,144 +576,6 @@ paths: $ref: '#/components/callbacks/ResourceUpdate' delete: $ref: '#/components/callbacks/ResourceDelete' - /bundles: - get: - summary: List all Bundles - description: List all Bundles - tags: - - bundles - responses: - '200': - description: OK - content: - application/json: - schema: - type: object - additionalProperties: false - properties: - data: - type: array - minItems: 0 - maxItems: 100 - items: - $ref: '#/components/schemas/Bundle' - links: - $ref: '#/components/schemas/Links' - '400': - $ref: '#/components/responses/BadRequest' - '429': - $ref: '#/components/responses/TooManyRequests' - operationId: list-bundles - post: - summary: Create a Bundle - description: Create a Bundle - tags: - - bundles - security: - - bearerAuth: [] - operationId: create-bundles - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/Bundle' - '400': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/UnprocessableEntity' - '429': - $ref: '#/components/responses/TooManyRequests' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Bundle' - examples: {} - '/bundle/{bundleId}': - parameters: - - schema: - type: string - maxLength: 36 - pattern: '[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}' - name: bundleId - in: path - required: true - get: - summary: Get a Bundle - description: Get a Bundle by its id - tags: - - bundles - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/Bundle' - '400': - $ref: '#/components/responses/BadRequest' - '404': - $ref: '#/components/responses/NotFound' - '429': - $ref: '#/components/responses/TooManyRequests' - operationId: show-bundle-bundleId - patch: - summary: Update a Bundle - description: Update a Bundle by its id - tags: - - bundles - security: - - bearerAuth: [] - operationId: update-bundle-bundleId - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/Software' - '400': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - '404': - $ref: '#/components/responses/NotFound' - '422': - $ref: '#/components/responses/UnprocessableEntity' - '429': - $ref: '#/components/responses/TooManyRequests' - requestBody: - required: true - content: - application/merge-patch+json: - schema: - $ref: '#/components/schemas/Bundle' - delete: - summary: Delete a Bundle - description: Delete a Bundle by its id - tags: - - bundles - security: - - bearerAuth: [] - operationId: remove-bundle-bundleId - responses: - '204': - $ref: '#/components/responses/NoContent' - '400': - $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - '404': - $ref: '#/components/responses/NotFound' - '429': - $ref: '#/components/responses/TooManyRequests' /logs: get: summary: List all Logs @@ -867,6 +729,20 @@ paths: in: query name: 'page[after]' description: Only results after this cursor + - schema: + type: string + format: date-time + example: '2022-06-07T09:56:23Z' + in: query + name: from + description: Only publishers created after this time (RFC 3339 datetime) + - schema: + type: string + format: date-time + example: '2022-06-07T14:56:23Z' + in: query + name: to + description: Only publishers created before this time (RFC 3339 datetime) post: summary: Create a new Publisher description: Create a new Publisher @@ -910,7 +786,7 @@ paths: required: true get: summary: Get a Publisher - description: Get a Publisher by its id + description: Get a Publisher by its id or alternativeId tags: - publishers responses: @@ -927,7 +803,7 @@ paths: operationId: show-publisher-publisherId patch: summary: Update a Publisher - description: Update a Publisher by its id. Only the fields that are provided will be updated, codeHosting field will be overwritten entirely. + description: Update a Publisher by its id or alternativeId tags: - publishers security: @@ -958,7 +834,7 @@ paths: $ref: '#/components/schemas/Publisher' delete: summary: Delete a Publisher - description: Delete a Publisher by its id + description: Delete a Publisher by its id or alternativeId tags: - publishers security: @@ -1531,8 +1407,13 @@ components: alternativeId: type: string description: | - Optional alternative custom id for this Publisher. This is useful for example if this - Publisher has another id or code in a different database. + Optional alternative user-provided identifier for this Publisher. + + If present, you can use it in place of the Publisher `id` as path query + argument. + + This is useful for example if this Publisher has another id or code in a + different database. maxLength: 255 example: 'ID-1234' pattern: '.*' @@ -1572,21 +1453,18 @@ components: type: string maxLength: 255 pattern: '.*' + description: > + The resource this log is about (fe. a particular Publisher / Software). + It might be absent if the log is not about a specific resource or if + the resource doesn't exist yet, like in case of the error log caused by + failure to create it. + example: /software/7589be36-f046-45c6-9223-b7de9dbf06cd readOnly: true required: - id - createdAt - updatedAt - message - Bundle: - title: Bundle - type: object - additionalProperties: false - properties: - id: - type: string - maxLength: 36 - pattern: '[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}' Links: type: object additionalProperties: false diff --git a/go.mod b/go.mod index 2e2d65f..59ed550 100644 --- a/go.mod +++ b/go.mod @@ -8,55 +8,70 @@ require ( github.com/go-playground/validator/v10 v10.11.0 github.com/go-testfixtures/testfixtures/v3 v3.8.0 github.com/gofiber/contrib/paseto v0.0.0-20220621082844-83549332c36e - github.com/gofiber/fiber/v2 v2.34.1 - github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa - github.com/stretchr/testify v1.7.5 - gorm.io/driver/postgres v1.3.7 - gorm.io/driver/sqlite v1.3.4 - gorm.io/gorm v1.23.6 + github.com/gofiber/fiber/v2 v2.52.1 + github.com/stretchr/testify v1.8.1 + gorm.io/driver/postgres v1.5.2 + gorm.io/driver/sqlite v1.5.2 + gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 ) -require golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 +require ( + github.com/ansrivas/fiberprometheus/v2 v2.4.1 + github.com/evanphx/json-patch/v5 v5.7.0 + golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 +) require ( - github.com/mattn/go-sqlite3 v1.14.13 // indirect - golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/gofiber/adaptor/v2 v2.1.25 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/lib/pq v1.10.9 + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-sqlite3 v1.14.17 + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/philhofer/fwd v1.1.2 // indirect + github.com/prometheus/client_golang v1.12.2 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/tinylib/msgp v1.1.8 // indirect + golang.org/x/net v0.17.0 // indirect + google.golang.org/protobuf v1.26.0 // indirect ) require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.5.0 // indirect github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7 // indirect github.com/o1egl/paseto v1.0.0 github.com/pilagod/gorm-cursor-paginator/v2 v2.3.0 ) require ( - github.com/andybalholm/brotli v1.0.4 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.12.1 - github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.11.0 // indirect - github.com/jackc/pgx/v4 v4.16.1 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.3.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/klauspost/compress v1.15.7 // indirect + github.com/klauspost/compress v1.17.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.38.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect - golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 91fb1c3..054be10 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,38 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/PuerkitoBio/purell v1.2.0 h1:/Jdm5QfyM8zdlqT6WVZU4cfP23sot6CEHA4CS49Ezig= github.com/PuerkitoBio/purell v1.2.0/go.mod h1:OhLRTaaIzhvIyofkJfB24gokC7tM42Px5UhoT32THBk= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= @@ -10,11 +42,31 @@ github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOd github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/ansrivas/fiberprometheus/v2 v2.4.1 h1:V87ahTcU/I4c8tD6GKiuyyB0Z82dw2VVqLDgBtUcUgc= +github.com/ansrivas/fiberprometheus/v2 v2.4.1/go.mod h1:ATJ3l0sufyoZBz+TEohAyQJqbgUSQaPwCHNL/L67Wnw= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/caarlos0/env/v6 v6.9.3 h1:Tyg69hoVXDnpO5Qvpsu8EoquarbPyQb+YwExWHP8wWU= github.com/caarlos0/env/v6 v6.9.3/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -24,7 +76,20 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/denisenkom/go-mssqldb v0.12.2 h1:1OcPn5GBIobjWNd+8yjfHNIaFX14B1pWI3F9HZy5KXw= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -38,20 +103,80 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-testfixtures/testfixtures/v3 v3.8.0 h1:ZdMx3/TQvzOWMm7x03IMpGTsIKRo6WCIfNm6dBOYO1M= github.com/go-testfixtures/testfixtures/v3 v3.8.0/go.mod h1:r3jeE9Ten7qBsZYSCwZiqCCUnq4pJsZEsq5ShqoX5aA= +github.com/gofiber/adaptor/v2 v2.1.25 h1:K2Ef2a7mUsCfL/oJdzbjyMXchGYuUUwIVXrYVm+P+xs= +github.com/gofiber/adaptor/v2 v2.1.25/go.mod h1:gOxtwMVqUStB5goAYtKd+hSvGupdd+aRIafZHPLNaUk= github.com/gofiber/contrib/paseto v0.0.0-20220621082844-83549332c36e h1:ZsZeaEFExhECRgT/yYWxmOtbKjPbZxhe9pk1MSz1rOE= github.com/gofiber/contrib/paseto v0.0.0-20220621082844-83549332c36e/go.mod h1:zU7RONlVB7dBKM0EijoExlxAWJtevT5+9RqAH2imOUI= -github.com/gofiber/fiber/v2 v2.34.1 h1:C6saXB7385HvtXX+XMzc5Dqj5S/aEXOfKCW7JNep4rA= github.com/gofiber/fiber/v2 v2.34.1/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U= +github.com/gofiber/fiber/v2 v2.36.0/go.mod h1:tgCr+lierLwLoVHHO/jn3Niannv34WRkQETU8wiL9fQ= +github.com/gofiber/fiber/v2 v2.52.1 h1:1RoU2NS+b98o1L77sdl5mboGPiW+0Ypsi5oLmcYlgHI= +github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7 h1:ux/56T2xqZO/3cP1I2F86qpeoYPCOzk+KF/UH/Ar+lk= github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= @@ -63,20 +188,13 @@ github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5 github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= -github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= -github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= -github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= @@ -84,12 +202,11 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.0 h1:brH0pCGBDkBW07HWlN/oSBXrmo3WB0UvZd1pIuDcL8Y= -github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= @@ -97,9 +214,7 @@ github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4 github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.11.0 h1:u4uiGPz/1hryuXzyaBhSk6dnIyyG2683olG2OV+UUgs= -github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= @@ -107,29 +222,38 @@ github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXg github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= -github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.7 h1:7cgTQxJCU/vy+oP/E3B9RGbQTgbiVzIJWIKOLoAsPok= -github.com/klauspost/compress v1.15.7/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -145,22 +269,39 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= -github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= -github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/o1egl/paseto v1.0.0 h1:bwpvPu2au176w4IBlhbyUv/S5VPptERIA99Oap5qUd0= github.com/o1egl/paseto v1.0.0/go.mod h1:5HxsZPmw/3RI2pAwGo1HhOOwSdvBpcuVzO7uDkm+CLU= +github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pilagod/gorm-cursor-paginator/v2 v2.3.0 h1:/jMv0+CtmpF5gf8Fn0JwgFsKcVrmvhPahHluHEcP13I= github.com/pilagod/gorm-cursor-paginator/v2 v2.3.0/go.mod h1:N2MKvSTVk9fHC/ofXDRSbHY9WZAe23g4UzBLorR54cE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -170,6 +311,30 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= @@ -181,14 +346,15 @@ github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThC github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -196,124 +362,374 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q= -github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= -github.com/valyala/fasthttp v1.38.0 h1:yTjSSNjuDi2PPvXY2836bIwLmiTS2T4T9p1coQshpco= github.com/valyala/fasthttp v1.38.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/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.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/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.1-0.20180807135948-17ff2d5776d2/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= -gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ= -gorm.io/driver/postgres v1.3.7/go.mod h1:f02ympjIcgtHEGFMZvdgTxODZ9snAHDb4hXfigBVuNI= +gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= +gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= -gorm.io/driver/sqlite v1.3.4 h1:NnFOPVfzi4CPsJPH4wXr6rMkPb4ElHEqKMvrsx9c9Fk= -gorm.io/driver/sqlite v1.3.4/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U= +gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc= +gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.7/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= -gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= -gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0= -gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/common/errors.go b/internal/common/errors.go index b624c26..f0c34a0 100644 --- a/internal/common/errors.go +++ b/internal/common/errors.go @@ -10,18 +10,21 @@ var ( ErrAuthentication = errors.New("token authentication failed") ErrInvalidDateTime = errors.New("invalid date time format (RFC 3339 needed)") ErrKeyLen = errors.New("PASETO_KEY must be 32 bytes long once base64-decoded") - - ErrDBUniqueConstraint = errors.New("db constraint violation") - ErrDBRecordNotFound = errors.New("record not found") ) +func InternalServerError(title string) ProblemJSONError { + return Error(fiber.StatusInternalServerError, title, fiber.ErrInternalServerError.Message) +} + func Error(status int, title string, detail string) ProblemJSONError { return ProblemJSONError{Title: title, Detail: detail, Status: status} } func ErrorWithValidationErrors( - status int, title string, detail string, validationErrors []ValidationError, + status int, title string, validationErrors []ValidationError, ) ProblemJSONError { + detail := GenerateErrorDetails(validationErrors) + return ProblemJSONError{Title: title, Detail: detail, Status: status, ValidationErrors: validationErrors} } @@ -48,11 +51,7 @@ func CustomErrorHandler(ctx *fiber.Ctx, err error) error { } } - err = ctx.Status(problemJSON.Status).JSON(problemJSON) - - // Needs to be after the call to JSON(), to override the - // automatic Content-Type - ctx.Set(fiber.HeaderContentType, "application/problem+json") + ctx.Status(problemJSON.Status) - return err + return ctx.JSON(problemJSON, "application/problem+json") } diff --git a/internal/common/requests.go b/internal/common/requests.go index 6c40f94..95fc904 100644 --- a/internal/common/requests.go +++ b/internal/common/requests.go @@ -11,11 +11,11 @@ type PublisherPost struct { } type PublisherPatch struct { - CodeHosting []CodeHosting `json:"codeHosting" validate:"gt=0"` - Description string `json:"description"` - Email string `json:"email" validate:"email"` - Active *bool `json:"active"` - AlternativeID string `json:"alternativeId" validate:"max=255"` + CodeHosting *[]CodeHosting `json:"codeHosting" validate:"omitempty,gt=0,dive"` + Description *string `json:"description"` + Email *string `json:"email" validate:"omitempty,email"` + Active *bool `json:"active"` + AlternativeID *string `json:"alternativeId" validate:"omitempty,max=255"` } type CodeHosting struct { @@ -31,9 +31,9 @@ type SoftwarePost struct { } type SoftwarePatch struct { - URL string `json:"url" validate:"url"` + URL *string `json:"url" validate:"omitempty,url"` Aliases *[]string `json:"aliases" validate:"omitempty,dive,url"` - PubliccodeYml string `json:"publiccodeYml"` + PubliccodeYml *string `json:"publiccodeYml"` Active *bool `json:"active"` } diff --git a/internal/common/validator.go b/internal/common/validator.go index 8540ec3..e61c8c3 100644 --- a/internal/common/validator.go +++ b/internal/common/validator.go @@ -2,12 +2,15 @@ package common import ( "errors" + "fmt" "reflect" "strings" "github.com/gofiber/fiber/v2" "github.com/go-playground/validator/v10" + + "github.com/italia/developers-italia-api/internal/jsondecoder" ) const ( @@ -18,7 +21,7 @@ const ( type ValidationError struct { Field string `json:"field"` Rule string `json:"rule"` - Value string `json:"value,omitempty"` + Value string `json:"value"` } func ValidateStruct(validateStruct interface{}) []ValidationError { @@ -62,14 +65,43 @@ func ValidateStruct(validateStruct interface{}) []ValidationError { func ValidateRequestEntity(ctx *fiber.Ctx, request interface{}, errorMessage string) error { if err := ctx.BodyParser(request); err != nil { - return Error(fiber.StatusBadRequest, errorMessage, "invalid json") + if errors.Is(err, jsondecoder.ErrUnknownField) { + return Error(fiber.StatusUnprocessableEntity, errorMessage, err.Error()) + } + + return Error(fiber.StatusBadRequest, errorMessage, "invalid or malformed JSON") } if err := ValidateStruct(request); err != nil { return ErrorWithValidationErrors( - fiber.StatusUnprocessableEntity, errorMessage, "invalid format", err, + fiber.StatusUnprocessableEntity, errorMessage, err, ) } return nil } + +func GenerateErrorDetails(validationErrors []ValidationError) string { + var errors []string + + for _, validationError := range validationErrors { + switch validationError.Rule { + case "required": + errors = append(errors, fmt.Sprintf("%s is required", validationError.Field)) + case "email": + errors = append(errors, fmt.Sprintf("%s is not a valid email", validationError.Field)) + case "min": + errors = append(errors, fmt.Sprintf("%s does not meet its size limits (too short)", validationError.Field)) + case "max": + errors = append(errors, fmt.Sprintf("%s does not meet its size limits (too long)", validationError.Field)) + case "gt": + errors = append(errors, fmt.Sprintf("%s does not meet its size limits (too few items)", validationError.Field)) + default: + errors = append(errors, fmt.Sprintf("%s is invalid", validationError.Field)) + } + } + + errorDetails := fmt.Sprintf("invalid format: %s", strings.Join(errors, ", ")) + + return errorDetails +} diff --git a/internal/database/database.go b/internal/database/database.go index 2a694fb..232c57e 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -1,57 +1,65 @@ package database import ( - "errors" + "fmt" "log" "strings" - "github.com/jackc/pgconn" + "github.com/italia/developers-italia-api/internal/models" - "github.com/italia/developers-italia-api/internal/common" - "github.com/jackc/pgerrcode" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/logger" ) -const ( - uniqueConstraintFailedErrorSQLite = "UNIQUE constraint failed" -) - -type Database interface { - Init(dsn string) (*gorm.DB, error) -} +func NewDatabase(connection string) (*gorm.DB, error) { + var ( + database *gorm.DB + err error + ) -//nolintlint:ireturn -func NewDatabase(env common.Environment) Database { - if env.IsTest() { + switch { + case strings.HasPrefix(connection, "file:"): log.Println("using SQLite database") - return &SQLiteDB{ - dsn: env.Database, - } - } + database, err = gorm.Open(sqlite.Open(connection), &gorm.Config{TranslateError: true}) + default: + log.Println("using Postgres database") - log.Println("using Postgres database") - - return &PostgresDB{ - dsn: env.Database, + database, err = gorm.Open(postgres.Open(connection), &gorm.Config{ + TranslateError: true, + PrepareStmt: true, + // Disable logging in production + Logger: logger.Default.LogMode(logger.Silent), + }) } -} -//nolint:errorlint -func WrapErrors(dbError error) error { - if strings.Contains(dbError.Error(), uniqueConstraintFailedErrorSQLite) { - return common.ErrDBUniqueConstraint + if err != nil { + return nil, fmt.Errorf("can't open database: %w", err) } - if e, ok := dbError.(*pgconn.PgError); ok { - if e.Code == pgerrcode.UniqueViolation { - return common.ErrDBUniqueConstraint - } + if err = database.AutoMigrate( + &models.Publisher{}, + &models.Event{}, + &models.CodeHosting{}, + &models.Software{}, + &models.SoftwareURL{}, + &models.Webhook{}, + ); err != nil { + return nil, fmt.Errorf("can't migrate database: %w", err) } - if errors.Is(dbError, gorm.ErrRecordNotFound) { - return common.ErrDBRecordNotFound + // Migrate logs only if there is no "entity" column yet, which should mean when the database + // is empty. + // This is a workaround for https://github.com/go-gorm/gorm/issues/5534 where GORM + // fails to migrate an existing generated column on PostgreSQL if it already exists. + var entity string + if database.Raw("SELECT entity FROM logs LIMIT 1").Scan(&entity).Error != nil { + if err = database.AutoMigrate(&models.Log{}); err != nil { + return nil, fmt.Errorf("can't migrate database: %w", err) + } } - return dbError + return database, nil } diff --git a/internal/database/postgres_database.go b/internal/database/postgres_database.go deleted file mode 100644 index ae0381b..0000000 --- a/internal/database/postgres_database.go +++ /dev/null @@ -1,41 +0,0 @@ -package database - -import ( - "fmt" - - "github.com/italia/developers-italia-api/internal/models" - "gorm.io/driver/postgres" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -type PostgresDB struct { - dsn string -} - -func (d *PostgresDB) Init(dsn string) (*gorm.DB, error) { - var err error - - database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ - PrepareStmt: true, - // Disable logging in production - Logger: logger.Default.LogMode(logger.Silent), - }) - if err != nil { - return nil, fmt.Errorf("can't open database: %w", err) - } - - if err = database.AutoMigrate( - &models.Publisher{}, - &models.Event{}, - &models.CodeHosting{}, - &models.Log{}, - &models.Software{}, - &models.SoftwareURL{}, - &models.Webhook{}, - ); err != nil { - return nil, fmt.Errorf("can't migrate database: %w", err) - } - - return database, nil -} diff --git a/internal/database/sqlite_database.go b/internal/database/sqlite_database.go deleted file mode 100644 index 5276101..0000000 --- a/internal/database/sqlite_database.go +++ /dev/null @@ -1,36 +0,0 @@ -package database - -import ( - "fmt" - - "github.com/italia/developers-italia-api/internal/models" - "gorm.io/driver/sqlite" - "gorm.io/gorm" -) - -type SQLiteDB struct { - dsn string -} - -func (d *SQLiteDB) Init(dsn string) (*gorm.DB, error) { - var err error - - database, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) - if err != nil { - return nil, fmt.Errorf("can't open database: %w", err) - } - - if err = database.AutoMigrate( - &models.Publisher{}, - &models.Event{}, - &models.CodeHosting{}, - &models.Log{}, - &models.Software{}, - &models.SoftwareURL{}, - &models.Webhook{}, - ); err != nil { - return nil, fmt.Errorf("can't migrate database: %w", err) - } - - return database, nil -} diff --git a/internal/handlers/general/pagination.go b/internal/handlers/general/pagination.go index 38cb988..b2a48d2 100644 --- a/internal/handlers/general/pagination.go +++ b/internal/handlers/general/pagination.go @@ -3,7 +3,6 @@ package general import ( "encoding/json" "fmt" - "strconv" "github.com/gofiber/fiber/v2" "github.com/pilagod/gorm-cursor-paginator/v2/paginator" @@ -48,13 +47,10 @@ func NewPaginatorWithConfig(ctx *fiber.Ctx, config *paginator.Config) *paginator paginator.SetBeforeCursor(before) } - if size := ctx.Query("page[size]"); size != "" { - //nolint:godox // need to implement this in the future - // TODO: make the API return the error if limit is not an integer - if limit, err := strconv.Atoi(size); err == nil { - paginator.SetLimit(limit) - } - } + //nolint:godox // need to implement this in the future + // TODO: make the API return the error if limit is not an integer + size := ctx.QueryInt("page[size]", DefaultLimitCount) + paginator.SetLimit(size) return paginator } diff --git a/internal/handlers/logs.go b/internal/handlers/logs.go index 295ca75..181e6b0 100644 --- a/internal/handlers/logs.go +++ b/internal/handlers/logs.go @@ -88,20 +88,18 @@ func (p *Log) GetLog(ctx *fiber.Ctx) error { // PostLog creates a new log. func (p *Log) PostLog(ctx *fiber.Ctx) error { - logReq := new(common.Log) + const errMsg = "can't create Log" - if err := ctx.BodyParser(&logReq); err != nil { - return common.Error(fiber.StatusBadRequest, "can't create Log", "invalid json") - } + logReq := new(common.Log) - if err := common.ValidateStruct(*logReq); err != nil { - return common.ErrorWithValidationErrors(fiber.StatusUnprocessableEntity, "can't create Log", "invalid format", err) + if err := common.ValidateRequestEntity(ctx, logReq, errMsg); err != nil { + return err //nolint:wrapcheck } log := models.Log{ID: utils.UUIDv4(), Message: logReq.Message} if err := p.db.Create(&log).Error; err != nil { - return common.Error(fiber.StatusInternalServerError, "can't create Log", "db error") + return common.Error(fiber.StatusInternalServerError, errMsg, "db error") } return ctx.JSON(&log) @@ -109,34 +107,28 @@ func (p *Log) PostLog(ctx *fiber.Ctx) error { // PatchLog updates the log with the given ID. func (p *Log) PatchLog(ctx *fiber.Ctx) error { - logReq := new(common.Log) + const errMsg = "can't update Log" - if err := ctx.BodyParser(logReq); err != nil { - return common.Error(fiber.StatusBadRequest, "can't update Log", "invalid json") - } + logReq := new(common.Log) - if err := common.ValidateStruct(*logReq); err != nil { - return common.ErrorWithValidationErrors(fiber.StatusUnprocessableEntity, "can't update Log", "invalid format", err) + if err := common.ValidateRequestEntity(ctx, logReq, errMsg); err != nil { + return err //nolint:wrapcheck } log := models.Log{} if err := p.db.First(&log, "id = ?", ctx.Params("id")).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return common.Error(fiber.StatusNotFound, "can't update Log", "Log was not found") + return common.Error(fiber.StatusNotFound, errMsg, "Log was not found") } - return common.Error( - fiber.StatusInternalServerError, - "can't update Log", - fiber.ErrInternalServerError.Message, - ) + return common.Error(fiber.StatusInternalServerError, errMsg, fiber.ErrInternalServerError.Message) } log.Message = logReq.Message if err := p.db.Updates(&log).Error; err != nil { - return common.Error(fiber.StatusInternalServerError, "can't update Log", "db error") + return common.Error(fiber.StatusInternalServerError, errMsg, "db error") } return ctx.JSON(&log) @@ -206,6 +198,8 @@ func (p *Log) GetSoftwareLogs(ctx *fiber.Ctx) error { // PostSoftwareLog creates a new log associated to a Software with the given ID and returns any error encountered. func (p *Log) PostSoftwareLog(ctx *fiber.Ctx) error { + const errMsg = "can't create Log" + logReq := new(common.Log) software := models.Software{} @@ -221,23 +215,21 @@ func (p *Log) PostSoftwareLog(ctx *fiber.Ctx) error { ) } - if err := ctx.BodyParser(&logReq); err != nil { - return common.Error(fiber.StatusBadRequest, "can't create Log", "invalid json") + if err := common.ValidateRequestEntity(ctx, logReq, errMsg); err != nil { + return err //nolint:wrapcheck } - if err := common.ValidateStruct(*logReq); err != nil { - return common.ErrorWithValidationErrors(fiber.StatusUnprocessableEntity, "can't create Log", "invalid format", err) - } + table := models.Software{}.TableName() log := models.Log{ ID: utils.UUIDv4(), Message: logReq.Message, - EntityID: software.ID, - EntityType: models.Software{}.TableName(), + EntityID: &software.ID, + EntityType: &table, } if err := p.db.Create(&log).Error; err != nil { - return common.Error(fiber.StatusInternalServerError, "can't create Log", "db error") + return common.Error(fiber.StatusInternalServerError, errMsg, "db error") } return ctx.JSON(&log) diff --git a/internal/handlers/publishers.go b/internal/handlers/publishers.go index e551db5..5ad2e44 100644 --- a/internal/handlers/publishers.go +++ b/internal/handlers/publishers.go @@ -4,8 +4,9 @@ import ( "errors" "fmt" "net/url" + "sort" - "github.com/italia/developers-italia-api/internal/database" + "golang.org/x/exp/slices" "github.com/italia/developers-italia-api/internal/handlers/general" @@ -42,7 +43,16 @@ func (p *Publisher) GetPublishers(ctx *fiber.Ctx) error { stmt := p.db.Preload("CodeHosting") - if all := ctx.Query("all", ""); all == "" { + stmt, err := general.Clauses(ctx, stmt, "") + if err != nil { + return common.Error( + fiber.StatusUnprocessableEntity, + "can't get Publishers", + err.Error(), + ) + } + + if all := ctx.QueryBool("all", false); !all { stmt = stmt.Scopes(models.Active) } @@ -71,8 +81,9 @@ func (p *Publisher) GetPublishers(ctx *fiber.Ctx) error { // GetPublisher gets the publisher with the given ID and returns any error encountered. func (p *Publisher) GetPublisher(ctx *fiber.Ctx) error { publisher := models.Publisher{} + id := ctx.Params("id") - if err := p.db.Preload("CodeHosting").First(&publisher, "id = ?", ctx.Params("id")).Error; err != nil { + if err := p.db.Preload("CodeHosting").First(&publisher, "id = ? or alternative_id = ?", id, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return common.Error(fiber.StatusNotFound, "can't get Publisher", "Publisher was not found") } @@ -96,6 +107,23 @@ func (p *Publisher) PostPublisher(ctx *fiber.Ctx) error { return err //nolint:wrapcheck } + if request.AlternativeID != nil { + //nolint:godox // postpone the fix + // FIXME: Possible TOCTTOU race here + result := p.db.Limit(1).Find(&models.Publisher{ID: *request.AlternativeID}) + + if result.Error != nil { + return common.Error(fiber.StatusInternalServerError, "can't create Publisher", "db error") + } + + if result.RowsAffected != 0 { + return common.Error(fiber.StatusConflict, + "can't create Publisher", + fmt.Sprintf("Publisher with id '%s' already exists", *request.AlternativeID), + ) + } + } + normalizedEmail := common.NormalizeEmail(request.Email) publisher := &models.Publisher{ @@ -119,15 +147,15 @@ func (p *Publisher) PostPublisher(ctx *fiber.Ctx) error { } if err := p.db.Create(&publisher).Error; err != nil { - switch database.WrapErrors(err) { //nolint:errorlint - case common.ErrDBRecordNotFound: + switch { + case errors.Is(err, gorm.ErrRecordNotFound): return common.Error(fiber.StatusNotFound, "can't create Publisher", "Publisher was not found") - case common.ErrDBUniqueConstraint: + case errors.Is(err, gorm.ErrDuplicatedKey): return common.Error(fiber.StatusConflict, "can't create Publisher", - "Publisher with provided description, email, alternativeId or CodeHosting URL already exists") + "description, alternativeId or codeHosting URL already exists") default: return common.Error(fiber.StatusInternalServerError, "can't create Publisher", @@ -139,74 +167,107 @@ func (p *Publisher) PostPublisher(ctx *fiber.Ctx) error { } // PatchPublisher updates the publisher with the given ID. CodeHosting URLs will be overwritten from the request. -func (p *Publisher) PatchPublisher(ctx *fiber.Ctx) error { - requests := new(common.PublisherPatch) +func (p *Publisher) PatchPublisher(ctx *fiber.Ctx) error { //nolint:cyclop,funlen // mostly error handling ifs + publisherReq := new(common.PublisherPatch) + publisher := models.Publisher{} + id := ctx.Params("id") - if err := common.ValidateRequestEntity(ctx, requests, "can't update Publisher"); err != nil { - return err //nolint:wrapcheck - } + // Preload will load all the associated CodeHosting. We'll manually handle that later. + if err := p.db.Preload("CodeHosting").First(&publisher, "id = ? or alternative_id = ?", id, id). + Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return common.Error(fiber.StatusNotFound, "can't update Publisher", "Publisher was not found") + } - publisher := models.Publisher{} + return common.Error( + fiber.StatusInternalServerError, + "can't update Publisher", + fiber.ErrInternalServerError.Message, + ) + } - if err := p.db.Transaction(func(gormTrx *gorm.DB) error { - return p.updatePublisherTrx(gormTrx, publisher, ctx, requests) - }); err != nil { + if err := common.ValidateRequestEntity(ctx, publisherReq, "can't update Publisher"); err != nil { return err //nolint:wrapcheck } - return ctx.JSON(&publisher) -} + if publisherReq.AlternativeID != nil { + //nolint:godox // postpone the fix + // FIXME: Possible TOCTTOU race here + result := p.db.Limit(1).Find(&models.Publisher{ID: *publisherReq.AlternativeID}) -func (p *Publisher) updatePublisherTrx( - gormTrx *gorm.DB, - publisher models.Publisher, - ctx *fiber.Ctx, - request *common.PublisherPatch, -) error { - if err := gormTrx.Model(&models.Publisher{}).Preload("CodeHosting"). - First(&publisher, "id = ?", ctx.Params("id")).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return common.Error(fiber.StatusNotFound, "Not found", "can't update Publisher. Publisher was not found") + if result.Error != nil { + return common.Error(fiber.StatusInternalServerError, "can't update Publisher", "db error") } - return common.Error(fiber.StatusInternalServerError, - "can't update Publisher", - fmt.Errorf("db error: %w", err).Error()) + if result.RowsAffected != 0 { + return common.Error(fiber.StatusConflict, + "can't update Publisher", + fmt.Sprintf("Publisher with id '%s' already exists", *publisherReq.AlternativeID), + ) + } } - if request.Description != "" { - publisher.Description = request.Description - } + // Slice of CodeHosting URLs that we expect in the database after the PATCH + var expectedURLs []string - if request.Email != "" { - normalizedEmail := common.NormalizeEmail(&request.Email) - publisher.Email = normalizedEmail + // application/merge-patch+json semantics: change CodeHosting only if + // the request specifies a "CodeHosting" key. + if publisherReq.CodeHosting != nil { + for _, ch := range *publisherReq.CodeHosting { + expectedURLs = append(expectedURLs, purell.MustNormalizeURLString(ch.URL, normalizeFlags)) + } + } else { + for _, ch := range publisher.CodeHosting { + expectedURLs = append(expectedURLs, ch.URL) + } } - if request.AlternativeID != "" { - publisher.AlternativeID = &request.AlternativeID - } + if err := p.db.Transaction(func(tran *gorm.DB) error { + codeHosting, err := syncCodeHosting(tran, publisher, expectedURLs) + if err != nil { + return err + } + + if publisherReq.Description != nil { + publisher.Description = *publisherReq.Description + } + if publisherReq.Email != nil { + publisher.Email = common.NormalizeEmail(publisherReq.Email) + } + if publisherReq.Active != nil { + publisher.Active = publisherReq.Active + } + if publisher.AlternativeID != nil { + publisher.AlternativeID = publisherReq.AlternativeID + } - if request.CodeHosting != nil && len(request.CodeHosting) > 0 { - gormTrx.Delete(&publisher.CodeHosting) + // Set CodeHosting to a zero value, so it's not touched by gorm's Update(), + // because we handle the alias manually + publisher.CodeHosting = []models.CodeHosting{} - for _, URLAddress := range request.CodeHosting { - publisher.CodeHosting = append(publisher.CodeHosting, models.CodeHosting{ID: utils.UUIDv4(), URL: URLAddress.URL}) + if err := tran.Updates(&publisher).Error; err != nil { + return err } - } - if err := gormTrx.Updates(&publisher).Error; err != nil { - return common.Error(fiber.StatusInternalServerError, - "can't update Publisher", - fmt.Errorf("db error: %w", err).Error()) + publisher.CodeHosting = codeHosting + + return nil + }); err != nil { + return common.Error(fiber.StatusInternalServerError, "can't update Publisher", err.Error()) } - return nil + // Sort the aliases to always have a consistent output + sort.Slice(publisher.CodeHosting, func(a int, b int) bool { + return publisher.CodeHosting[a].URL < publisher.CodeHosting[b].URL + }) + + return ctx.JSON(&publisher) } // DeletePublisher deletes the publisher with the given ID. func (p *Publisher) DeletePublisher(ctx *fiber.Ctx) error { - result := p.db.Select("CodeHosting").Delete(&models.Publisher{ID: ctx.Params("id")}) + id := ctx.Params("id") + result := p.db.Select("CodeHosting").Where("id = ? or alternative_id = ?", id, id).Delete(&models.Publisher{}) if result.Error != nil { return common.Error(fiber.StatusInternalServerError, "can't delete Publisher", "db error") @@ -218,3 +279,59 @@ func (p *Publisher) DeletePublisher(ctx *fiber.Ctx) error { return ctx.SendStatus(fiber.StatusNoContent) } + +// syncCodeHosting synchs the CodeHosting for a `publisher` in the database to reflect the +// passed slice of `codeHosting` URLs. +// +// It returns the slice of CodeHosting in the database. +func syncCodeHosting( //nolint:cyclop // mostly error handling ifs + gormdb *gorm.DB, publisher models.Publisher, codeHosting []string, +) ([]models.CodeHosting, error) { + toRemove := []string{} // Slice of CodeHosting ids to remove from the database + toAdd := []models.CodeHosting{} // Slice of CodeHosting to add to the database + + // Map mirroring the state of CodeHosting for this software in the database, + // keyed by url + urlMap := map[string]models.CodeHosting{} + + for _, ch := range publisher.CodeHosting { + urlMap[ch.URL] = ch + } + + for url, ch := range urlMap { + if !slices.Contains(codeHosting, url) { + toRemove = append(toRemove, ch.ID) + + delete(urlMap, url) + } + } + + for _, url := range codeHosting { + _, exists := urlMap[url] + if !exists { + ch := models.CodeHosting{ID: utils.UUIDv4(), URL: url, PublisherID: publisher.ID} + + toAdd = append(toAdd, ch) + urlMap[url] = ch + } + } + + if len(toRemove) > 0 { + if err := gormdb.Delete(&models.CodeHosting{}, toRemove).Error; err != nil { + return nil, err + } + } + + if len(toAdd) > 0 { + if err := gormdb.Create(toAdd).Error; err != nil { + return nil, err + } + } + + retCodeHosting := make([]models.CodeHosting, 0, len(urlMap)) + for _, ch := range urlMap { + retCodeHosting = append(retCodeHosting, ch) + } + + return retCodeHosting, nil +} diff --git a/internal/handlers/software.go b/internal/handlers/software.go index d94f782..b71dc31 100644 --- a/internal/handlers/software.go +++ b/internal/handlers/software.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/json" "errors" "sort" @@ -12,6 +13,8 @@ import ( "github.com/italia/developers-italia-api/internal/handlers/general" "github.com/italia/developers-italia-api/internal/models" "gorm.io/gorm" + + jsonpatch "github.com/evanphx/json-patch/v5" ) type SoftwareInterface interface { @@ -26,6 +29,11 @@ type Software struct { db *gorm.DB } +var ( + errLoadNotFound = errors.New("Software was not found") + errLoad = errors.New("error while loading Software") +) + func NewSoftware(db *gorm.DB) *Software { return &Software{db: db} } @@ -63,7 +71,7 @@ func (p *Software) GetAllSoftware(ctx *fiber.Ctx) error { //nolint:cyclop // mos stmt.Where("id = ?", softwareURL.SoftwareID) } else { - if all := ctx.Query("all", ""); all == "" { + if all := ctx.QueryBool("all", false); !all { stmt = stmt.Scopes(models.Active) } } @@ -112,36 +120,16 @@ func (p *Software) GetAllSoftware(ctx *fiber.Ctx) error { //nolint:cyclop // mos // GetSoftware gets the software with the given ID and returns any error encountered. func (p *Software) GetSoftware(ctx *fiber.Ctx) error { + const errMsg = "can't get Software" + software := models.Software{} - if err := p.db.First(&software, "id = ?", ctx.Params("id")).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return common.Error(fiber.StatusNotFound, "can't get Software", "Software was not found") + if err := loadSoftware(p.db, &software, ctx.Params("id")); err != nil { + if errors.Is(err, errLoadNotFound) { + return common.Error(fiber.StatusNotFound, errMsg, "Software was not found") } - return common.Error( - fiber.StatusInternalServerError, - "can't get Software", - fiber.ErrInternalServerError.Message, - ) - } - - if err := p.db. - Where("software_id = ? AND id <> ?", software.ID, software.SoftwareURLID).Find(&software.Aliases). - Error; err != nil { - return common.Error( - fiber.StatusInternalServerError, - "can't get Software", - fiber.ErrInternalServerError.Message, - ) - } - - if err := p.db.Where("id = ?", software.SoftwareURLID).First(&software.URL).Error; err != nil { - return common.Error( - fiber.StatusInternalServerError, - "can't get Software", - fiber.ErrInternalServerError.Message, - ) + return common.InternalServerError(errMsg) } return ctx.JSON(&software) @@ -149,16 +137,12 @@ func (p *Software) GetSoftware(ctx *fiber.Ctx) error { // PostSoftware creates a new software. func (p *Software) PostSoftware(ctx *fiber.Ctx) error { - softwareReq := new(common.SoftwarePost) + const errMsg = "can't create Software" - if err := ctx.BodyParser(&softwareReq); err != nil { - return common.Error(fiber.StatusBadRequest, "can't create Software", "invalid json") - } + softwareReq := new(common.SoftwarePost) - if err := common.ValidateStruct(*softwareReq); err != nil { - return common.ErrorWithValidationErrors( - fiber.StatusUnprocessableEntity, "can't create Software", "invalid format", err, - ) + if err := common.ValidateRequestEntity(ctx, softwareReq, errMsg); err != nil { + return err //nolint:wrapcheck } aliases := []models.SoftwareURL{} @@ -180,98 +164,100 @@ func (p *Software) PostSoftware(ctx *fiber.Ctx) error { } if err := p.db.Create(&software).Error; err != nil { - return common.Error(fiber.StatusInternalServerError, "can't create Software", err.Error()) + return common.Error(fiber.StatusInternalServerError, errMsg, err.Error()) } return ctx.JSON(&software) } // PatchSoftware updates the software with the given ID. -func (p *Software) PatchSoftware(ctx *fiber.Ctx) error { //nolint:cyclop // mostly error handling ifs - softwareReq := new(common.SoftwarePatch) +func (p *Software) PatchSoftware(ctx *fiber.Ctx) error { //nolint:funlen,cyclop + const errMsg = "can't update Software" + + softwareReq := common.SoftwarePatch{} software := models.Software{} - // Preload will load all the associated aliases, which include - // also the canonical url. We'll manually handle that later. - if err := p.db.Preload("Aliases").First(&software, "id = ?", ctx.Params("id")). - Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return common.Error(fiber.StatusNotFound, "can't update Software", "Software was not found") + if err := loadSoftware(p.db, &software, ctx.Params("id")); err != nil { + if errors.Is(err, errLoadNotFound) { + return common.Error(fiber.StatusNotFound, errMsg, "Software was not found") } - return common.Error( - fiber.StatusInternalServerError, - "can't update Software", - fiber.ErrInternalServerError.Message, - ) + return common.Error(fiber.StatusInternalServerError, errMsg, fiber.ErrInternalServerError.Message) } - if err := ctx.BodyParser(softwareReq); err != nil { - return common.Error(fiber.StatusBadRequest, "can't update Software", "invalid json") + if err := common.ValidateRequestEntity(ctx, &softwareReq, errMsg); err != nil { + return err //nolint:wrapcheck } - if err := common.ValidateStruct(*softwareReq); err != nil { - return common.ErrorWithValidationErrors( - fiber.StatusUnprocessableEntity, "can't update Software", "invalid format", err, - ) + softwareJSON, err := json.Marshal(&software) + if err != nil { + return common.Error(fiber.StatusInternalServerError, errMsg, err.Error()) } - // Slice of urls that we expect in the database after the PATCH (url + aliases) - var expectedURLs []string - - // application/merge-patch+json semantics: change url only if - // the request specifies an "url" key. - url := software.URL.URL - if softwareReq.URL != "" { - url = softwareReq.URL + updatedJSON, err := jsonpatch.MergePatch(softwareJSON, ctx.Body()) + if err != nil { + return common.Error(fiber.StatusInternalServerError, errMsg, err.Error()) } - // application/merge-patch+json semantics: change aliases only if - // the request specifies an "aliases" key. - if softwareReq.Aliases != nil { - expectedURLs = *softwareReq.Aliases - } else { - for _, alias := range software.Aliases { - expectedURLs = append(expectedURLs, alias.URL) - } + var updatedSoftware models.Software + + err = json.Unmarshal(updatedJSON, &updatedSoftware) + if err != nil { + return common.Error(fiber.StatusInternalServerError, errMsg, err.Error()) } - expectedURLs = append(expectedURLs, url) + // Slice of aliases that we expect to be in the database after the PATCH + expectedAliases := make([]string, 0, len(updatedSoftware.Aliases)) + for _, alias := range updatedSoftware.Aliases { + expectedAliases = append(expectedAliases, alias.URL) + } if err := p.db.Transaction(func(tran *gorm.DB) error { - updatedURL, aliases, err := syncAliases(tran, software, url, expectedURLs) + //nolint:gocritic // it's fine, we want to another slice + currentURLs := append(software.Aliases, software.URL) + + updatedURL, aliases, err := syncAliases( + tran, + software.ID, + currentURLs, + updatedSoftware.URL.URL, + expectedAliases, + ) if err != nil { return err } - software.PubliccodeYml = softwareReq.PubliccodeYml - software.Active = softwareReq.Active - // Manually set the canonical URL via the foreign key because of a limitation in gorm - software.SoftwareURLID = updatedURL.ID - software.URL = *updatedURL + updatedSoftware.SoftwareURLID = updatedURL.ID + updatedSoftware.URL = *updatedURL // Set Aliases to a zero value, so it's not touched by gorm's Update(), // because we handle the alias manually - software.Aliases = []models.SoftwareURL{} + updatedSoftware.Aliases = []models.SoftwareURL{} - if err := tran.Updates(&software).Error; err != nil { + if err := tran.Updates(&updatedSoftware).Error; err != nil { return err } - software.Aliases = aliases + updatedSoftware.Aliases = aliases return nil }); err != nil { - return common.Error(fiber.StatusInternalServerError, "can't update Software", err.Error()) + switch { + case errors.Is(err, gorm.ErrDuplicatedKey): + return common.Error(fiber.StatusConflict, errMsg, "URL already exists") + default: + //nolint:wrapcheck // default to not wrap other errors, the handler will take care of this + return err + } } // Sort the aliases to always have a consistent output - sort.Slice(software.Aliases, func(a int, b int) bool { - return software.Aliases[a].URL < software.Aliases[b].URL + sort.Slice(updatedSoftware.Aliases, func(a int, b int) bool { + return updatedSoftware.Aliases[a].URL < updatedSoftware.Aliases[b].URL }) - return ctx.JSON(&software) + return ctx.JSON(&updatedSoftware) } // DeleteSoftware deletes the software with the given ID. @@ -289,12 +275,38 @@ func (p *Software) DeleteSoftware(ctx *fiber.Ctx) error { return ctx.SendStatus(fiber.StatusNoContent) } -// syncAliases synchs the SoftwareURLs for a `software` in the database to reflect the -// passed list of `expectedURLs` and the canonical `url`. +func loadSoftware(gormdb *gorm.DB, software *models.Software, id string) error { + if err := gormdb.First(&software, "id = ?", id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errLoadNotFound + } + + return errLoad + } + + if err := gormdb. + Where("software_id = ? AND id <> ?", software.ID, software.SoftwareURLID).Find(&software.Aliases). + Error; err != nil { + return errLoad + } + + if err := gormdb.Debug().Where("id = ?", software.SoftwareURLID).First(&software.URL).Error; err != nil { + return errLoad + } + + return nil +} + +// syncAliases synchs the SoftwareURLs for a `Software` in the database to reflect the +// passed list of `expectedAliases` and the canonical `url`. // // It returns the new canonical SoftwareURL and the new slice of aliases or an error if any. func syncAliases( //nolint:cyclop // mostly error handling ifs - gormdb *gorm.DB, software models.Software, canonicalURL string, expectedURLs []string, + gormdb *gorm.DB, + softwareID string, + currentURLs []models.SoftwareURL, + expectedURL string, + expectedAliases []string, ) (*models.SoftwareURL, []models.SoftwareURL, error) { toRemove := []string{} // Slice of SoftwareURL ids to remove from the database toAdd := []models.SoftwareURL{} // Slice of SoftwareURLs to add to the database @@ -303,25 +315,28 @@ func syncAliases( //nolint:cyclop // mostly error handling ifs // keyed by url urlMap := map[string]models.SoftwareURL{} - for _, alias := range software.Aliases { - urlMap[alias.URL] = alias + for _, url := range currentURLs { + urlMap[url.URL] = url } - for url, alias := range urlMap { - if !slices.Contains(expectedURLs, url) { - toRemove = append(toRemove, alias.ID) + //nolint:gocritic // it's fine, we want to another slice + allSoftwareURLs := append(expectedAliases, expectedURL) - delete(urlMap, url) + for urlStr, softwareURL := range urlMap { + if !slices.Contains(allSoftwareURLs, urlStr) { + toRemove = append(toRemove, softwareURL.ID) + + delete(urlMap, urlStr) } } - for _, url := range expectedURLs { - _, exists := urlMap[url] + for _, urlStr := range allSoftwareURLs { + _, exists := urlMap[urlStr] if !exists { - alias := models.SoftwareURL{ID: utils.UUIDv4(), URL: url, SoftwareID: software.ID} + su := models.SoftwareURL{ID: utils.UUIDv4(), URL: urlStr, SoftwareID: softwareID} - toAdd = append(toAdd, alias) - urlMap[url] = alias + toAdd = append(toAdd, su) + urlMap[urlStr] = su } } @@ -337,17 +352,16 @@ func syncAliases( //nolint:cyclop // mostly error handling ifs } } - updatedCanonicalURL := urlMap[canonicalURL] + updatedURL := urlMap[expectedURL] - // Remove the canonical URL from the aliases, because it need to be its own - // field. It was loaded previously together with the other aliases in Preload(), - // because of limitation in gorm. - delete(urlMap, canonicalURL) + // Remove the canonical URL from the rest of the URLs, so we can return + // URL and aliases in different fields. + delete(urlMap, expectedURL) aliases := make([]models.SoftwareURL, 0, len(urlMap)) for _, alias := range urlMap { aliases = append(aliases, alias) } - return &updatedCanonicalURL, aliases, nil + return &updatedURL, aliases, nil } diff --git a/internal/handlers/webhooks.go b/internal/handlers/webhooks.go index 9d08358..f88d325 100644 --- a/internal/handlers/webhooks.go +++ b/internal/handlers/webhooks.go @@ -118,18 +118,14 @@ func (p *Webhook[T]) GetSingleResourceWebhooks(ctx *fiber.Ctx) error { // PostSingleResourceWebhook creates a new webhook associated to resources // (fe. Software, Publishers) and returns any error encountered. func (p *Webhook[T]) PostResourceWebhook(ctx *fiber.Ctx) error { + const errMsg = "can't create Webhook" + webhookReq := new(common.Webhook) var resource T - if err := ctx.BodyParser(&webhookReq); err != nil { - return common.Error(fiber.StatusBadRequest, "can't create Webhook", "invalid json") - } - - if err := common.ValidateStruct(*webhookReq); err != nil { - return common.ErrorWithValidationErrors( - fiber.StatusUnprocessableEntity, "can't create Webhook", "invalid format", err, - ) + if err := common.ValidateRequestEntity(ctx, webhookReq, errMsg); err != nil { + return err //nolint:wrapcheck } webhook := models.Webhook{ @@ -141,7 +137,7 @@ func (p *Webhook[T]) PostResourceWebhook(ctx *fiber.Ctx) error { } if err := p.db.Create(&webhook).Error; err != nil { - return common.Error(fiber.StatusInternalServerError, "can't create Webhook", "db error") + return common.Error(fiber.StatusInternalServerError, errMsg, "db error") } return ctx.JSON(&webhook) @@ -150,6 +146,8 @@ func (p *Webhook[T]) PostResourceWebhook(ctx *fiber.Ctx) error { // PostResourceWebhook creates a new webhook associated to a resource with the given ID // (fe. a specific Software or Publisher) and returns any error encountered. func (p *Webhook[T]) PostSingleResourceWebhook(ctx *fiber.Ctx) error { + const errMsg = "can't create Webhook" + webhookReq := new(common.Webhook) var resource T @@ -166,14 +164,8 @@ func (p *Webhook[T]) PostSingleResourceWebhook(ctx *fiber.Ctx) error { ) } - if err := ctx.BodyParser(&webhookReq); err != nil { - return common.Error(fiber.StatusBadRequest, "can't create Webhook", "invalid json") - } - - if err := common.ValidateStruct(*webhookReq); err != nil { - return common.ErrorWithValidationErrors( - fiber.StatusUnprocessableEntity, "can't create Webhook", "invalid format", err, - ) + if err := common.ValidateRequestEntity(ctx, webhookReq, errMsg); err != nil { + return err //nolint:wrapcheck } webhook := models.Webhook{ @@ -185,7 +177,7 @@ func (p *Webhook[T]) PostSingleResourceWebhook(ctx *fiber.Ctx) error { } if err := p.db.Create(&webhook).Error; err != nil { - return common.Error(fiber.StatusInternalServerError, "can't create Webhook", "db error") + return common.Error(fiber.StatusInternalServerError, errMsg, "db error") } return ctx.JSON(&webhook) @@ -193,28 +185,24 @@ func (p *Webhook[T]) PostSingleResourceWebhook(ctx *fiber.Ctx) error { // PatchWebhook updates the webhook with the given ID. func (p *Webhook[T]) PatchWebhook(ctx *fiber.Ctx) error { - webhookReq := new(common.Webhook) + const errMsg = "can't update Webhook" - if err := ctx.BodyParser(webhookReq); err != nil { - return common.Error(fiber.StatusBadRequest, "can't update Webhook", "invalid json") - } + webhookReq := new(common.Webhook) - if err := common.ValidateStruct(*webhookReq); err != nil { - return common.ErrorWithValidationErrors( - fiber.StatusUnprocessableEntity, "can't update Webhook", "invalid format", err, - ) + if err := common.ValidateRequestEntity(ctx, webhookReq, errMsg); err != nil { + return err //nolint:wrapcheck } webhook := models.Webhook{} if err := p.db.First(&webhook, "id = ?", ctx.Params("id")).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return common.Error(fiber.StatusNotFound, "can't update Webhook", "Webhook was not found") + return common.Error(fiber.StatusNotFound, errMsg, "Webhook was not found") } return common.Error( fiber.StatusInternalServerError, - "can't update Webhook", + errMsg, fiber.ErrInternalServerError.Message, ) } @@ -222,7 +210,7 @@ func (p *Webhook[T]) PatchWebhook(ctx *fiber.Ctx) error { webhook.URL = webhookReq.URL if err := p.db.Updates(&webhook).Error; err != nil { - return common.Error(fiber.StatusInternalServerError, "can't update Webhook", "db error") + return common.Error(fiber.StatusInternalServerError, errMsg, "db error") } return ctx.JSON(&webhook) diff --git a/internal/jsondecoder/jsondecoder.go b/internal/jsondecoder/jsondecoder.go new file mode 100644 index 0000000..1744a2d --- /dev/null +++ b/internal/jsondecoder/jsondecoder.go @@ -0,0 +1,43 @@ +package jsondecoder + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +var ( + ErrExtraDataAfterDecoding = errors.New("extra data after decoding") + ErrUnknownField = errors.New("unknown field in JSON input") +) + +// UnmarshalDisallowUnknownFieldsUnmarshal parses the JSON-encoded data +// and stores the result in the value pointed to by v like json.Unmarshal, +// but with DisallowUnknownFields() set by default for extra security. +func UnmarshalDisallowUnknownFields(data []byte, v interface{}) error { + dec := json.NewDecoder(bytes.NewReader(data)) + dec.DisallowUnknownFields() + + if err := dec.Decode(v); err != nil { + // Ugly, but the encoding/json uses a dynamic error here + if strings.HasPrefix(err.Error(), "json: unknown field ") { + return ErrUnknownField + } + + // we want to provide an alternative implementation, with the + // unwrapped errors + //nolint:wrapcheck + return err + } + + // Check if there's any data left in the decoder's buffer. + // This ensures that there's no extra JSON after the main object + // otherwise something like '{"foo": 1}{"bar": 2}' or even '{}garbage' + // will not error out. + if dec.More() { + return ErrExtraDataAfterDecoding + } + + return nil +} diff --git a/internal/models/models.go b/internal/models/models.go index 4d50e4d..a2185db 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -1,6 +1,7 @@ package models import ( + "encoding/json" "fmt" "time" @@ -25,20 +26,20 @@ type Log struct { DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` // Entity this Log entry is about (fe. Publisher, Software, etc.) - EntityID string `json:"-"` - EntityType string `json:"-"` + EntityID *string `json:"-"` + EntityType *string `json:"-"` + Entity string `json:"entity,omitempty" gorm:"->;type:text GENERATED ALWAYS AS (CASE WHEN entity_id IS NULL THEN NULL ELSE ('/' || entity_type || '/' || entity_id) END) STORED;default:(-);"` //nolint:lll } type Publisher struct { - ID string `json:"id" gorm:"primaryKey"` - Email *string `json:"email,omitempty"` - Description string `json:"description" gorm:"uniqueIndex;not null"` - CodeHosting []CodeHosting `json:"codeHosting" gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;unique"` - Active *bool `json:"active" gorm:"default:true;not null"` - AlternativeID *string `json:"alternativeId,omitempty" gorm:"uniqueIndex"` - CreatedAt time.Time `json:"createdAt" gorm:"index"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + ID string `json:"id" gorm:"primaryKey"` + Email *string `json:"email,omitempty"` + Description string `json:"description" gorm:"uniqueIndex;not null"` + CodeHosting []CodeHosting `json:"codeHosting" gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;unique"` + Active *bool `json:"active" gorm:"default:true;not null"` + AlternativeID *string `json:"alternativeId,omitempty" gorm:"uniqueIndex"` + CreatedAt time.Time `json:"createdAt" gorm:"index"` + UpdatedAt time.Time `json:"updatedAt"` } func (Publisher) TableName() string { @@ -54,13 +55,12 @@ func (CodeHosting) TableName() string { } type CodeHosting struct { - ID string `json:"-" gorm:"primaryKey"` - URL string `json:"url" gorm:"not null;uniqueIndex"` - Group *bool `json:"group" gorm:"default:true;not null"` - PublisherID string `json:"-"` - CreatedAt time.Time `json:"createdAt" gorm:"index"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + ID string `json:"-" gorm:"primaryKey"` + URL string `json:"url" gorm:"not null;uniqueIndex"` + Group *bool `json:"group" gorm:"default:true;not null"` + PublisherID string `json:"-"` + CreatedAt time.Time `json:"createdAt" gorm:"index"` + UpdatedAt time.Time `json:"updatedAt"` } type Software struct { @@ -89,25 +89,30 @@ func (s Software) UUID() string { return s.ID } +//nolint:musttag // we are using a custom MarshalJSON method type SoftwareURL struct { ID string `gorm:"primarykey"` URL string `gorm:"uniqueIndex"` SoftwareID string `gorm:"not null"` - CreatedAt time.Time `json:"createdAt" gorm:"index"` - UpdatedAt time.Time `json:"updatedAt"` + CreatedAt time.Time `gorm:"index"` + UpdatedAt time.Time } func (su SoftwareURL) MarshalJSON() ([]byte, error) { return ([]byte)(fmt.Sprintf(`"%s"`, su.URL)), nil } +func (su *SoftwareURL) UnmarshalJSON(data []byte) error { + //nolint:wrapcheck // we want to pass along the error here + return json.Unmarshal(data, &su.URL) +} + type Webhook struct { - ID string `json:"id" gorm:"primaryKey"` - URL string `json:"url" gorm:"index:idx_webhook_url,unique"` - Secret string `json:"-"` - CreatedAt time.Time `json:"createdAt" gorm:"index"` - UpdatedAt time.Time `json:"updatedAt"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + ID string `json:"id" gorm:"primaryKey"` + URL string `json:"url" gorm:"index:idx_webhook_url,unique"` + Secret string `json:"-"` + CreatedAt time.Time `json:"createdAt" gorm:"index"` + UpdatedAt time.Time `json:"updatedAt"` // Entity this Webhook is for (fe. Publisher, Software, etc.) EntityID string `json:"-" gorm:"index:idx_webhook_url,unique"` diff --git a/internal/models/models_test.go b/internal/models/models_test.go index 9359288..2b91b2b 100644 --- a/internal/models/models_test.go +++ b/internal/models/models_test.go @@ -37,7 +37,7 @@ func init() { // TODO: investigate the root cause sqlitedb.Exec("PRAGMA journal_mode=WAL;") - db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{TranslateError: true}) if err != nil { log.Fatal(err) } @@ -93,7 +93,7 @@ func TestSoftwareCreate(t *testing.T) { PubliccodeYml: "-", }, ).Error - assert.EqualError(t, err, "UNIQUE constraint failed: software.software_url_id") + assert.ErrorIs(t, err, gorm.ErrDuplicatedKey) } func TestSoftwareURLCreate(t *testing.T) { @@ -117,7 +117,7 @@ func TestSoftwareURLCreate(t *testing.T) { URL: "https://new-2.example.org", }, ).Error - assert.EqualError(t, err, "UNIQUE constraint failed: software_urls.id") + assert.ErrorIs(t, err, gorm.ErrDuplicatedKey) } func TestPublisherCreate(t *testing.T) { @@ -144,7 +144,7 @@ func TestPublisherCreate(t *testing.T) { Email: &email, }, ).Error - assert.EqualError(t, err, "UNIQUE constraint failed: publishers.description") + assert.ErrorIs(t, err, gorm.ErrDuplicatedKey) // Duplicate alternativeId alternativeID := "alternative-id-12345" @@ -155,7 +155,7 @@ func TestPublisherCreate(t *testing.T) { AlternativeID: &alternativeID, }, ).Error - assert.EqualError(t, err, "UNIQUE constraint failed: publishers.alternative_id") + assert.ErrorIs(t, err, gorm.ErrDuplicatedKey) } func TestWebhookCreate(t *testing.T) { @@ -181,7 +181,7 @@ func TestWebhookCreate(t *testing.T) { URL: "https://new-webhook-2.example.org", }, ).Error - assert.EqualError(t, err, "UNIQUE constraint failed: webhooks.id") + assert.ErrorIs(t, err, gorm.ErrDuplicatedKey) } func TestEventCreate(t *testing.T) { @@ -207,5 +207,5 @@ func TestEventCreate(t *testing.T) { EntityType: "software", }, ).Error - assert.EqualError(t, err, "UNIQUE constraint failed: events.id") + assert.ErrorIs(t, err, gorm.ErrDuplicatedKey) } diff --git a/main.go b/main.go index 7196696..4dad7b6 100644 --- a/main.go +++ b/main.go @@ -2,22 +2,25 @@ package main import ( "log" + "os" "time" "github.com/caarlos0/env/v6" "gorm.io/gorm" "github.com/italia/developers-italia-api/internal/common" + "github.com/italia/developers-italia-api/internal/database" "github.com/italia/developers-italia-api/internal/handlers" + "github.com/italia/developers-italia-api/internal/jsondecoder" "github.com/italia/developers-italia-api/internal/middleware" "github.com/italia/developers-italia-api/internal/models" "github.com/italia/developers-italia-api/internal/webhooks" + "github.com/ansrivas/fiberprometheus/v2" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cache" "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/recover" - "github.com/italia/developers-italia-api/internal/database" ) func main() { @@ -32,9 +35,7 @@ func Setup() *fiber.App { panic(err) } - db := database.NewDatabase(common.EnvironmentConfig) - - gormDB, err := db.Init(common.EnvironmentConfig.Database) + gormDB, err := database.NewDatabase(common.EnvironmentConfig.Database) if err != nil { panic(err) } @@ -54,6 +55,9 @@ func Setup() *fiber.App { app := fiber.New(fiber.Config{ ErrorHandler: common.CustomErrorHandler, + // Fiber doesn't set DisallowUnknownFields by default + // (https://github.com/gofiber/fiber/issues/2601) + JSONDecoder: jsondecoder.UnmarshalDisallowUnknownFields, }) // Automatically recover panics in handlers @@ -72,9 +76,10 @@ func Setup() *fiber.App { app.Use(cache.New(cache.Config{ Next: func(ctx *fiber.Ctx) bool { - // Don't cache POST, PUT, PATCH or /status - return ctx.Method() != fiber.MethodGet || ctx.Route().Path == "/v1/status" + // Don't cache /status + return ctx.Route().Path == "/v1/status" }, + Methods: []string{fiber.MethodGet, fiber.MethodHead}, CacheControl: true, Expiration: 10 * time.Second, //nolint:gomnd KeyGenerator: func(ctx *fiber.Ctx) string { @@ -88,6 +93,10 @@ func Setup() *fiber.App { common.EnvironmentConfig.PasetoKey = middleware.NewRandomPasetoKey() } + prometheus := fiberprometheus.New(os.Args[0]) + prometheus.RegisterAt(app, "/metrics") + app.Use(prometheus.Middleware) + app.Use(middleware.NewPasetoMiddleware(common.EnvironmentConfig)) setupHandlers(app, gormDB) @@ -127,16 +136,16 @@ func setupHandlers(app *fiber.App, gormDB *gorm.DB) { v1.Delete("/software/:id", softwareHandler.DeleteSoftware) v1.Get("/logs", logHandler.GetLogs) - v1.Get("/logs/:id", logHandler.GetLog) + v1.Get("/logs/:id", logHandler.GetLog) v1.Post("/logs", logHandler.PostLog) - v1.Patch("/logs/:id", logHandler.PatchLog) - v1.Delete("/logs/:id", logHandler.DeleteLog) + v1.Patch("/logs/:id", logHandler.PatchLog) + v1.Delete("/logs/:id", logHandler.DeleteLog) v1.Get("/software/:id/logs", logHandler.GetSoftwareLogs) v1.Post("/software/:id/logs", logHandler.PostSoftwareLog) v1.Get("/status", statusHandler.GetStatus) - v1.Get("/webhooks/:id", publisherWebhookHandler.GetWebhook) - v1.Patch("/webhooks/:id", publisherWebhookHandler.PatchWebhook) - v1.Delete("/webhooks/:id", publisherWebhookHandler.DeleteWebhook) + v1.Get("/webhooks/:id", publisherWebhookHandler.GetWebhook) + v1.Patch("/webhooks/:id", publisherWebhookHandler.PatchWebhook) + v1.Delete("/webhooks/:id", publisherWebhookHandler.DeleteWebhook) } diff --git a/main_test.go b/main_test.go index d05f9b7..9326610 100644 --- a/main_test.go +++ b/main_test.go @@ -16,6 +16,9 @@ import ( "github.com/go-testfixtures/testfixtures/v3" "github.com/gofiber/fiber/v2" "github.com/stretchr/testify/assert" + + _ "github.com/lib/pq" + _ "github.com/mattn/go-sqlite3" ) const UUID_REGEXP = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" @@ -23,6 +26,7 @@ const UUID_REGEXP = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12 var ( app *fiber.App db *sql.DB + dbDriver string goodToken = "Bearer v2.local.TwwHUQEi8hr2Eo881_Bs5vK9dHOR5BgEU24QRf-U7VmUwI1yOEA6mFT0EsXioMkFT_T-jjrtIJ_Nv8f6hR6ifJXUOuzWEkm9Ijq1mqSjQatD3aDqKMyjjBA" badToken = "Bearer v2.local.UngfrCDNwGUw4pff2oBNoyxYvOErcbVVqLndl6nzONafUCzktaOeMSmoI7B0h62zoxXXLqTm_Phl" ) @@ -31,9 +35,9 @@ type TestCase struct { description string // Test input - query string - body string - headers map[string][]string + query string + body string + headers map[string][]string // Expected output expectedCode int @@ -43,23 +47,36 @@ type TestCase struct { } func init() { - _ = os.Remove("./test.db") + // Test on SQLite by default if DATABASE_DSN is not set + if _, exists := os.LookupEnv("DATABASE_DSN"); !exists { + _ = os.Setenv("DATABASE_DSN", "file:./test.db") + _ = os.Remove("./test.db") + } - _ = os.Setenv("DATABASE_DSN", "file:./test.db") _ = os.Setenv("ENVIRONMENT", "test") // echo -n 'test-paseto-key-dont-use-in-prod' | base64 _ = os.Setenv("PASETO_KEY", "dGVzdC1wYXNldG8ta2V5LWRvbnQtdXNlLWluLXByb2Q=") + dsn := os.Getenv("DATABASE_DSN") + switch { + case strings.HasPrefix(dsn, "postgres:"): + dbDriver = "postgres" + default: + dbDriver = "sqlite3" + } + var err error - db, err = sql.Open("sqlite3", os.Getenv("DATABASE_DSN")) + db, err = sql.Open(dbDriver, dsn) if err != nil { log.Fatal(err) } // This is needed, otherwise we get a database-locked error // TODO: investigate the root cause - _, _ = db.Exec("PRAGMA journal_mode=WAL;") + if dbDriver == "sqlite3" { + _, _ = db.Exec("PRAGMA journal_mode=WAL;") + } // Setup the app as it is done in the main function app = Setup() @@ -74,7 +91,7 @@ func TestMain(m *testing.M) { func loadFixtures(t *testing.T) { fixtures, err := testfixtures.New( testfixtures.Database(db), - testfixtures.Dialect("sqlite"), + testfixtures.Dialect(dbDriver), testfixtures.Directory("test/testdata/fixtures/"), ) assert.Nil(t, err) @@ -366,6 +383,53 @@ func TestPublishersEndpoints(t *testing.T) { assert.Equal(t, "wrong cursor format in page[after] or page[before]", response["detail"]) }, }, + { + description: `GET with "from" query param`, + query: "GET /v1/publishers?from=2018-11-10T00:56:23Z", + + expectedCode: 200, + expectedContentType: "application/json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.IsType(t, []interface{}{}, response["data"]) + data := response["data"].([]interface{}) + + assert.Equal(t, 14, len(data)) + }, + }, + { + description: `GET with invalid "from" query param`, + query: "GET /v1/publishers?from=3", + + expectedCode: 422, + expectedContentType: "application/problem+json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, `can't get Publishers`, response["title"]) + assert.Equal(t, "invalid date time format (RFC 3339 needed)", response["detail"]) + }, + }, + { + description: `GET with "to" query param`, + query: "GET /v1/publishers?to=2018-11-01T09:56:23Z", + + expectedCode: 200, + expectedContentType: "application/json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + data := response["data"].([]interface{}) + + assert.Equal(t, 13, len(data)) + }, + }, + { + description: `GET with invalid "to" query param`, + query: "GET /v1/publishers?to=3", + + expectedCode: 422, + expectedContentType: "application/problem+json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, `can't get Publishers`, response["title"]) + assert.Equal(t, "invalid date time format (RFC 3339 needed)", response["detail"]) + }, + }, // GET /publishers/:id { @@ -399,6 +463,25 @@ func TestPublishersEndpoints(t *testing.T) { } }, }, + { + description: "GET publisher with alternativeId", + query: "GET /v1/publishers/alternative-id-12345", + expectedCode: 200, + expectedContentType: "application/json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, "15fda7c4-6bbf-4387-8f89-258c1e6facb0", response["id"]) + assert.Equal(t, "alternative-id-12345", response["alternativeId"]) + + _, err := time.Parse(time.RFC3339, response["createdAt"].(string)) + assert.Nil(t, err) + _, err = time.Parse(time.RFC3339, response["updatedAt"].(string)) + assert.Nil(t, err) + + for key := range response { + assert.Contains(t, []string{"id", "createdAt", "updatedAt", "codeHosting", "email", "description", "active", "alternativeId"}, key) + } + }, + }, // POST /publishers { @@ -469,27 +552,39 @@ func TestPublishersEndpoints(t *testing.T) { }, { description: "POST publisher with duplicate alternativeId", - query: "POST /v1/publishers", - body: `{"alternativeId": "alternative-id-12345", "description":"new description", "codeHosting": [{"url" : "https://example-testcase-xx3.com"}], "email":"example-testcase-3-pass@example.com"}`, + query: "POST /v1/publishers", + body: `{"alternativeId": "alternative-id-12345", "description":"new description", "codeHosting": [{"url" : "https://example-testcase-xx3.com"}], "email":"example-testcase-3-pass@example.com"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 409, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't create Publisher","detail":"description, alternativeId or codeHosting URL already exists","status":409}`, + }, + { + description: "POST publisher with alternativeId matching an existing id", + query: "POST /v1/publishers", + body: `{"alternativeId": "2ded32eb-c45e-4167-9166-a44e18b8adde", "description":"new description", "codeHosting": [{"url" : "https://example-testcase-xx3.com"}]}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, }, expectedCode: 409, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Publisher","detail":"Publisher with provided description, email, alternativeId or CodeHosting URL already exists","status":409}`, + expectedBody: `{"title":"can't create Publisher","detail":"Publisher with id '2ded32eb-c45e-4167-9166-a44e18b8adde' already exists","status":409}`, }, { description: "POST publisher with empty alternativeId", - query: "POST /v1/publishers", - body: `{"alternativeId": "", "description":"new description", "codeHosting": [{"url" : "https://gitlab.example.com/repo"}]}`, + query: "POST /v1/publishers", + body: `{"alternativeId": "", "description":"new description", "codeHosting": [{"url" : "https://gitlab.example.com/repo"}]}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, }, expectedCode: 422, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Publisher","detail":"invalid format","status":422,"validationErrors":[{"field":"alternativeId","rule":"min"}]}`, + expectedBody: `{"title":"can't create Publisher","detail":"invalid format: alternativeId does not meet its size limits (too short)","status":422,"validationErrors":[{"field":"alternativeId","rule":"min","value":""}]}`, }, { query: "POST /v1/publishers - NOT normalized URL validation passed", @@ -521,14 +616,14 @@ func TestPublishersEndpoints(t *testing.T) { { description: "POST publishers with duplicate URL (when normalized)", query: "POST /v1/publishers", - body: `{"codeHosting": [{"url" : "https://1-a.exAMple.org/code/repo"}], "description":"new description"}`, + body: `{"codeHosting": [{"url" : "https://1-a.exAMple.org/code/repo"}], "description":"new description"}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, }, expectedCode: 409, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Publisher","detail":"Publisher with provided description, email, alternativeId or CodeHosting URL already exists","status":409}`, + expectedBody: `{"title":"can't create Publisher","detail":"description, alternativeId or codeHosting URL already exists","status":409}`, }, { description: "POST new publisher with an existing email", @@ -545,9 +640,9 @@ func TestPublishersEndpoints(t *testing.T) { }, }, { - description: "POST new publisher with an existing email (not normalized)", - query: "POST /v1/publishers", - body: `{"codeHosting": [{"url" : "https://new-url.example.com"}], "email":"FoobaR@1.example.org", "description": "new publisher description"}`, + description: "POST new publisher with an existing email (not normalized)", + query: "POST /v1/publishers", + body: `{"codeHosting": [{"url" : "https://new-url.example.com"}], "email":"FoobaR@1.example.org", "description": "new publisher description"}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, @@ -559,9 +654,9 @@ func TestPublishersEndpoints(t *testing.T) { }, }, { - description: "POST new publisher with no email", - query: "POST /v1/publishers", - body: `{"codeHosting": [{"url" : "https://new-url.example.com"}], "description": "new publisher description"}`, + description: "POST new publisher with no email", + query: "POST /v1/publishers", + body: `{"codeHosting": [{"url" : "https://new-url.example.com"}], "description": "new publisher description"}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, @@ -576,27 +671,27 @@ func TestPublishersEndpoints(t *testing.T) { }, }, { - description: "POST new publisher with empty email", - query: "POST /v1/publishers", - body: `{"email": "", "codeHosting": [{"url" : "https://new-url.example.com"}], "description": "new publisher description"}`, + description: "POST new publisher with empty email", + query: "POST /v1/publishers", + body: `{"email": "", "codeHosting": [{"url" : "https://new-url.example.com"}], "description": "new publisher description"}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, }, expectedCode: 422, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Publisher","detail":"invalid format","status":422,"validationErrors":[{"field":"email","rule":"email"}]}`, + expectedBody: `{"title":"can't create Publisher","detail":"invalid format: email is not a valid email","status":422,"validationErrors":[{"field":"email","rule":"email","value":""}]}`, }, { - query: "POST /v1/publishers - Description already exist", - body: `{"codeHosting": [{"url" : "https://example-testcase-xx3.com"}], "description": "Publisher description 1"}`, + query: "POST /v1/publishers - Description already exist", + body: `{"codeHosting": [{"url" : "https://example-testcase-xx3.com"}], "description": "Publisher description 1"}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, }, expectedCode: 409, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Publisher","detail":"Publisher with provided description, email, alternativeId or CodeHosting URL already exists","status":409}`, + expectedBody: `{"title":"can't create Publisher","detail":"description, alternativeId or codeHosting URL already exists","status":409}`, }, { description: "POST new publisher with no description", @@ -608,7 +703,7 @@ func TestPublishersEndpoints(t *testing.T) { }, expectedCode: 422, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Publisher","detail":"invalid format","status":422,"validationErrors":[{"field":"description","rule":"required"}]}`, + expectedBody: `{"title":"can't create Publisher","detail":"invalid format: description is required","status":422,"validationErrors":[{"field":"description","rule":"required","value":""}]}`, }, { description: "POST new publisher with empty description", @@ -620,31 +715,31 @@ func TestPublishersEndpoints(t *testing.T) { }, expectedCode: 422, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Publisher","detail":"invalid format","status":422,"validationErrors":[{"field":"description","rule":"required"}]}`, + expectedBody: `{"title":"can't create Publisher","detail":"invalid format: description is required","status":422,"validationErrors":[{"field":"description","rule":"required","value":""}]}`, }, { description: "POST publisher with duplicate alternativeId", - query: "POST /v1/publishers", - body: `{"alternativeId": "alternative-id-12345", "description":"new description", "codeHosting": [{"url" : "https://example-testcase-xx3.com"}]}`, + query: "POST /v1/publishers", + body: `{"alternativeId": "alternative-id-12345", "description":"new description", "codeHosting": [{"url" : "https://example-testcase-xx3.com"}]}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, }, expectedCode: 409, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Publisher","detail":"Publisher with provided description, email, alternativeId or CodeHosting URL already exists","status":409}`, + expectedBody: `{"title":"can't create Publisher","detail":"description, alternativeId or codeHosting URL already exists","status":409}`, }, { description: "POST publishers with invalid payload", query: "POST /v1/publishers", - body: `{"url": "-"}`, + body: `{"description": "-"}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, }, expectedCode: 422, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Publisher","detail":"invalid format","status":422,"validationErrors":[{"field":"codeHosting","rule":"required"},{"field":"description","rule":"required"}]}`, + expectedBody: `{"title":"can't create Publisher","detail":"invalid format: codeHosting is required","status":422,"validationErrors":[{"field":"codeHosting","rule":"required","value":""}]}`, }, { description: "POST publishers - wrong token", @@ -669,7 +764,7 @@ func TestPublishersEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Publisher`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, { @@ -719,7 +814,7 @@ func TestPublishersEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Publisher`, response["title"]) - assert.Equal(t, "invalid format", response["detail"]) + assert.Equal(t, "invalid format: url is invalid, description is required, email is not a valid email", response["detail"]) assert.IsType(t, []interface{}{}, response["validationErrors"]) @@ -745,53 +840,149 @@ func TestPublishersEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Publisher`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, + + // PATCH /publishers/:id { - description: "PATCH non-existing publishers", - query: "PATCH /v1/publishers/NO_SUCH_publishers", - body: `{"codeHosting": [{"url" : "https://www.example.com"}], "email":"example@example.com"}`, + description: "PATCH non existing publisher", + query: "PATCH /v1/publishers/NO_SUCH_PUBLISHER", + body: ``, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, }, expectedCode: 404, - expectedBody: `{"title":"Not found","detail":"can't update Publisher. Publisher was not found","status":404}`, - expectedContentType: "application/problem+json", - }, - //TODO fix database locked test - /* - { - query: "PATCH /v1/publishers/15fda7c4-6bbf-4387-8f89-258c1e6fafb1", - body: `{"codeHosting": [{"url" : "https://www.example.com"}], "email":"example@example.com"}`, - headers: map[string][]string{ - "Authorization": {goodToken}, - "Content-Type": {"application/json"}, - }, - - expectedCode: 200, - expectedContentType: "application/json", - validateFunc: func(t *testing.T, response map[string]interface{}) { - assert.IsType(t, []interface{}{}, response["codeHosting"]) - assert.Equal(t, 3, len(response["codeHosting"].([]interface{}))) - - match, err := regexp.MatchString(UUID_REGEXP, response["id"].(string)) - assert.Nil(t, err) - assert.True(t, match) + expectedBody: `{"title":"can't update Publisher","detail":"Publisher was not found","status":404}`, + expectedContentType: "application/problem+json", + }, + { + description: "PATCH a publisher", + query: "PATCH /v1/publishers/2ded32eb-c45e-4167-9166-a44e18b8adde", + body: `{"description": "new PATCHed description", "codeHosting": [{"url": "https://gitlab.example.org/patched-repo"}]}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 200, + expectedContentType: "application/json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, "new PATCHed description", response["description"]) + assert.IsType(t, []interface{}{}, response["codeHosting"]) - created, err := time.Parse(time.RFC3339, response["createdAt"].(string)) - assert.Nil(t, err) + codeHosting := response["codeHosting"].([]interface{}) + assert.Equal(t, 1, len(codeHosting)) - updated, err := time.Parse(time.RFC3339, response["updatedAt"].(string)) - assert.Nil(t, err) + firstCodeHosting := codeHosting[0].(map[string]interface{}) - assert.Greater(t, updated, created) - }, - },*/ + assert.Equal(t, "https://gitlab.example.org/patched-repo", firstCodeHosting["url"]) + assert.Equal(t, "2ded32eb-c45e-4167-9166-a44e18b8adde", response["id"]) + + created, err := time.Parse(time.RFC3339, response["createdAt"].(string)) + assert.Nil(t, err) + + updated, err := time.Parse(time.RFC3339, response["updatedAt"].(string)) + assert.Nil(t, err) + + assert.Greater(t, updated, created) + }, + }, + { + description: "PATCH publishers with no codeHosting (should leave current codeHosting untouched)", + query: "PATCH /v1/publishers/2ded32eb-c45e-4167-9166-a44e18b8adde", + body: `{"description": "new description"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + + expectedCode: 200, + expectedContentType: "application/json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, "2ded32eb-c45e-4167-9166-a44e18b8adde", response["id"]) + assert.Equal(t, "new description", response["description"]) + assert.Equal(t, "foobar@1.example.org", response["email"]) + + assert.IsType(t, []interface{}{}, response["codeHosting"]) + + codeHosting := response["codeHosting"].([]interface{}) + assert.Equal(t, 2, len(codeHosting)) + + firstCodeHosting := codeHosting[0].(map[string]interface{}) + assert.Equal(t, "https://1-a.example.org/code/repo", firstCodeHosting["url"]) + secondCodeHosting := codeHosting[1].(map[string]interface{}) + assert.Equal(t, "https://1-b.example.org/code/repo", secondCodeHosting["url"]) + + assert.Equal(t, "2018-05-01T00:00:00Z", response["createdAt"]) + created, err := time.Parse(time.RFC3339, response["createdAt"].(string)) + assert.Nil(t, err) + + updated, err := time.Parse(time.RFC3339, response["updatedAt"].(string)) + assert.Nil(t, err) + + assert.Greater(t, updated, created) + }, + }, + { + description: "PATCH publishers with empty codeHosting", + query: "PATCH /v1/publishers/2ded32eb-c45e-4167-9166-a44e18b8adde", + body: `{"description": "new description", "codeHosting": []}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't update Publisher","detail":"invalid format: codeHosting does not meet its size limits (too few items)","status":422,"validationErrors":[{"field":"codeHosting","rule":"gt","value":""}]}`, + }, + { + description: "PATCH a publisher via alternativeId", + query: "PATCH /v1/publishers/alternative-id-12345", + body: `{"description": "new PATCHed description via alternativeId", "codeHosting": [{"url": "https://gitlab.example.org/patched-repo"}]}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 200, + expectedContentType: "application/json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, "new PATCHed description via alternativeId", response["description"]) + assert.IsType(t, []interface{}{}, response["codeHosting"]) + + codeHosting := response["codeHosting"].([]interface{}) + assert.Equal(t, 1, len(codeHosting)) + + firstCodeHosting := codeHosting[0].(map[string]interface{}) + + assert.Equal(t, "https://gitlab.example.org/patched-repo", firstCodeHosting["url"]) + assert.Equal(t, "15fda7c4-6bbf-4387-8f89-258c1e6facb0", response["id"]) + + created, err := time.Parse(time.RFC3339, response["createdAt"].(string)) + assert.Nil(t, err) + + updated, err := time.Parse(time.RFC3339, response["updatedAt"].(string)) + assert.Nil(t, err) + + assert.Greater(t, updated, created) + }, + }, + { + description: "PATCH a publisher with alternativeId matching an existing id", + query: "PATCH /v1/publishers/2ded32eb-c45e-4167-9166-a44e18b8adde", + body: `{"alternativeId": "47807e0c-0613-4aea-9917-5455cc6eddad"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 409, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't update Publisher","detail":"Publisher with id '47807e0c-0613-4aea-9917-5455cc6eddad' already exists","status":409}`, + }, { description: "PATCH publishers - wrong token", - query: "PATCH /v1/publishers/15fda7c4-6bbf-4387-8f89-258c1e6fafb1", + query: "PATCH /v1/publishers/2ded32eb-c45e-4167-9166-a44e18b8adde", body: ``, headers: map[string][]string{ "Authorization": {badToken}, @@ -802,8 +993,8 @@ func TestPublishersEndpoints(t *testing.T) { expectedContentType: "application/problem+json", }, { - description: "PATCH publishers with invalid JSON", - query: "PATCH /v1/publishers/15fda7c4-6bbf-4387-8f89-258c1e6fafb1", + description: "PATCH publisher with invalid JSON", + query: "PATCH /v1/publishers/2ded32eb-c45e-4167-9166-a44e18b8adde", body: `INVALID_JSON`, headers: map[string][]string{ "Authorization": {goodToken}, @@ -813,40 +1004,36 @@ func TestPublishersEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't update Publisher`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) - }, - }, - //TODO fix database locked test - /* - { - description: "PATCH publishers with validation errors", - query: "PATCH /v1/publishers/15fda7c4-6bbf-4387-8f89-258c1e6fafb1", - body: `{"codeHosting": [{"url" : "INVALID_URL"}], "email":"example@example.com"}`, - headers: map[string][]string{ - "Authorization": {goodToken}, - "Content-Type": {"application/json"}, - }, - expectedCode: 422, - expectedContentType: "application/problem+json", - validateFunc: func(t *testing.T, response map[string]interface{}) { - assert.Equal(t, `can't update Publisher`, response["title"]) - assert.Equal(t, "invalid format", response["detail"]) - - assert.IsType(t, []interface{}{}, response["validationErrors"]) - - validationErrors := response["validationErrors"].([]interface{}) - assert.Equal(t, 1, len(validationErrors)) - - firstValidationError := validationErrors[0].(map[string]interface{}) - - for key := range firstValidationError { - assert.Contains(t, []string{"field", "rule", "value"}, key) - } - }, - },*/ + assert.Equal(t, "invalid or malformed JSON", response["detail"]) + }, + }, + { + description: "PATCH publishers with JSON with extra fields", + query: "PATCH /v1/publishers/2ded32eb-c45e-4167-9166-a44e18b8adde", + body: `{"description": "new description", "EXTRA_FIELD": "extra field not in schema"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't update Publisher","detail":"unknown field in JSON input","status":422}`, + }, + { + description: "PATCH publisher with validation errors", + query: "PATCH /v1/publishers/2ded32eb-c45e-4167-9166-a44e18b8adde", + body: `{"description": "new description", "codeHosting": [{"url": "INVALID_URL"}]}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't update Publisher","detail":"invalid format: url is invalid","status":422,"validationErrors":[{"field":"url","rule":"url","value":"INVALID_URL"}]}`, + }, { description: "PATCH publishers with empty body", - query: "PATCH /v1/publishers/15fda7c4-6bbf-4387-8f89-258c1e6fafb1", + query: "PATCH /v1/publishers/2ded32eb-c45e-4167-9166-a44e18b8adde", body: "", headers: map[string][]string{ "Authorization": {goodToken}, @@ -854,11 +1041,17 @@ func TestPublishersEndpoints(t *testing.T) { }, expectedCode: 400, expectedContentType: "application/problem+json", - validateFunc: func(t *testing.T, response map[string]interface{}) { - assert.Equal(t, `can't update Publisher`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) - }, + expectedBody: `{"title":"can't update Publisher","detail":"invalid or malformed JSON","status":400}`, }, + // TODO: enforce this? + // { + // query: "PATCH /v1/publishers with no Content-Type", + // body: "", + // headers: map[string][]string{ + // "Authorization": {goodToken}, + // }, + // expectedCode: 404, + // } // DELETE /publishers/:id { @@ -884,7 +1077,18 @@ func TestPublishersEndpoints(t *testing.T) { expectedContentType: "application/problem+json", }, { - query: "DELETE /v1/publishers/15fda7c4-6bbf-4387-8f89-258c1e6fafb1", + query: "DELETE /v1/publishers/15fda7c4-6bbf-4387-8f89-258c1e6fafb1", + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 204, + expectedBody: "", + expectedContentType: "", + }, + { + description: "DELETE publisher via alternativeId", + query: "DELETE /v1/publishers/alternative-id-12345", headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, @@ -898,7 +1102,7 @@ func TestPublishersEndpoints(t *testing.T) { // GET /publishers/:id/webhooks { - query: "GET /v1/publishers/47807e0c-0613-4aea-9917-5455cc6eddad/webhooks", + query: "GET /v1/publishers/47807e0c-0613-4aea-9917-5455cc6eddad/webhooks", expectedCode: 200, expectedContentType: "application/json", @@ -988,8 +1192,8 @@ func TestPublishersEndpoints(t *testing.T) { }, }, { - query: "POST /v1/publishers/98a069f7-57b0-464d-b300-4b4b336297a0/webhooks", - body: `{"url": "https://new.example.org", "secret": "xyz"}`, + query: "POST /v1/publishers/98a069f7-57b0-464d-b300-4b4b336297a0/webhooks", + body: `{"url": "https://new.example.org", "secret": "xyz"}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, @@ -1040,24 +1244,21 @@ func TestPublishersEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Webhook`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, - // TODO: make this pass - // { - // description: "POST /v1/publishers/98a069f7-57b0-464d-b300-4b4b336297a0/webhooks with JSON with extra fields", - // body: `{"url": "https://new.example.org", EXTRA_FIELD: "extra field not in schema"}`, - // headers: map[string][]string{ - // "Authorization": {goodToken}, - // "Content-Type": {"application/json"}, - // }, - // expectedCode: 422, - // expectedContentType: "application/problem+json", - // validateFunc: func(t *testing.T, response map[string]interface{}) { - // assert.Equal(t, `can't create Webhook`, response["title"]) - // assert.Equal(t, "invalid json", response["detail"]) - // }, - // }, + { + description: "POST /v1/publishers/98a069f7-57b0-464d-b300-4b4b336297a0/webhooks with JSON with extra fields", + query: "POST /v1/publishers/98a069f7-57b0-464d-b300-4b4b336297a0/webhooks", + body: `{"url": "https://new.example.org", "EXTRA_FIELD": "extra field not in schema"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't create Webhook","detail":"unknown field in JSON input","status":422}`, + }, { description: "POST webhook with validation errors", query: "POST /v1/publishers/98a069f7-57b0-464d-b300-4b4b336297a0/webhooks", @@ -1070,7 +1271,7 @@ func TestPublishersEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Webhook`, response["title"]) - assert.Equal(t, "invalid format", response["detail"]) + assert.Equal(t, "invalid format: url is invalid", response["detail"]) assert.IsType(t, []interface{}{}, response["validationErrors"]) @@ -1096,7 +1297,7 @@ func TestPublishersEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Webhook`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, // TODO: enforce this? @@ -1562,7 +1763,7 @@ func TestSoftwareEndpoints(t *testing.T) { }, expectedCode: 422, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Software","detail":"invalid format","status":422,"validationErrors":[{"field":"url","rule":"required"}]}`, + expectedBody: `{"title":"can't create Software","detail":"invalid format: url is required","status":422,"validationErrors":[{"field":"url","rule":"required","value":""}]}`, }, { description: "POST software - wrong token", @@ -1587,25 +1788,21 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Software`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, - // TODO: make this pass - // { - // descrption: "POST /v1/software with JSON with extra fields", - // query: "POST /v1/software", - // body: `{"publiccodeYml": "-", EXTRA_FIELD: "extra field not in schema"}`, - // headers: map[string][]string{ - // "Authorization": {goodToken}, - // "Content-Type": {"application/json"}, - // }, - // expectedCode: 422, - // expectedContentType: "application/problem+json", - // validateFunc: func(t *testing.T, response map[string]interface{}) { - // assert.Equal(t, `can't create Software`, response["title"]) - // assert.Equal(t, "invalid json", response["detail"]) - // }, - // }, + { + description: "POST /v1/software with JSON with extra fields", + query: "POST /v1/software", + body: `{"publiccodeYml": "-", "EXTRA_FIELD": "extra field not in schema"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't create Software","detail":"unknown field in JSON input","status":422}`, + }, { description: "POST software with optional boolean field set to false", query: "POST /v1/software", @@ -1632,7 +1829,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Software`, response["title"]) - assert.Equal(t, "invalid format", response["detail"]) + assert.Equal(t, "invalid format: url is required", response["detail"]) assert.IsType(t, []interface{}{}, response["validationErrors"]) @@ -1658,7 +1855,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Software`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, // TODO: enforce this? @@ -1685,8 +1882,9 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", }, { - query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", - body: `{"publiccodeYml": "publiccodedata", "url": "https://software-new.example.org", "aliases": ["https://software.example.com", "https://software-old.example.org"]}`, + description: "PATCH a software resource", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `{"publiccodeYml": "publiccodedata", "url": "https://software-new.example.org", "aliases": ["https://software.example.com", "https://software-old.example.org"]}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, @@ -1695,6 +1893,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedCode: 200, expectedContentType: "application/json", validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, true, response["active"]) assert.Equal(t, "https://software-new.example.org", response["url"]) assert.IsType(t, []interface{}{}, response["aliases"]) @@ -1731,16 +1930,17 @@ func TestSoftwareEndpoints(t *testing.T) { expectedCode: 200, expectedContentType: "application/json", + expectedBody: "", validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, true, response["active"]) assert.Equal(t, "https://software-new.example.org", response["url"]) assert.IsType(t, []interface{}{}, response["aliases"]) aliases := response["aliases"].([]interface{}) - assert.Equal(t, 2, len(aliases)) + assert.Equal(t, 1, len(aliases)) - assert.Equal(t, "https://18-a.example.org/code/repo", aliases[0]) - assert.Equal(t, "https://18-b.example.org/code/repo", aliases[1]) + assert.Equal(t, "https://18-b.example.org/code/repo", aliases[0]) assert.Equal(t, "publiccodedata", response["publiccodeYml"]) @@ -1769,6 +1969,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedCode: 200, expectedContentType: "application/json", validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, true, response["active"]) assert.Equal(t, "https://software-new.example.org", response["url"]) assert.IsType(t, []interface{}{}, response["aliases"]) @@ -1791,6 +1992,118 @@ func TestSoftwareEndpoints(t *testing.T) { assert.Greater(t, updated, created) }, }, + { + description: "PATCH software with an already existing URL (of another software)", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `{"publiccodeYml": "publiccodedata", "url": "https://21-b.example.org/code/repo"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + + expectedCode: 409, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't update Software","detail":"URL already exists","status":409}`, + }, + { + description: "PATCH software, change active", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `{"active": false}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + + expectedCode: 200, + expectedContentType: "application/json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, false, response["active"]) + assert.Equal(t, "https://18-a.example.org/code/repo", response["url"]) + + assert.IsType(t, []interface{}{}, response["aliases"]) + + aliases := response["aliases"].([]interface{}) + assert.Equal(t, 1, len(aliases)) + + assert.Equal(t, "https://18-b.example.org/code/repo", aliases[0]) + + assert.Equal(t, "-", response["publiccodeYml"]) + + match, err := regexp.MatchString(UUID_REGEXP, response["id"].(string)) + assert.Nil(t, err) + assert.True(t, match) + + created, err := time.Parse(time.RFC3339, response["createdAt"].(string)) + assert.Nil(t, err) + + updated, err := time.Parse(time.RFC3339, response["updatedAt"].(string)) + assert.Nil(t, err) + + assert.Greater(t, updated, created) + }, + }, + { + description: "PATCH software, switch url and alias", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `{"url": "https://18-b.example.org/code/repo", "aliases": ["https://18-a.example.org/code/repo"]}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + + expectedCode: 200, + expectedContentType: "application/json", + validateFunc: func(t *testing.T, response map[string]interface{}) { + assert.Equal(t, "https://18-b.example.org/code/repo", response["url"]) + + assert.IsType(t, []interface{}{}, response["aliases"]) + + aliases := response["aliases"].([]interface{}) + assert.Equal(t, 1, len(aliases)) + + assert.Equal(t, "https://18-a.example.org/code/repo", aliases[0]) + + assert.Equal(t, "-", response["publiccodeYml"]) + + match, err := regexp.MatchString(UUID_REGEXP, response["id"].(string)) + assert.Nil(t, err) + assert.True(t, match) + + created, err := time.Parse(time.RFC3339, response["createdAt"].(string)) + assert.Nil(t, err) + + updated, err := time.Parse(time.RFC3339, response["updatedAt"].(string)) + assert.Nil(t, err) + + assert.Greater(t, updated, created) + }, + }, + { + description: "PATCH software using an already taken URL as url", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `{"url": "https://15-b.example.org/code/repo"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + + expectedCode: 409, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't update Software","detail":"URL already exists","status":409}`, + }, + { + description: "PATCH software using an already taken URL as an alias", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `{"aliases": ["https://16-b.example.org/code/repo"]}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + + expectedCode: 409, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't update Software","detail":"URL already exists","status":409}`, + }, { description: "PATCH software - wrong token", query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", @@ -1815,25 +2128,21 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't update Software`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, - // TODO: make this pass - // { - // description: "PATCH software with JSON with extra fields", - // query: "PATCH /v1/software", - // body: `{"publiccodeYml": "-", EXTRA_FIELD: "extra field not in schema"}`, - // headers: map[string][]string{ - // "Authorization": {goodToken}, - // "Content-Type": {"application/json"}, - // }, - // expectedCode: 422, - // expectedContentType: "application/problem+json", - // validateFunc: func(t *testing.T, response map[string]interface{}) { - // assert.Equal(t, `can't create Software`, response["title"]) - // assert.Equal(t, "invalid json", response["detail"]) - // }, - // }, + { + description: "PATCH software with JSON with extra fields", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `{"publiccodeYml": "-", "EXTRA_FIELD": "extra field not in schema"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't update Software","detail":"unknown field in JSON input","status":422}`, + }, { description: "PATCH software with validation errors", query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", @@ -1846,7 +2155,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't update Software`, response["title"]) - assert.Equal(t, "invalid format", response["detail"]) + assert.Equal(t, "invalid format: url is invalid", response["detail"]) assert.IsType(t, []interface{}{}, response["validationErrors"]) @@ -1860,6 +2169,19 @@ func TestSoftwareEndpoints(t *testing.T) { } }, }, + { + description: "PATCH software with an empty url", + query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", + body: `{"url": ""}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't update Software","detail":"invalid format: url is invalid","status":422,"validationErrors":[{"field":"url","rule":"url","value":""}]}`, + }, { description: "PATCH software with empty body", query: "PATCH /v1/software/59803fb7-8eec-4fe5-a354-8926009c364a", @@ -1872,7 +2194,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't update Software`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, // TODO: enforce this? @@ -1887,8 +2209,8 @@ func TestSoftwareEndpoints(t *testing.T) { // DELETE /software/:id { - description: "Delete non-existent software", - query: "DELETE /v1/software/eea19c82-0449-11ed-bd84-d8bbc146d165", + description: "Delete non-existent software", + query: "DELETE /v1/software/eea19c82-0449-11ed-bd84-d8bbc146d165", headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, @@ -1909,7 +2231,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", }, { - query: "DELETE /v1/software/11e101c4-f989-4cc4-a665-63f9f34e83f6", + query: "DELETE /v1/software/11e101c4-f989-4cc4-a665-63f9f34e83f6", headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, @@ -1922,7 +2244,7 @@ func TestSoftwareEndpoints(t *testing.T) { // GET /software/:id/logs { - query: "GET /v1/software/c353756e-8597-4e46-a99b-7da2e141603b/logs", + query: "GET /v1/software/c353756e-8597-4e46-a99b-7da2e141603b/logs", expectedCode: 200, expectedContentType: "application/json", @@ -1957,6 +2279,8 @@ func TestSoftwareEndpoints(t *testing.T) { _, err = time.Parse(time.RFC3339, log["updatedAt"].(string)) assert.Nil(t, err) + assert.Equal(t, "/software/c353756e-8597-4e46-a99b-7da2e141603b", log["entity"]) + for key := range log { assert.Contains(t, []string{"id", "createdAt", "updatedAt", "message", "entity"}, key) } @@ -1968,8 +2292,6 @@ func TestSoftwareEndpoints(t *testing.T) { prevCreatedAt = &createdAt } - - // TODO assert.NotEmpty(t, firstLog["entity"]) }, }, { @@ -2020,8 +2342,8 @@ func TestSoftwareEndpoints(t *testing.T) { }, }, { - query: "POST /v1/software/c353756e-8597-4e46-a99b-7da2e141603b/logs", - body: `{"message": "New software log from test suite"}`, + query: "POST /v1/software/c353756e-8597-4e46-a99b-7da2e141603b/logs", + body: `{"message": "New software log from test suite"}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, @@ -2041,6 +2363,8 @@ func TestSoftwareEndpoints(t *testing.T) { _, err = time.Parse(time.RFC3339, response["updatedAt"].(string)) assert.Nil(t, err) + assert.Equal(t, "/software/c353756e-8597-4e46-a99b-7da2e141603b", response["entity"]) + // TODO: check the record was actually created in the database }, }, @@ -2068,24 +2392,21 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Log`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, - // TODO: make this pass - // { - // description: "POST /v1/software/c353756e-8597-4e46-a99b-7da2e141603b/logs with JSON with extra fields", - // body: `{"message": "new log", EXTRA_FIELD: "extra field not in schema"}`, - // headers: map[string][]string{ - // "Authorization": {goodToken}, - // "Content-Type": {"application/json"}, - // }, - // expectedCode: 422, - // expectedContentType: "application/problem+json", - // validateFunc: func(t *testing.T, response map[string]interface{}) { - // assert.Equal(t, `can't create Log`, response["title"]) - // assert.Equal(t, "invalid json", response["detail"]) - // }, - // }, + { + description: "POST /v1/software/c353756e-8597-4e46-a99b-7da2e141603b/logs with JSON with extra fields", + query: "POST /v1/software/c353756e-8597-4e46-a99b-7da2e141603b/logs", + body: `{"message": "new log", "EXTRA_FIELD": "extra field not in schema"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't create Log","detail":"unknown field in JSON input","status":422}`, + }, { description: "POST log with validation errors", query: "POST /v1/software/c353756e-8597-4e46-a99b-7da2e141603b/logs", @@ -2098,7 +2419,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Log`, response["title"]) - assert.Equal(t, "invalid format", response["detail"]) + assert.Equal(t, "invalid format: message is required", response["detail"]) assert.IsType(t, []interface{}{}, response["validationErrors"]) @@ -2124,7 +2445,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Log`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, // TODO: enforce this? @@ -2139,7 +2460,7 @@ func TestSoftwareEndpoints(t *testing.T) { // GET /software/:id/webhooks { - query: "GET /v1/software/c5dec6fa-8a01-4881-9e7d-132770d4214d/webhooks", + query: "GET /v1/software/c5dec6fa-8a01-4881-9e7d-132770d4214d/webhooks", expectedCode: 200, expectedContentType: "application/json", @@ -2208,7 +2529,7 @@ func TestSoftwareEndpoints(t *testing.T) { links := response["links"].(map[string]interface{}) assert.Nil(t, links["prev"]) - assert.Equal(t, "?page[after]=WyIwMDAxLTAxLTAxVDAwOjAwOjAwWiIsImU3ZjZkYmRhLWMzZjUtNGIyZi1iM2Q4LTM5YTM0MDI2ZTYwYSJd", links["next"]) + assert.Equal(t, "?page[after]=WyIyMDE3LTA1LTAxVDAwOjAwOjAwWiIsImU3ZjZkYmRhLWMzZjUtNGIyZi1iM2Q4LTM5YTM0MDI2ZTYwYSJd", links["next"]) }, }, @@ -2229,8 +2550,8 @@ func TestSoftwareEndpoints(t *testing.T) { }, }, { - query: "POST /v1/software/c5dec6fa-8a01-4881-9e7d-132770d4214d/webhooks", - body: `{"url": "https://new.example.org", "secret": "xyz"}`, + query: "POST /v1/software/c5dec6fa-8a01-4881-9e7d-132770d4214d/webhooks", + body: `{"url": "https://new.example.org", "secret": "xyz"}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, @@ -2281,24 +2602,21 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Webhook`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, - // TODO: make this pass - // { - // description: "POST /v1/software/c5dec6fa-8a01-4881-9e7d-132770d4214d/webhooks with JSON with extra fields", - // body: `{"url": "https://new.example.org", EXTRA_FIELD: "extra field not in schema"}`, - // headers: map[string][]string{ - // "Authorization": {goodToken}, - // "Content-Type": {"application/json"}, - // }, - // expectedCode: 422, - // expectedContentType: "application/problem+json", - // validateFunc: func(t *testing.T, response map[string]interface{}) { - // assert.Equal(t, `can't create Webhook`, response["title"]) - // assert.Equal(t, "invalid json", response["detail"]) - // }, - // }, + { + description: "POST /v1/software/c5dec6fa-8a01-4881-9e7d-132770d4214d/webhooks with JSON with extra fields", + query: "POST /v1/software/c5dec6fa-8a01-4881-9e7d-132770d4214d/webhooks", + body: `{"url": "https://new.example.org", "EXTRA_FIELD": "extra field not in schema"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't create Webhook","detail":"unknown field in JSON input","status":422}`, + }, { description: "POST webhook with validation errors", query: "POST /v1/software/c5dec6fa-8a01-4881-9e7d-132770d4214d/webhooks", @@ -2311,7 +2629,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Webhook`, response["title"]) - assert.Equal(t, "invalid format", response["detail"]) + assert.Equal(t, "invalid format: url is invalid", response["detail"]) assert.IsType(t, []interface{}{}, response["validationErrors"]) @@ -2337,7 +2655,7 @@ func TestSoftwareEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Webhook`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, // TODO: enforce this? @@ -2389,13 +2707,25 @@ func TestLogsEndpoints(t *testing.T) { _, err = time.Parse(time.RFC3339, log["updatedAt"].(string)) assert.Nil(t, err) + // Only certain logs from the fixtures have an associated entity. + // + // FIXME: This is ugly, see the issue about improving tests: + // https://github.com/italia/developers-italia-api/issues/91 + if log["id"] == "2dfb2bc2-042d-11ed-9338-d8bbc146d165" || + log["id"] == "12f30d9e-042e-11ed-8ddc-d8bbc146d165" || + log["id"] == "18a70362-042e-11ed-b793-d8bbc146d165" { + assert.Equal(t, "/software/c353756e-8597-4e46-a99b-7da2e141603b", log["entity"]) + } else if log["id"] == "53650508-042e-11ed-9b84-d8bbc146d165" { + assert.Equal(t, "/publishers/2ded32eb-c45e-4167-9166-a44e18b8adde", log["entity"]) + } else { + assert.Nil(t, log["entity"]) + } + var prevCreatedAt *time.Time = nil for key := range log { assert.Contains(t, []string{"id", "createdAt", "updatedAt", "message", "entity"}, key) } - // TODO assert.NotEmpty(t, firstLog["entity"]) - // Check the logs are ordered by descending createdAt if prevCreatedAt != nil { assert.GreaterOrEqual(t, *prevCreatedAt, createdAt) @@ -2564,6 +2894,8 @@ func TestLogsEndpoints(t *testing.T) { _, err = time.Parse(time.RFC3339, response["updatedAt"].(string)) assert.Nil(t, err) + assert.Nil(t, response["entity"]) + // TODO: check the record was actually created in the database }, }, @@ -2577,7 +2909,7 @@ func TestLogsEndpoints(t *testing.T) { }, expectedCode: 422, expectedContentType: "application/problem+json", - expectedBody: `{"title":"can't create Log","detail":"invalid format","status":422,"validationErrors":[{"field":"message","rule":"required"}]}`, + expectedBody: `{"title":"can't create Log","detail":"unknown field in JSON input","status":422}`, }, { description: "POST log - wrong token", @@ -2603,24 +2935,21 @@ func TestLogsEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Log`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, - // TODO: make this pass - // { - // query: "POST /v1/logs with JSON with extra fields", - // body: `{"message": "new log", EXTRA_FIELD: "extra field not in schema"}`, - // headers: map[string][]string{ - // "Authorization": {goodToken}, - // "Content-Type": {"application/json"}, - // }, - // expectedCode: 422, - // expectedContentType: "application/problem+json", - // validateFunc: func(t *testing.T, response map[string]interface{}) { - // assert.Equal(t, `can't create Log`, response["title"]) - // assert.Equal(t, "invalid json", response["detail"]) - // }, - // }, + { + description: "POST /v1/logs with JSON with extra fields", + query: "POST /v1/logs", + body: `{"message": "new log", "EXTRA_FIELD": "extra field not in schema"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't create Log","detail":"unknown field in JSON input","status":422}`, + }, { description: "POST log with validation errors", query: "POST /v1/logs", @@ -2633,7 +2962,7 @@ func TestLogsEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Log`, response["title"]) - assert.Equal(t, "invalid format", response["detail"]) + assert.Equal(t, "invalid format: message is required", response["detail"]) assert.IsType(t, []interface{}{}, response["validationErrors"]) @@ -2659,7 +2988,7 @@ func TestLogsEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't create Log`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, // TODO: enforce this? @@ -2696,8 +3025,8 @@ func TestWebhooksEndpoints(t *testing.T) { // PATCH /webhooks/:id { - query: "PATCH /v1/webhooks/007bc84a-7e2d-43a0-b7e1-a256d4114aa7", - body: `{"url": "https://new.example.org/receiver"}`, + query: "PATCH /v1/webhooks/007bc84a-7e2d-43a0-b7e1-a256d4114aa7", + body: `{"url": "https://new.example.org/receiver"}`, headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, @@ -2741,24 +3070,21 @@ func TestWebhooksEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't update Webhook`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, - // TODO: make this pass - // { - // query: "PATCH /v1/webhooks/007bc84a-7e2d-43a0-b7e1-a256d4114aa7 with JSON with extra fields", - // body: `{"url": "https://new.example.org/receiver", EXTRA_FIELD: "extra field not in schema"}`, - // headers: map[string][]string{ - // "Authorization": {goodToken}, - // "Content-Type": {"application/json"}, - // }, - // expectedCode: 422, - // expectedContentType: "application/problem+json", - // validateFunc: func(t *testing.T, response map[string]interface{}) { - // assert.Equal(t, `can't create Webhook`, response["title"]) - // assert.Equal(t, "invalid json", response["detail"]) - // }, - // }, + { + description: "PATCH /v1/webhooks/007bc84a-7e2d-43a0-b7e1-a256d4114aa7 with JSON with extra fields", + query: "PATCH /v1/webhooks/007bc84a-7e2d-43a0-b7e1-a256d4114aa7", + body: `{"url": "https://new.example.org/receiver", "EXTRA_FIELD": "extra field not in schema"}`, + headers: map[string][]string{ + "Authorization": {goodToken}, + "Content-Type": {"application/json"}, + }, + expectedCode: 422, + expectedContentType: "application/problem+json", + expectedBody: `{"title":"can't update Webhook","detail":"unknown field in JSON input","status":422}`, + }, { description: "PATCH webhook with validation errors", query: "PATCH /v1/webhooks/007bc84a-7e2d-43a0-b7e1-a256d4114aa7", @@ -2771,7 +3097,7 @@ func TestWebhooksEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't update Webhook`, response["title"]) - assert.Equal(t, "invalid format", response["detail"]) + assert.Equal(t, "invalid format: url is invalid", response["detail"]) assert.IsType(t, []interface{}{}, response["validationErrors"]) @@ -2797,7 +3123,7 @@ func TestWebhooksEndpoints(t *testing.T) { expectedContentType: "application/problem+json", validateFunc: func(t *testing.T, response map[string]interface{}) { assert.Equal(t, `can't update Webhook`, response["title"]) - assert.Equal(t, "invalid json", response["detail"]) + assert.Equal(t, "invalid or malformed JSON", response["detail"]) }, }, // TODO: enforce this? @@ -2812,14 +3138,17 @@ func TestWebhooksEndpoints(t *testing.T) { // DELETE /webhooks/:id { - description: "Delete non-existent webhook", - query: "DELETE /v1/webhooks/NO_SUCH_WEBHOOK", + description: "Delete non-existent webhook", + query: "DELETE /v1/webhooks/NO_SUCH_WEBHOOK", headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, }, - expectedCode: 404, - expectedBody: `{"title":"can't delete Webhook","detail":"Webhook was not found","status":404}`, + expectedCode: 404, + // This error is different from because it's returned directly from Fiber's + // route constraints, so we don't need to hit the database to find the resource + // because we already know that's not a valid webhook id looking at its format. + expectedBody: `{"title":"Not Found","detail":"Cannot DELETE /v1/webhooks/NO_SUCH_WEBHOOK","status":404}`, expectedContentType: "application/problem+json", }, { @@ -2834,7 +3163,7 @@ func TestWebhooksEndpoints(t *testing.T) { expectedContentType: "application/problem+json", }, { - query: "DELETE /v1/webhooks/24bc1b5d-fe81-47be-9d55-910f820bdd04", + query: "DELETE /v1/webhooks/24bc1b5d-fe81-47be-9d55-910f820bdd04", headers: map[string][]string{ "Authorization": {goodToken}, "Content-Type": {"application/json"}, diff --git a/publiccode.yml b/publiccode.yml new file mode 100644 index 0000000..9ebe916 --- /dev/null +++ b/publiccode.yml @@ -0,0 +1,64 @@ +# This repository adheres to the publiccode.yml standard by including this +# metadata file that makes public software easily discoverable. +# More info at https://github.com/publiccodeyml/publiccode.yml + +publiccodeYmlVersion: '0.3' +categories: + - data-collection +description: + it: + apiDocumentation: 'https://developers.italia.it/it/api/developers-italia' + features: + - RESTful API Design + - Can use either SQLite or PostgreSQL + - Optional rate limiting to manage the number of requests per minute. + - Containerization support + - Helm chart available + longDescription: | + **developers-italia-api** is a RESTful API for the free and open-source + software catalog aimed at Italian public administrations. This API serves + as a crucial tool in managing and accessing the software catalog, + providing a streamlined and efficient way for public administrations and + developers to interact with the repository. It's built with a focus on + ease of use and reliability, ensuring that users can effectively leverage + the catalog for their software needs. + shortDescription: |- + RESTful API for the free and open-source software catalog aimed at Italian + public administrations. This API serves +developmentStatus: beta +it: + conforme: + gdpr: false + lineeGuidaDesign: false + misureMinimeSicurezza: true + modelloInteroperabilita: true + countryExtensionVersion: '0.2' + piattaforme: + anpr: false + cie: false + pagopa: false + spid: false + riuso: + codiceIPA: pcm +legal: + license: AGPL-3.0-or-later +localisation: + availableLanguages: + - en + localisationReady: false +logo: .github/logo.png +maintenance: + contacts: + - name: Fabio Bonelli + type: community +name: developers-italia-api +platforms: + - web +dependsOn: + open: + - name: PostgreSQL + optional: true +releaseDate: '2023-11-04' +softwareType: standalone/backend +softwareVersion: v0.10.1 +url: 'https://github.com/italia/developers-italia-api' diff --git a/test/testdata/fixtures/logs.yml b/test/testdata/fixtures/logs.yml index 64e9486..4572ad1 100644 --- a/test/testdata/fixtures/logs.yml +++ b/test/testdata/fixtures/logs.yml @@ -27,6 +27,8 @@ - id: 53650508-042e-11ed-9b84-d8bbc146d165 created_at: 2010-02-15 23:59:59 updated_at: 2010-12-31 23:59:59 + entity_id: 2ded32eb-c45e-4167-9166-a44e18b8adde + entity_type: publishers message: A log message - id: 55438aac-042e-11ed-848a-d8bbc146d165 diff --git a/test/testdata/fixtures/webhooks.yml b/test/testdata/fixtures/webhooks.yml index 2a2a6ab..8b0eab5 100644 --- a/test/testdata/fixtures/webhooks.yml +++ b/test/testdata/fixtures/webhooks.yml @@ -12,8 +12,8 @@ entity_type: software secret: url: https://3-a.example.org/receiver - created_at: '2017-05-01t00:00:00+00:00' - updated_at: '2017-05-01t00:00:00+00:00' + created_at: '2017-05-01T00:00:00+00:00' + updated_at: '2017-05-01T00:00:00+00:00' - id: d6334000-69a8-43a1-ab43-50bb04e14eed entity_id: 9f135268-a37e-4ead-96ec-e4a24bb9344a entity_type: software