diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index c788100e..00000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to make participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, ethnicity, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies within all project spaces, and it also applies when -an individual is representing the project or its community in public spaces. -Examples of representing a project or community include using an official -project e-mail address, posting via an official social media account, or acting -as an appointed representative at an online or offline event. Representation of -a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at contact+coc@mercure.rocks. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 3df6d044..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: dunglas diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..e993f44f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - + package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + commit-message: + prefix: ci diff --git a/.github/linters/.gitleaks.toml b/.github/linters/.gitleaks.toml new file mode 100644 index 00000000..839c8147 --- /dev/null +++ b/.github/linters/.gitleaks.toml @@ -0,0 +1,13 @@ + +title = "gitleaks config" + +[extend] +# useDefault will extend the base configuration with the default gitleaks config: +# https://github.com/zricethezav/gitleaks/blob/master/config/gitleaks.toml +useDefault = true + +[allowlist] +paths = [ + '''authorization_test.go''', + '''(.*?)(key)''' +] diff --git a/.github/linters/.htmlhintrc b/.github/linters/.htmlhintrc index f613b688..c3f4a5d5 100644 --- a/.github/linters/.htmlhintrc +++ b/.github/linters/.htmlhintrc @@ -4,7 +4,7 @@ "attr-value-double-quotes": true, "attr-value-not-empty": false, "attr-no-duplication": true, - "doctype-first": true, + "doctype-first": false, "tag-pair": true, "tag-self-close": false, "spec-char-escape": true, diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml index ab381c0c..f10f13af 100644 --- a/.github/linters/.markdown-lint.yml +++ b/.github/linters/.markdown-lint.yml @@ -21,9 +21,9 @@ MD004: false # Unordered list style MD007: indent: 2 # Unordered list indentation -MD013: - line_length: 400 # Line length 80 is far too short - tables: false +MD010: + ignore_code_languages: [caddyfile] +MD013: false MD026: punctuation: ".,;:!。,;:" # List of not allowed MD029: false # Ordered list item prefix diff --git a/.github/linters/.textlintrc b/.github/linters/.textlintrc new file mode 100644 index 00000000..bd70fc22 --- /dev/null +++ b/.github/linters/.textlintrc @@ -0,0 +1,13 @@ +{ + "filters": { + "comments": true + }, + "rules": { + "terminology": { + "defaultTerms": true, + "exclude": [ + "(?<=(?:\\w+[^.?!])? )internet\\b(?! explorer)" + ] + } + } +} diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 870ec086..199f35d0 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,12 +1,16 @@ name: Release Hub on: + pull_request: + branches: + - main push: - tags: - - 'v*' branches: - - '*' - pull_request: + - main + tags: + - v*.*.* + workflow_dispatch: + inputs: {} permissions: contents: write diff --git a/.github/workflows/ci-chart.yml b/.github/workflows/ci-chart.yml index 49be48d5..9a16f37c 100644 --- a/.github/workflows/ci-chart.yml +++ b/.github/workflows/ci-chart.yml @@ -1,8 +1,14 @@ name: Lint and Test Chart on: - push: pull_request: + branches: + - main + push: + branches: + - main + tags: + - v*.*.* jobs: lint-test: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 115bb6cc..421770bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,14 @@ name: Lint and Test Hub on: - push: pull_request: + branches: + - main + push: + branches: + - main + tags: + - v*.*.* env: GO111MODULE: 'on' @@ -32,7 +38,7 @@ jobs: strategy: matrix: go: [ '1.20', '1.21' ] - + fail-fast: false name: Test runs-on: ubuntu-latest steps: @@ -54,21 +60,20 @@ jobs: run: go test -race -covermode atomic -coverprofile=profile.cov -coverpkg=github.com/dunglas/mercure ./... - name: Test Caddy module + working-directory: caddy/ run: | go test -timeout 1m -race -covermode atomic -coverprofile=profile.cov -coverpkg=github.com/dunglas/mercure ./... sed '1d' profile.cov >> ../profile.cov - working-directory: ./caddy - name: Upload coverage results uses: shogo82148/actions-goveralls@v1 with: path-to-profile: profile.cov + parallel: true - name: Start Mercure - run: | - cd caddy/mercure - sudo MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' go run main.go start --config ../../Caddyfile.dev - cd - + working-directory: caddy/mercure/ + run: sudo MERCURE_PUBLISHER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' MERCURE_SUBSCRIBER_JWT_KEY='!ChangeThisMercureHubJWTSecretKey!' go run main.go start --config ../../Caddyfile.dev - uses: actions/setup-node@v3 with: @@ -87,3 +92,11 @@ jobs: - name: Run Playwright tests working-directory: conformance-tests/ run: npx playwright test + + finish: + needs: test + runs-on: ubuntu-latest + steps: + - uses: shogo82148/actions-goveralls@v1 + with: + parallel-finished: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 163bb37f..0636a675 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,7 +1,14 @@ name: Lint Docs and Non-Go Code + on: - push: pull_request: + branches: + - main + push: + branches: + - main + tags: + - v*.*.* jobs: lint: @@ -13,8 +20,11 @@ jobs: - name: Lint Code Base uses: super-linter/super-linter/slim@v4 env: - VALIDATE_ALL_CODEBASE: false + VALIDATE_ALL_CODEBASE: true VALIDATE_GO: false + VALIDATE_PHP_PHPCS: false + VALIDATE_KUBERNETES_CONFORM: false VALIDATE_KUBERNETES_KUBEVAL: false + VALIDATE_TYPESCRIPT_STANDARD: false DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.golangci.yml b/.golangci.yml index 9af3fc36..47948b04 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -44,3 +44,4 @@ issues: - godox - noctx - wrapcheck + - goconst diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 00000000..c056d53a --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1 @@ +spec/** diff --git a/.textlintignore b/.textlintignore new file mode 100644 index 00000000..8cc0e7a6 --- /dev/null +++ b/.textlintignore @@ -0,0 +1 @@ +spec/mercure.txt diff --git a/Dockerfile.legacy b/Dockerfile.legacy index e801d543..1911bb6a 100644 --- a/Dockerfile.legacy +++ b/Dockerfile.legacy @@ -1,3 +1,4 @@ +# hadolint ignore=DL3006 FROM gcr.io/distroless/static COPY mercure / CMD ["/mercure"] diff --git a/README.md b/README.md index c1fc3fcc..d2e742cd 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ It is especially useful to publish async and real-time updates of resources serv * [Full documentation](https://mercure.rocks/docs) * [Demo](https://demo.mercure.rocks/) -[The protocol](https://mercure.rocks/spec) is maintained in this repository and is also available as [an Internet Draft](https://datatracker.ietf.org/doc/draft-dunglas-mercure/). +[The protocol](https://mercure.rocks/spec) is maintained in this repository and is also available as [an Internet-Draft](https://datatracker.ietf.org/doc/draft-dunglas-mercure/). A reference, production-grade, implementation of [**a Mercure hub**](https://mercure.rocks/docs/hub/install) (the server) is also available in this repository. It's free software (AGPL) written in Go. It is provided along with a library that can be used in any Go application to implement the Mercure protocol directly (without a hub) and [an official Docker image](https://hub.docker.com/r/dunglas/mercure). @@ -31,7 +31,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md). ## License and Copyright -See https://mercure.rocks/docs/hub/license. +See [license information](https://mercure.rocks/docs/hub/license). ## Credits diff --git a/authorization.go b/authorization.go index bf1a059f..1a6dc595 100644 --- a/authorization.go +++ b/authorization.go @@ -28,6 +28,7 @@ type role int const ( defaultCookieName = "mercureAuthorization" + bearerPrefix = "Bearer " roleSubscriber role = iota rolePublisher ) @@ -54,7 +55,7 @@ var ( func authorize(r *http.Request, jwtConfig *jwtConfig, publishOrigins []string, cookieName string) (*claims, error) { authorizationHeaders, headerExists := r.Header["Authorization"] if headerExists { - if len(authorizationHeaders) != 1 || len(authorizationHeaders[0]) < 48 || authorizationHeaders[0][:7] != "Bearer " { + if len(authorizationHeaders) != 1 || len(authorizationHeaders[0]) < 48 || authorizationHeaders[0][:7] != bearerPrefix { return nil, ErrInvalidAuthorizationHeader } diff --git a/authorization_test.go b/authorization_test.go index 8c3c265c..9fadd88b 100644 --- a/authorization_test.go +++ b/authorization_test.go @@ -29,6 +29,7 @@ bCd7nPuNAyYHCOOHAgMBAAE= -----END PUBLIC KEY----- ` +//nolint:gosec const privateKeyRsa = `-----BEGIN RSA PRIVATE KEY----- MIICWwIBAAKBgHVwuJsFmzsFnOkGj+OgAp4lTNqRCF0RZSmjY+ECWOJ3sSEzQ8qt kJe61uSjr/PKmqvBxxex0YtUL7waSS4jvq3ws8BmWIxK2GqoAVjLjK8HzThSPQpg @@ -70,8 +71,8 @@ func TestAuthorizeMultipleAuthorizationHeader(t *testing.T) { r.Header.Add("Authorization", validEmptyHeader) claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "invalid \"Authorization\" HTTP header") - assert.Nil(t, claims) + require.Error(t, err, `invalid "Authorization" HTTP header`) + require.Nil(t, claims) } func TestAuthorizeMultipleAuthorizationHeaderRsa(t *testing.T) { @@ -80,8 +81,8 @@ func TestAuthorizeMultipleAuthorizationHeaderRsa(t *testing.T) { r.Header.Add("Authorization", validEmptyHeaderRsa) claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodRS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "invalid \"Authorization\" HTTP header") - assert.Nil(t, claims) + require.Error(t, err, `invalid "Authorization" HTTP header`) + require.Nil(t, claims) } func TestAuthorizeAuthorizationHeaderTooShort(t *testing.T) { @@ -89,8 +90,8 @@ func TestAuthorizeAuthorizationHeaderTooShort(t *testing.T) { r.Header.Add("Authorization", "Bearer x") claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "invalid \"Authorization\" HTTP header") - assert.Nil(t, claims) + require.Error(t, err, `invalid "Authorization" HTTP header`) + require.Nil(t, claims) } func TestAuthorizeAuthorizationHeaderNoBearer(t *testing.T) { @@ -98,8 +99,8 @@ func TestAuthorizeAuthorizationHeaderNoBearer(t *testing.T) { r.Header.Add("Authorization", "Greater "+validEmptyHeader) claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "invalid \"Authorization\" HTTP header") - assert.Nil(t, claims) + require.EqualError(t, err, `invalid "Authorization" HTTP header`) + require.Nil(t, claims) } func TestAuthorizeAuthorizationHeaderNoBearerRsa(t *testing.T) { @@ -107,103 +108,103 @@ func TestAuthorizeAuthorizationHeaderNoBearerRsa(t *testing.T) { r.Header.Add("Authorization", "Greater "+validEmptyHeaderRsa) claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodRS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "invalid \"Authorization\" HTTP header") - assert.Nil(t, claims) + require.EqualError(t, err, `invalid "Authorization" HTTP header`) + require.Nil(t, claims) } func TestAuthorizeAuthorizationHeaderInvalidAlg(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil) - r.Header.Add("Authorization", "Bearer "+createDummyNoneSignedJWT()) + r.Header.Add("Authorization", bearerPrefix+createDummyNoneSignedJWT()) claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: 'none' signature type is not allowed") - assert.Nil(t, claims) + require.EqualError(t, err, "unable to parse JWT: 'none' signature type is not allowed") + require.Nil(t, claims) } func TestAuthorizeAuthorizationHeaderInvalidKey(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil) - r.Header.Add("Authorization", "Bearer "+validEmptyHeader) + r.Header.Add("Authorization", bearerPrefix+validEmptyHeader) claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: signature is invalid") - assert.Nil(t, claims) + require.EqualError(t, err, "unable to parse JWT: signature is invalid") + require.Nil(t, claims) } func TestAuthorizeAuthorizationHeaderInvalidKeyRsa(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil) - r.Header.Add("Authorization", "Bearer "+validEmptyHeaderRsa) + r.Header.Add("Authorization", bearerPrefix+validEmptyHeaderRsa) claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodRS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: unable to parse RSA public key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key") - assert.Nil(t, claims) + require.EqualError(t, err, "unable to parse JWT: unable to parse RSA public key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key") + require.Nil(t, claims) } func TestAuthorizeAuthorizationHeaderNoContent(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil) - r.Header.Add("Authorization", "Bearer "+validEmptyHeader) + r.Header.Add("Authorization", bearerPrefix+validEmptyHeader) claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName) - require.Nil(t, err) - assert.Nil(t, claims.Mercure.Publish) - assert.Nil(t, claims.Mercure.Subscribe) + require.NoError(t, err) + require.Nil(t, claims.Mercure.Publish) + require.Nil(t, claims.Mercure.Subscribe) } func TestAuthorizeAuthorizationHeaderNoContentRsa(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil) - r.Header.Add("Authorization", "Bearer "+validEmptyHeaderRsa) + r.Header.Add("Authorization", bearerPrefix+validEmptyHeaderRsa) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - require.Nil(t, err) - assert.Nil(t, claims.Mercure.Publish) - assert.Nil(t, claims.Mercure.Subscribe) + require.NoError(t, err) + require.Nil(t, claims.Mercure.Publish) + require.Nil(t, claims.Mercure.Subscribe) } func TestAuthorizeAuthorizationHeader(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil) - r.Header.Add("Authorization", "Bearer "+validFullHeader) + r.Header.Add("Authorization", bearerPrefix+validFullHeader) claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } func TestAuthorizeAuthorizationHeaderRsa(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil) - r.Header.Add("Authorization", "Bearer "+validFullHeaderRsa) + r.Header.Add("Authorization", bearerPrefix+validFullHeaderRsa) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } func TestAuthorizeAuthorizationHeaderNamespacedRsa(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil) - r.Header.Add("Authorization", "Bearer "+validFullHeaderNamespacedRsa) + r.Header.Add("Authorization", bearerPrefix+validFullHeaderNamespacedRsa) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } func TestAuthorizeAuthorizationHeaderRsaWithCert(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil) - r.Header.Add("Authorization", "Bearer "+validFullHeaderRsaForCert) + r.Header.Add("Authorization", bearerPrefix+validFullHeaderRsaForCert) claims, err := authorize(r, &jwtConfig{[]byte(certificateRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } func TestAuthorizeAuthorizationHeaderWrongAlgorithm(t *testing.T) { r, _ := http.NewRequest(http.MethodGet, defaultHubURL, nil) - r.Header.Add("Authorization", "Bearer "+validFullHeaderRsa) + r.Header.Add("Authorization", bearerPrefix+validFullHeaderRsa) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), nil}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: : unexpected signing method") + require.EqualError(t, err, "unable to parse JWT: : unexpected signing method") assert.Nil(t, claims) } @@ -214,8 +215,8 @@ func TestAuthorizeAuthorizationQueryTooShort(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "invalid \"authorization\" Query parameter") - assert.Nil(t, claims) + require.EqualError(t, err, `invalid "authorization" Query parameter`) + require.Nil(t, claims) } func TestAuthorizeAuthorizationQueryInvalidAlg(t *testing.T) { @@ -225,8 +226,8 @@ func TestAuthorizeAuthorizationQueryInvalidAlg(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: 'none' signature type is not allowed") - assert.Nil(t, claims) + require.EqualError(t, err, "unable to parse JWT: 'none' signature type is not allowed") + require.Nil(t, claims) } func TestAuthorizeAuthorizationQueryInvalidKey(t *testing.T) { @@ -236,8 +237,8 @@ func TestAuthorizeAuthorizationQueryInvalidKey(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: signature is invalid") - assert.Nil(t, claims) + require.EqualError(t, err, "unable to parse JWT: signature is invalid") + require.Nil(t, claims) } func TestAuthorizeAuthorizationQueryInvalidKeyRsa(t *testing.T) { @@ -247,8 +248,8 @@ func TestAuthorizeAuthorizationQueryInvalidKeyRsa(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodRS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: unable to parse RSA public key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key") - assert.Nil(t, claims) + require.EqualError(t, err, "unable to parse JWT: unable to parse RSA public key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key") + require.Nil(t, claims) } func TestAuthorizeAuthorizationQueryNoContent(t *testing.T) { @@ -258,9 +259,9 @@ func TestAuthorizeAuthorizationQueryNoContent(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName) - require.Nil(t, err) - assert.Nil(t, claims.Mercure.Publish) - assert.Nil(t, claims.Mercure.Subscribe) + require.NoError(t, err) + require.Nil(t, claims.Mercure.Publish) + require.Nil(t, claims.Mercure.Subscribe) } func TestAuthorizeAuthorizationQueryNoContentRsa(t *testing.T) { @@ -270,9 +271,9 @@ func TestAuthorizeAuthorizationQueryNoContentRsa(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - require.Nil(t, err) - assert.Nil(t, claims.Mercure.Publish) - assert.Nil(t, claims.Mercure.Subscribe) + require.NoError(t, err) + require.Nil(t, claims.Mercure.Publish) + require.Nil(t, claims.Mercure.Subscribe) } func TestAuthorizeAuthorizationQuery(t *testing.T) { @@ -282,7 +283,7 @@ func TestAuthorizeAuthorizationQuery(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } @@ -294,7 +295,7 @@ func TestAuthorizeAuthorizationQueryRsa(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } @@ -306,7 +307,7 @@ func TestAuthorizeAuthorizationQueryNamespacedRsa(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } @@ -318,7 +319,7 @@ func TestAuthorizeAuthorizationQueryRsaWithCert(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte(certificateRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } @@ -330,8 +331,8 @@ func TestAuthorizeAuthorizationQueryWrongAlgorithm(t *testing.T) { r.URL.RawQuery = query.Encode() claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), nil}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: : unexpected signing method") - assert.Nil(t, claims) + require.EqualError(t, err, "unable to parse JWT: : unexpected signing method") + require.Nil(t, claims) } func TestAuthorizeCookieInvalidAlg(t *testing.T) { @@ -339,8 +340,8 @@ func TestAuthorizeCookieInvalidAlg(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: createDummyNoneSignedJWT()}) claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: 'none' signature type is not allowed") - assert.Nil(t, claims) + require.EqualError(t, err, "unable to parse JWT: 'none' signature type is not allowed") + require.Nil(t, claims) } func TestAuthorizeCookieInvalidKey(t *testing.T) { @@ -348,8 +349,8 @@ func TestAuthorizeCookieInvalidKey(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeader}) claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: signature is invalid") - assert.Nil(t, claims) + require.EqualError(t, err, "unable to parse JWT: signature is invalid") + require.Nil(t, claims) } func TestAuthorizeCookieEmptyKeyRsa(t *testing.T) { @@ -357,8 +358,8 @@ func TestAuthorizeCookieEmptyKeyRsa(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeaderRsa}) claims, err := authorize(r, &jwtConfig{[]byte{}, jwt.SigningMethodRS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "unable to parse JWT: unable to parse RSA public key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key") - assert.Nil(t, claims) + require.EqualError(t, err, "unable to parse JWT: unable to parse RSA public key: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key") + require.Nil(t, claims) } func TestAuthorizeCookieInvalidKeyRsa(t *testing.T) { @@ -366,9 +367,9 @@ func TestAuthorizeCookieInvalidKeyRsa(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeaderRsa}) claims, err := authorize(r, &jwtConfig{[]byte(privateKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - assert.Error(t, err) + require.Error(t, err) + require.Nil(t, claims) assert.Contains(t, err.Error(), "unable to parse JWT: unable to parse RSA public key") // The error message changed in Go 1.17 - assert.Nil(t, claims) } func TestAuthorizeCookieNoContent(t *testing.T) { @@ -376,9 +377,9 @@ func TestAuthorizeCookieNoContent(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeader}) claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName) - require.Nil(t, err) - assert.Nil(t, claims.Mercure.Publish) - assert.Nil(t, claims.Mercure.Subscribe) + require.NoError(t, err) + require.Nil(t, claims.Mercure.Publish) + require.Nil(t, claims.Mercure.Subscribe) } func TestAuthorizeCookieNoContentRsa(t *testing.T) { @@ -386,9 +387,9 @@ func TestAuthorizeCookieNoContentRsa(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validEmptyHeaderRsa}) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - require.Nil(t, err) - assert.Nil(t, claims.Mercure.Publish) - assert.Nil(t, claims.Mercure.Subscribe) + require.NoError(t, err) + require.Nil(t, claims.Mercure.Publish) + require.Nil(t, claims.Mercure.Subscribe) } func TestAuthorizeCookie(t *testing.T) { @@ -396,7 +397,7 @@ func TestAuthorizeCookie(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader}) claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } @@ -406,7 +407,7 @@ func TestAuthorizeCookieRsa(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa}) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } @@ -416,8 +417,8 @@ func TestAuthorizeCookieNoOriginNoReferer(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader}) claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "an \"Origin\" or a \"Referer\" HTTP header must be present to use the cookie-based authorization mechanism") - assert.Nil(t, claims) + require.EqualError(t, err, `an "Origin" or a "Referer" HTTP header must be present to use the cookie-based authorization mechanism`) + require.Nil(t, claims) } func TestAuthorizeCookieNoOriginNoRefererRsa(t *testing.T) { @@ -425,8 +426,8 @@ func TestAuthorizeCookieNoOriginNoRefererRsa(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa}) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{}, defaultCookieName) - assert.EqualError(t, err, "an \"Origin\" or a \"Referer\" HTTP header must be present to use the cookie-based authorization mechanism") - assert.Nil(t, claims) + require.EqualError(t, err, `an "Origin" or a "Referer" HTTP header must be present to use the cookie-based authorization mechanism`) + require.Nil(t, claims) } func TestAuthorizeCookieOriginNotAllowed(t *testing.T) { @@ -435,8 +436,8 @@ func TestAuthorizeCookieOriginNotAllowed(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader}) claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"http://example.net"}, defaultCookieName) - assert.EqualError(t, err, `"http://example.com": origin not allowed to post updates`) - assert.Nil(t, claims) + require.EqualError(t, err, `"http://example.com": origin not allowed to post updates`) + require.Nil(t, claims) } func TestAuthorizeCookieOriginNotAllowedRsa(t *testing.T) { @@ -445,8 +446,8 @@ func TestAuthorizeCookieOriginNotAllowedRsa(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa}) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{"http://example.net"}, defaultCookieName) - assert.EqualError(t, err, `"http://example.com": origin not allowed to post updates`) - assert.Nil(t, claims) + require.EqualError(t, err, `"http://example.com": origin not allowed to post updates`) + require.Nil(t, claims) } func TestAuthorizeCookieRefererNotAllowed(t *testing.T) { @@ -455,8 +456,8 @@ func TestAuthorizeCookieRefererNotAllowed(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader}) claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"http://example.net"}, defaultCookieName) - assert.EqualError(t, err, `"http://example.com": origin not allowed to post updates`) - assert.Nil(t, claims) + require.EqualError(t, err, `"http://example.com": origin not allowed to post updates`) + require.Nil(t, claims) } func TestAuthorizeCookieRefererNotAllowedRsa(t *testing.T) { @@ -465,8 +466,8 @@ func TestAuthorizeCookieRefererNotAllowedRsa(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa}) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{"http://example.net"}, defaultCookieName) - assert.EqualError(t, err, `"http://example.com": origin not allowed to post updates`) - assert.Nil(t, claims) + require.EqualError(t, err, `"http://example.com": origin not allowed to post updates`) + require.Nil(t, claims) } func TestAuthorizeCookieInvalidReferer(t *testing.T) { @@ -475,8 +476,8 @@ func TestAuthorizeCookieInvalidReferer(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader}) claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"http://example.net"}, defaultCookieName) - assert.EqualError(t, err, `unable to parse referer: parse "http://192.168.0.%31/": invalid URL escape "%31"`) - assert.Nil(t, claims) + require.EqualError(t, err, `unable to parse referer: parse "http://192.168.0.%31/": invalid URL escape "%31"`) + require.Nil(t, claims) } func TestAuthorizeCookieInvalidRefererRsa(t *testing.T) { @@ -485,8 +486,8 @@ func TestAuthorizeCookieInvalidRefererRsa(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa}) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{"http://example.net"}, defaultCookieName) - assert.EqualError(t, err, `unable to parse referer: parse "http://192.168.0.%31/": invalid URL escape "%31"`) - assert.Nil(t, claims) + require.EqualError(t, err, `unable to parse referer: parse "http://192.168.0.%31/": invalid URL escape "%31"`) + require.Nil(t, claims) } func TestAuthorizeCookieOriginHasPriority(t *testing.T) { @@ -496,7 +497,7 @@ func TestAuthorizeCookieOriginHasPriority(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader}) claims, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"http://example.net"}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } @@ -508,7 +509,7 @@ func TestAuthorizeCookieOriginHasPriorityRsa(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeaderRsa}) claims, err := authorize(r, &jwtConfig{[]byte(publicKeyRsa), jwt.SigningMethodRS256}, []string{"http://example.net"}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) assert.Equal(t, []string{"foo", "bar"}, claims.Mercure.Publish) assert.Equal(t, []string{"foo", "baz"}, claims.Mercure.Subscribe) } @@ -519,7 +520,7 @@ func TestAuthorizeAllOriginsAllowed(t *testing.T) { r.AddCookie(&http.Cookie{Name: defaultCookieName, Value: validFullHeader}) _, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"*"}, defaultCookieName) - require.Nil(t, err) + require.NoError(t, err) } func TestAuthorizeCustomCookieNane(t *testing.T) { @@ -528,7 +529,7 @@ func TestAuthorizeCustomCookieNane(t *testing.T) { r.AddCookie(&http.Cookie{Name: "foo", Value: validFullHeader}) _, err := authorize(r, &jwtConfig{[]byte("!ChangeMe!"), jwt.SigningMethodHS256}, []string{"*"}, "foo") - require.Nil(t, err) + require.NoError(t, err) } func TestCanReceive(t *testing.T) { diff --git a/bolt_transport_test.go b/bolt_transport_test.go index 196076fd..5dfaca49 100644 --- a/bolt_transport_test.go +++ b/bolt_transport_test.go @@ -38,7 +38,7 @@ func TestBoltTransportHistory(t *testing.T) { s := NewSubscriber("8", transport.logger) s.SetTopics(topics, nil) - require.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) var count int for { @@ -71,7 +71,7 @@ func TestBoltTransportLogsBogusLastEventID(t *testing.T) { s := NewSubscriber("711131", logger) s.SetTopics(topics, nil) - require.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) log := sink.String() assert.Contains(t, log, `"LastEventID":"711131"`) @@ -90,7 +90,7 @@ func TestBoltTopicSelectorHistory(t *testing.T) { s := NewSubscriber(EarliestLastEventID, transport.logger) s.SetTopics([]string{"http://example.com/subscribed", "http://example.com/subscribed-public-only"}, []string{"http://example.com/subscribed"}) - require.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) assert.Equal(t, "1", (<-s.Receive()).ID) assert.Equal(t, "4", (<-s.Receive()).ID) @@ -111,7 +111,7 @@ func TestBoltTransportRetrieveAllHistory(t *testing.T) { s := NewSubscriber(EarliestLastEventID, transport.logger) s.SetTopics(topics, nil) - require.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) var count int for { @@ -141,7 +141,7 @@ func TestBoltTransportHistoryAndLive(t *testing.T) { s := NewSubscriber("8", transport.logger) s.SetTopics(topics, nil) - require.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) var wg sync.WaitGroup wg.Add(1) @@ -192,13 +192,13 @@ func TestBoltTransportPurgeHistory(t *testing.T) { func TestNewBoltTransport(t *testing.T) { u, _ := url.Parse("bolt://test.db?bucket_name=demo") transport, err := NewBoltTransport(u, zap.NewNop()) - assert.Nil(t, err) + require.NoError(t, err) require.NotNil(t, transport) transport.Close() u, _ = url.Parse("bolt://") _, err = NewBoltTransport(u, zap.NewNop()) - assert.EqualError(t, err, `"bolt:": invalid transport: missing path`) + require.EqualError(t, err, `"bolt:": invalid transport: missing path`) u, _ = url.Parse("bolt:///test.db") _, err = NewBoltTransport(u, zap.NewNop()) @@ -208,11 +208,11 @@ func TestNewBoltTransport(t *testing.T) { u, _ = url.Parse("bolt://test.db?cleanup_frequency=invalid") _, err = NewBoltTransport(u, zap.NewNop()) - assert.EqualError(t, err, `"bolt://test.db?cleanup_frequency=invalid": invalid "cleanup_frequency" parameter "invalid": invalid transport: strconv.ParseFloat: parsing "invalid": invalid syntax`) + require.EqualError(t, err, `"bolt://test.db?cleanup_frequency=invalid": invalid "cleanup_frequency" parameter "invalid": invalid transport: strconv.ParseFloat: parsing "invalid": invalid syntax`) u, _ = url.Parse("bolt://test.db?size=invalid") _, err = NewBoltTransport(u, zap.NewNop()) - assert.EqualError(t, err, `"bolt://test.db?size=invalid": invalid "size" parameter "invalid": invalid transport: strconv.ParseUint: parsing "invalid": invalid syntax`) + require.EqualError(t, err, `"bolt://test.db?size=invalid": invalid "size" parameter "invalid": invalid transport: strconv.ParseUint: parsing "invalid": invalid syntax`) } func TestBoltTransportDoNotDispatchUntilListen(t *testing.T) { @@ -222,7 +222,7 @@ func TestBoltTransportDoNotDispatchUntilListen(t *testing.T) { assert.Implements(t, (*Transport)(nil), transport) s := NewSubscriber("", transport.logger) - require.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) var wg sync.WaitGroup wg.Add(1) @@ -248,21 +248,21 @@ func TestBoltTransportDispatch(t *testing.T) { s := NewSubscriber("", transport.logger) s.SetTopics([]string{"https://example.com/foo", "https://example.com/private"}, []string{"https://example.com/private"}) - require.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) notSubscribed := &Update{Topics: []string{"not-subscribed"}} - require.Nil(t, transport.Dispatch(notSubscribed)) + require.NoError(t, transport.Dispatch(notSubscribed)) subscribedNotAuthorized := &Update{Topics: []string{"https://example.com/foo"}, Private: true} - require.Nil(t, transport.Dispatch(subscribedNotAuthorized)) + require.NoError(t, transport.Dispatch(subscribedNotAuthorized)) public := &Update{Topics: s.SubscribedTopics} - require.Nil(t, transport.Dispatch(public)) + require.NoError(t, transport.Dispatch(public)) assert.Equal(t, public, <-s.Receive()) private := &Update{Topics: s.AllowedPrivateTopics, Private: true} - require.Nil(t, transport.Dispatch(private)) + require.NoError(t, transport.Dispatch(private)) assert.Equal(t, private, <-s.Receive()) } @@ -276,10 +276,10 @@ func TestBoltTransportClosed(t *testing.T) { s := NewSubscriber("", transport.logger) s.SetTopics([]string{"https://example.com/foo"}, nil) - require.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) - require.Nil(t, transport.Close()) - require.NotNil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.Close()) + require.Error(t, transport.AddSubscriber(s)) assert.Equal(t, transport.Dispatch(&Update{Topics: s.SubscribedTopics}), ErrClosedTransport) @@ -295,11 +295,11 @@ func TestBoltCleanDisconnectedSubscribers(t *testing.T) { s1 := NewSubscriber("", transport.logger) s1.SetTopics([]string{"foo"}, []string{}) - require.Nil(t, transport.AddSubscriber(s1)) + require.NoError(t, transport.AddSubscriber(s1)) s2 := NewSubscriber("", transport.logger) s2.SetTopics([]string{"foo"}, []string{}) - require.Nil(t, transport.AddSubscriber(s2)) + require.NoError(t, transport.AddSubscriber(s2)) assert.Equal(t, 2, transport.subscribers.Len()) @@ -319,30 +319,31 @@ func TestBoltGetSubscribers(t *testing.T) { defer os.Remove("test.db") s1 := NewSubscriber("", transport.logger) - require.Nil(t, transport.AddSubscriber(s1)) + require.NoError(t, transport.AddSubscriber(s1)) s2 := NewSubscriber("", transport.logger) - require.Nil(t, transport.AddSubscriber(s2)) + require.NoError(t, transport.AddSubscriber(s2)) lastEventID, subscribers, err := transport.GetSubscribers() + require.NoError(t, err) + assert.Equal(t, EarliestLastEventID, lastEventID) assert.Len(t, subscribers, 2) assert.Contains(t, subscribers, s1) assert.Contains(t, subscribers, s2) - assert.Nil(t, err) } func TestBoltLastEventID(t *testing.T) { db, err := bolt.Open("test.db", 0o600, nil) defer os.Remove("test.db") - require.Nil(t, err) + require.NoError(t, err) db.Update(func(tx *bolt.Tx) error { bucket, err := tx.CreateBucketIfNotExists([]byte(defaultBoltBucketName)) - require.Nil(t, err) + require.NoError(t, err) seq, err := bucket.NextSequence() - require.Nil(t, err) + require.NoError(t, err) prefix := make([]byte, 8) binary.BigEndian.PutUint64(prefix, seq) @@ -355,7 +356,7 @@ func TestBoltLastEventID(t *testing.T) { return bucket.Put(key, []byte("invalid")) }) - require.Nil(t, db.Close()) + require.NoError(t, db.Close()) transport := createBoltTransport("bolt://test.db") require.NotNil(t, transport) diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go index f67fdfa2..5564b4b9 100644 --- a/caddy/caddy_test.go +++ b/caddy/caddy_test.go @@ -16,6 +16,7 @@ import ( ) const ( + bearerPrefix = "Bearer " publisherJWT = "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdfX0.vhMwOaN5K68BTIhWokMLOeOJO4EPfT64brd8euJOA4M" publisherJWTRSA = "eyJhbGciOiJSUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.iwryQ5k-CWNCNQLPg7CtgTdDWbG_CurSxDK8kMjTZfprGhh7Yli1SFt8WB3U4zbZ2wxUO7UfprZq3hnl8nSrozO9KDTCDwCYhMgRlcrdwm6XL1uXFwMJt4VSmp1srCQotv0FgT11jF8Km1vMQQOnUC27Va9fbfRtITVsjxsveYeMJqusVWO6F3vAvkM35oL8E8qgBbfrG_lnuhb_9Ws6RIq4YOslkOar_gopEs00CITxmV_aHVHRYzeW7QpycxjC7m8Mp-lKzaUewvJuKWI5HsM134xfaH8RAHSvh6H9pVQAiJ9tyc17bAx46M98WMsHFokVwz3rd7PoGGou6A7y5RzeGpiSxykTWCPPcBnxJ1gwUYqEYGTnRjl9JmhHY_VfQP4edyU-zhmMCCSie8rvkRDilAQGd5kj5m1voSn-EqA13sSe69evXxVUIB2nO70qHCcHBBHxunLqTIIerpc3F9_WWM4_Q_0j9CoTd2aFyuq_sdc6RcmAE3uTznp2DyKNQkT1EfpY7xCCe1MR-Webez5Ioa1EMDP0KrvLdnNRmuM3THSu1pqcvPV7Di7dJci5QWsYEmaP8cLuuZXdAhy_UoSgzbvfT_8mlDoJ9VvDXLJ39OwGYIyZiZ9VTNXm8mxre993cqg7boZRS8x70VRxnjmNxm40SgEvb6CHYO0lSBU" subscriberJWT = "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InN1YnNjcmliZSI6WyIqIl19fQ.g3w81T7YQLKLrgovor9uEKUiOCAx6DmAAbq18qmDwsY" @@ -82,7 +83,7 @@ func TestMercure(t *testing.T) { req, err := http.NewRequest(http.MethodPost, "http://localhost:9080/.well-known/mercure", strings.NewReader(body.Encode())) require.Nil(t, err) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+publisherJWT) + req.Header.Add("Authorization", bearerPrefix+publisherJWT) resp := tester.AssertResponseCode(req, http.StatusOK) resp.Body.Close() @@ -155,7 +156,7 @@ func TestJWTPlaceholders(t *testing.T) { req, err := http.NewRequest(http.MethodPost, "http://localhost:9080/.well-known/mercure", strings.NewReader(body.Encode())) require.Nil(t, err) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+publisherJWTRSA) + req.Header.Add("Authorization", bearerPrefix+publisherJWTRSA) resp := tester.AssertResponseCode(req, http.StatusOK) resp.Body.Close() diff --git a/common/version_test.go b/common/version_test.go index 678eb1a3..6d352d10 100644 --- a/common/version_test.go +++ b/common/version_test.go @@ -17,8 +17,8 @@ func TestVersionInfo(t *testing.T) { Architecture: "amd64", } - assert.Equal(t, v.Shortline(), "dev") - assert.Equal(t, v.ChangelogURL(), "https://github.com/dunglas/mercure/releases/latest") + assert.Equal(t, "dev", v.Shortline()) + assert.Equal(t, "https://github.com/dunglas/mercure/releases/latest", v.ChangelogURL()) } func TestVersionInfoWithBuildDate(t *testing.T) { @@ -31,8 +31,8 @@ func TestVersionInfoWithBuildDate(t *testing.T) { Architecture: "amd64", } - assert.Equal(t, v.Shortline(), "1.0.0, built at 2020-05-03T18:42:44Z") - assert.Equal(t, v.ChangelogURL(), "https://github.com/dunglas/mercure/releases/tag/v1.0.0") + assert.Equal(t, "1.0.0, built at 2020-05-03T18:42:44Z", v.Shortline()) + assert.Equal(t, "https://github.com/dunglas/mercure/releases/tag/v1.0.0", v.ChangelogURL()) } func TestVersionInfoWithCommit(t *testing.T) { @@ -45,8 +45,8 @@ func TestVersionInfoWithCommit(t *testing.T) { Architecture: "amd64", } - assert.Equal(t, v.Shortline(), "1.0.0, commit 96ee2b9") - assert.Equal(t, v.ChangelogURL(), "https://github.com/dunglas/mercure/releases/tag/v1.0.0") + assert.Equal(t, "1.0.0, commit 96ee2b9", v.Shortline()) + assert.Equal(t, "https://github.com/dunglas/mercure/releases/tag/v1.0.0", v.ChangelogURL()) } func TestVersionInfoWithBuildDateAndCommit(t *testing.T) { @@ -59,8 +59,8 @@ func TestVersionInfoWithBuildDateAndCommit(t *testing.T) { Architecture: "amd64", } - assert.Equal(t, v.Shortline(), "1.0.0, commit 96ee2b9, built at 2020-05-03T18:42:44Z") - assert.Equal(t, v.ChangelogURL(), "https://github.com/dunglas/mercure/releases/tag/v1.0.0") + assert.Equal(t, "1.0.0, commit 96ee2b9, built at 2020-05-03T18:42:44Z", v.Shortline()) + assert.Equal(t, "https://github.com/dunglas/mercure/releases/tag/v1.0.0", v.ChangelogURL()) } func TestVersionMetricsCollectorInitialization(t *testing.T) { @@ -95,5 +95,5 @@ func TestVersionMetricsCollectorInitialization(t *testing.T) { t.Fatal(err) } - assert.Equal(t, 1.0, *metricOut.Gauge.Value) + assert.Equal(t, 1.0, metricOut.GetGauge().GetValue()) //nolint:testifylint } diff --git a/config_test.go b/config_test.go index c7386275..8313b82f 100644 --- a/config_test.go +++ b/config_test.go @@ -7,11 +7,12 @@ import ( "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMissingConfig(t *testing.T) { err := ValidateConfig(viper.New()) - assert.EqualError(t, err, `invalid config: one of "jwt_key" or "publisher_jwt_key" configuration parameter must be defined`) + require.EqualError(t, err, `invalid config: one of "jwt_key" or "publisher_jwt_key" configuration parameter must be defined`) } func TestMissingKeyFile(t *testing.T) { @@ -20,7 +21,7 @@ func TestMissingKeyFile(t *testing.T) { v.Set("cert_file", "foo") err := ValidateConfig(v) - assert.EqualError(t, err, `invalid config: if the "cert_file" configuration parameter is defined, "key_file" must be defined too`) + require.EqualError(t, err, `invalid config: if the "cert_file" configuration parameter is defined, "key_file" must be defined too`) } func TestMissingCertFile(t *testing.T) { @@ -29,7 +30,7 @@ func TestMissingCertFile(t *testing.T) { v.Set("key_file", "foo") err := ValidateConfig(v) - assert.EqualError(t, err, `invalid config: if the "key_file" configuration parameter is defined, "cert_file" must be defined too`) + require.EqualError(t, err, `invalid config: if the "key_file" configuration parameter is defined, "cert_file" must be defined too`) } func TestSetFlags(t *testing.T) { diff --git a/demo.go b/demo.go index c8956359..4789eb62 100644 --- a/demo.go +++ b/demo.go @@ -9,6 +9,8 @@ import ( "time" ) +const linkSuffix = `>; rel="mercure"` + // uiContent is our static web server content. // //go:embed public @@ -29,7 +31,7 @@ func (h *Hub) Demo(w http.ResponseWriter, r *http.Request) { body := query.Get("body") jwt := query.Get("jwt") - hubLink := "<" + defaultHubURL + ">; rel=\"mercure\"" + hubLink := "<" + defaultHubURL + linkSuffix if h.cookieName != defaultCookieName { hubLink = hubLink + "; cookie-name=\"" + h.cookieName + "\"" } diff --git a/demo_test.go b/demo_test.go index 543f0430..4e4d6b1e 100644 --- a/demo_test.go +++ b/demo_test.go @@ -19,7 +19,7 @@ func TestEmptyBodyAndJWT(t *testing.T) { resp := w.Result() assert.Equal(t, "application/ld+json", resp.Header.Get("Content-Type")) - assert.Equal(t, []string{"<" + defaultHubURL + ">; rel=\"mercure\"", "; rel=\"self\""}, resp.Header["Link"]) + assert.Equal(t, []string{"<" + defaultHubURL + linkSuffix, "; rel=\"self\""}, resp.Header["Link"]) cookie := resp.Cookies()[0] assert.Equal(t, "mercureAuthorization", cookie.Name) @@ -40,7 +40,7 @@ func TestBodyAndJWT(t *testing.T) { resp := w.Result() assert.Contains(t, resp.Header.Get("Content-Type"), "xml") // Before Go 1.17, the charset wasn't set - assert.Equal(t, []string{"<" + defaultHubURL + ">; rel=\"mercure\"", "&jwt=token>; rel=\"self\""}, resp.Header["Link"]) + assert.Equal(t, []string{"<" + defaultHubURL + linkSuffix, "&jwt=token>; rel=\"self\""}, resp.Header["Link"]) cookie := resp.Cookies()[0] assert.Equal(t, "mercureAuthorization", cookie.Name) diff --git a/docs/ecosystem/awesome.md b/docs/ecosystem/awesome.md index 2b728af2..9891cf99 100644 --- a/docs/ecosystem/awesome.md +++ b/docs/ecosystem/awesome.md @@ -74,12 +74,12 @@ ### French 🇫🇷 -* 📺 [Notifications instantanées avec Mercure (Grafikart)](https://www.grafikart.fr/tutoriels/symfony-mercure-1151) -* 📺 [Live Coding : Notifications temps réel avec Mercure](https://www.youtube.com/watch?v=tqqJ1ul2M-E) -* 📺 [Explication des Server Sent Events (SSE) avec Mercure](https://www.youtube.com/watch?v=Q4LRN2wXuIc) -* 📺 [Mercure : des UIs toujours synchronisées avec les données en BDD](https://www.youtube.com/watch?v=UcBa4AugNTE) -* 📺 [Mercure, et PHP s'enamoure enfin du temps réel](https://www.youtube.com/watch?v=GugURP88Rgg) -* 📺 [Async avec Messenger, AMQP et Mercure](https://www.youtube.com/watch?v=cHPbcuydJiA) +* 📺 [Notifications instantanées avec Mercure (Grafikart)](https://www.grafikart.fr/tutoriels/symfony-mercure-1151) +* 📺 [Live Coding : Notifications temps réel avec Mercure](https://www.youtube.com/watch?v=tqqJ1ul2M-E) +* 📺 [Explication des Server Sent Events (SSE) avec Mercure](https://www.youtube.com/watch?v=Q4LRN2wXuIc) +* 📺 [Mercure : des UIs toujours synchronisées avec les données en BDD](https://www.youtube.com/watch?v=UcBa4AugNTE) +* 📺 [Mercure, et PHP s'enamoure enfin du temps réel](https://www.youtube.com/watch?v=GugURP88Rgg) +* 📺 [Async avec Messenger, AMQP et Mercure](https://www.youtube.com/watch?v=cHPbcuydJiA) * [Mercure, un protocole pour pousser des mises à jour vers des navigateurs et app mobiles en temps réel (Les-Tilleuls.coop)](https://les-tilleuls.coop/blog/mercure-un-protocole-pour-pousser-des-mises-a-jour-vers-des-navigateurs-et-app-mobiles-en-temps-reel) * [Symfony et Mercure](https://afsy.fr/avent/2019/21-symfony-et-mercure) * [À la découverte de Mercure](https://blog.eleven-labs.com/fr/a-la-decouverte-de-mercure/) diff --git a/docs/ecosystem/hotwire.md b/docs/ecosystem/hotwire.md index a2c4958c..062f2229 100644 --- a/docs/ecosystem/hotwire.md +++ b/docs/ecosystem/hotwire.md @@ -21,12 +21,14 @@ The native [`EventSource` class](https://developer.mozilla.org/en-US/docs/Web/AP To broadcast messages through Turbo Streams, simply send a `POST` HTTP request to [the Mercure hub](../hub/install.md): - curl \ - -H 'Authorization: Bearer ' \ - -d 'topic=my-stream' \ - -d 'data=' \ + -d 'topic=my-stream' \ + -d 'data=` as parameter. +The client can then subscribe to the Mercure's event stream corresponding to this subscription by creating a new `EventSource` with a URL like `https://example.com/.well-known/mercure?topic=https://example.com/subscriptions/` as parameter. Updates for the given subscription can then be sent from the GraphQL server to the clients through the Mercure hub (in the `data` property of the server-sent event). diff --git a/examples/chat/Dockerfile b/examples/chat/Dockerfile index cec5cf1f..98d92307 100644 --- a/examples/chat/Dockerfile +++ b/examples/chat/Dockerfile @@ -1,7 +1,10 @@ -FROM tiangolo/meinheld-gunicorn:python3.8-alpine3.11 +FROM tiangolo/meinheld-gunicorn:python3.9 + +WORKDIR /app COPY ./requirements.txt /app -RUN pip install -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + COPY ./main.py . COPY ./static static COPY ./templates templates diff --git a/examples/chat/main.py b/examples/chat/main.py index e12a1ca8..f3efc773 100644 --- a/examples/chat/main.py +++ b/examples/chat/main.py @@ -22,50 +22,66 @@ COOKIE_DOMAIN: the cookie domain (default: None) """ -from flask import Flask, make_response, request, render_template, abort -import jwt import os -import json + +import jwt +from flask import Flask, abort, make_response, render_template, request from uritemplate import expand -HUB_URL = os.environ.get( - 'HUB_URL', 'https://localhost/.well-known/mercure') -JWT_KEY = os.environ.get('JWT_KEY', '!ChangeThisMercureHubJWTSecretKey!') +HUB_URL = os.environ.get("HUB_URL", "https://localhost/.well-known/mercure") +JWT_KEY = os.environ.get("JWT_KEY", "!ChangeThisMercureHubJWTSecretKey!") MESSAGE_URI_TEMPLATE = os.environ.get( - 'MESSAGE_URI_TEMPLATE', 'https://chat.example.com/messages/{id}') + "MESSAGE_URI_TEMPLATE", "https://chat.example.com/messages/{id}" +) -SUBSCRIPTIONS_TEMPLATE = '/.well-known/mercure/subscriptions/{topic}{/subscriber}' +SUBSCRIPTIONS_TEMPLATE = "/.well-known/mercure/subscriptions/{topic}{/subscriber}" SUBSCRIPTIONS_TOPIC = expand(SUBSCRIPTIONS_TEMPLATE, topic=MESSAGE_URI_TEMPLATE) app = Flask(__name__) -@app.route('/', methods=['GET']) +@app.route("/", methods=["GET"]) def join(): - return render_template('join.html') + return render_template("join.html") -@app.route('/', methods=['POST']) +@app.route("/", methods=["POST"]) def chat(): - username = request.form['username'] + username = request.form["username"] if not username: abort(400) token = jwt.encode( - {'mercure': - { - 'subscribe': [MESSAGE_URI_TEMPLATE, SUBSCRIPTIONS_TEMPLATE], - 'publish': [MESSAGE_URI_TEMPLATE], - 'payload': {'username': username} + { + "mercure": { + "subscribe": [MESSAGE_URI_TEMPLATE, SUBSCRIPTIONS_TEMPLATE], + "publish": [MESSAGE_URI_TEMPLATE], + "payload": {"username": username}, } - }, + }, JWT_KEY, - algorithm='HS256', + algorithm="HS256", ) - resp = make_response(render_template('chat.html', config={ - 'hubURL': HUB_URL, 'messageURITemplate': MESSAGE_URI_TEMPLATE, 'subscriptionsTopic': SUBSCRIPTIONS_TOPIC, 'username': username})) - resp.set_cookie('mercureAuthorization', token, httponly=True, path='/.well-known/mercure', - samesite="strict", domain=os.environ.get('COOKIE_DOMAIN', None), secure=request.is_secure) # Force secure to True for real apps + resp = make_response( + render_template( + "chat.html", + config={ + "hubURL": HUB_URL, + "messageURITemplate": MESSAGE_URI_TEMPLATE, + "subscriptionsTopic": SUBSCRIPTIONS_TOPIC, + "username": username, + }, + ) + ) + resp.set_cookie( + "mercureAuthorization", + token, + httponly=True, + path="/.well-known/mercure", + samesite="strict", + domain=os.environ.get("COOKIE_DOMAIN", None), + secure=request.is_secure, + ) # Force secure to True for real apps return resp diff --git a/examples/chat/static/app.css b/examples/chat/static/app.css index 013c0096..b5b63836 100644 --- a/examples/chat/static/app.css +++ b/examples/chat/static/app.css @@ -1,19 +1,20 @@ .hero img { - width: 40%; + width: 40%; } -#chat #messages, #chat .panel { - height: 100%; - overflow-y: scroll; +#chat #messages, +#chat .panel { + height: 100%; + overflow-y: scroll; } #chat .columns { - height: 70vh; + height: 70vh; } #chat .box { - display: flex; - flex-direction: column; - justify-content: space-between; - height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; } diff --git a/examples/chat/static/chat.js b/examples/chat/static/chat.js index 23160c89..fb9488ba 100644 --- a/examples/chat/static/chat.js +++ b/examples/chat/static/chat.js @@ -1,105 +1,107 @@ -const type = "https://chat.example.com/Message"; +/* eslint-env browser */ + +const type = 'https://chat.example.com/Message' const { hubURL, messageURITemplate, subscriptionsTopic, username } = JSON.parse( - document.getElementById("config").textContent -); + document.getElementById('config').textContent +) -document.getElementById("username").textContent = username; +document.getElementById('username').textContent = username -const $messages = document.getElementById("messages"); -const $messageTemplate = document.getElementById("message"); -const $userList = document.getElementById("user-list"); -const $onlineUserTemplate = document.getElementById("online-user"); +const $messages = document.getElementById('messages') +const $messageTemplate = document.getElementById('message') +const $userList = document.getElementById('user-list') +const $onlineUserTemplate = document.getElementById('online-user') -let userList, es; +let userList; (async () => { const resp = await fetch(new URL(subscriptionsTopic, hubURL), { - credentials: "include", - }); - const subscriptionCollection = await resp.json(); + credentials: 'include' + }) + const subscriptionCollection = await resp.json() userList = new Map( subscriptionCollection.subscriptions .reduce((acc, { payload }) => { - if (payload.username != username) acc.push([payload.username, true]); - return acc; + if (payload.username !== username) acc.push([payload.username, true]) + return acc }, []) .sort() - ); - updateUserListView(); + ) + updateUserListView() - const subscribeURL = new URL(hubURL); + const subscribeURL = new URL(hubURL) subscribeURL.searchParams.append( - "lastEventID", + 'lastEventID', subscriptionCollection.lastEventID - ); - subscribeURL.searchParams.append("topic", messageURITemplate); + ) + subscribeURL.searchParams.append('topic', messageURITemplate) subscribeURL.searchParams.append( - "topic", + 'topic', `${subscriptionsTopic}{/subscriber}` - ); + ) - const es = new EventSource(subscribeURL, { withCredentials: true }); + const es = new EventSource(subscribeURL, { withCredentials: true }) es.onmessage = ({ data }) => { - const update = JSON.parse(data); + const update = JSON.parse(data) - if (update["@type"] === type) { - displayMessage(update); - return; + if (update['@type'] === type) { + displayMessage(update) + return } - if (update["type"] === "Subscription") { - updateUserList(update); - return; + if (update.type === 'Subscription') { + updateUserList(update) + return } - console.warn("Received an unsupported update type", update); - }; -})(); + console.warn('Received an unsupported update type', update) + } +})() const updateUserListView = () => { - $userList.textContent = ""; + $userList.textContent = '' userList.forEach((_, username) => { - const el = document.importNode($onlineUserTemplate.content, true); - el.querySelector(".username").textContent = username; - $userList.append(el); - }); -}; + const el = document.importNode($onlineUserTemplate.content, true) + el.querySelector('.username').textContent = username + $userList.append(el) + }) +} const displayMessage = ({ username, message }) => { - const el = document.importNode($messageTemplate.content, true); - el.querySelector(".username").textContent = username; - el.querySelector(".msg").textContent = message; - $messages.append(el); + const el = document.importNode($messageTemplate.content, true) + el.querySelector('.username').textContent = username + el.querySelector('.msg').textContent = message + $messages.append(el) // scroll at the bottom when a new message is received - $messages.scrollTop = $messages.scrollHeight; -}; + $messages.scrollTop = $messages.scrollHeight +} const updateUserList = ({ active, payload }) => { - if (username === payload.username) return; + if (username === payload.username) return - active ? userList.set(payload.username, true) : userList.delete(payload.username); - userList = new Map([...userList.entries()].sort()); + active ? userList.set(payload.username, true) : userList.delete(payload.username) + userList = new Map([...userList.entries()].sort()) - updateUserListView(); -}; + updateUserListView() +} -document.querySelector("form").onsubmit = function (e) { - e.preventDefault(); +document.querySelector('form').onsubmit = function (e) { + e.preventDefault() - const uid = window.crypto.getRandomValues(new Uint8Array(10)).join(""); - const messageTopic = messageURITemplate.replace("{id}", uid); + const uid = window.crypto.getRandomValues(new Uint8Array(10)).join('') + const messageTopic = messageURITemplate.replace('{id}', uid) const body = new URLSearchParams({ data: JSON.stringify({ - "@type": type, - "@id": messageTopic, - username: username, - message: this.elements.message.value, + '@type': type, + '@id': messageTopic, + username, + message: this.elements.message.value }), topic: messageTopic, - private: true, - }); - fetch(hubURL, { method: "POST", body, credentials: "include" }); - this.elements.message.value = ""; - this.elements.message.focus(); -}; + private: true + }) + fetch(hubURL, { method: 'POST', body, credentials: 'include' }) + this.elements.message.value = '' + this.elements.message.focus() +} diff --git a/examples/publish/node.js b/examples/publish/node.js index d14dbf5a..f3e0e677 100644 --- a/examples/publish/node.js +++ b/examples/publish/node.js @@ -1,35 +1,35 @@ -const http = require("http"); -const querystring = require("querystring"); +const http = require('http') +const querystring = require('querystring') const demoJwt = - "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM"; + 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM' const postData = querystring.stringify({ - topic: "https://localhost/demo/books/1.jsonld", - data: JSON.stringify({ key: "updated value" }), -}); + topic: 'https://localhost/demo/books/1.jsonld', + data: JSON.stringify({ key: 'updated value' }) +}) const req = http.request( { - hostname: "localhost", - port: "3000", - path: "/.well-known/mercure", - method: "POST", + hostname: 'localhost', + port: '3000', + path: '/.well-known/mercure', + method: 'POST', headers: { Authorization: `Bearer ${demoJwt}`, - "Content-Type": "application/x-www-form-urlencoded", - "Content-Length": Buffer.byteLength(postData), - }, + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData) + } }, (res) => { - console.log(`Status: ${res.statusCode}`); - console.log(`Headers: ${JSON.stringify(res.headers)}`); + console.log(`Status: ${res.statusCode}`) + console.log(`Headers: ${JSON.stringify(res.headers)}`) } -); +) -req.on("error", (e) => { - console.error(`An error occurred: ${e.message}`); -}); +req.on('error', (e) => { + console.error(`An error occurred: ${e.message}`) +}) -req.write(postData); -req.end(); +req.write(postData) +req.end() diff --git a/examples/publish/php.php b/examples/publish/php.php index c46e7e51..36571fcd 100644 --- a/examples/publish/php.php +++ b/examples/publish/php.php @@ -9,6 +9,6 @@ echo file_get_contents('https://localhost/.well-known/mercure', false, stream_context_create(['http' => [ 'method' => 'POST', - 'header' => "Content-type: application/x-www-form-urlencoded\r\nAuthorization: Bearer ".DEMO_JWT, + 'header' => "Content-type: application/x-www-form-urlencoded\r\nAuthorization: Bearer " . DEMO_JWT, 'content' => $postData, ]])); diff --git a/examples/publish/ruby.rb b/examples/publish/ruby.rb index 5faf75c0..f47ed682 100644 --- a/examples/publish/ruby.rb +++ b/examples/publish/ruby.rb @@ -1,13 +1,15 @@ -require 'json' -require 'net/http' +# frozen_string_literal: true -token = 'eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM' +require "json" +require "net/http" -Net::HTTP.start('localhost', 3000) do |http| - req = Net::HTTP::Post.new('/.well-known/mercure') - req['Authorization'] = "Bearer #{token}" +token = "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM" + +Net::HTTP.start("localhost", 3000) do |http| + req = Net::HTTP::Post.new("/.well-known/mercure") + req["Authorization"] = "Bearer #{token}" req.form_data = { - topic: 'https://localhost/demo/books/1.jsonld', + topic: "https://localhost/demo/books/1.jsonld", data: { key: :value }.to_json } req = http.request(req) diff --git a/examples/subscribe/python.py b/examples/subscribe/python.py index 586e7730..83408c15 100644 --- a/examples/subscribe/python.py +++ b/examples/subscribe/python.py @@ -1,17 +1,18 @@ -from sseclient import SSEClient -import jwt import os +import jwt +from sseclient import SSEClient + token = jwt.encode( - {'mercure': {'subscribe': ['*']}}, - os.environ.get('JWT_KEY', '!ChangeThisMercureHubJWTSecretKey!'), - algorithm='HS256', + {"mercure": {"subscribe": ["*"]}}, + os.environ.get("JWT_KEY", "!ChangeThisMercureHubJWTSecretKey!"), + algorithm="HS256", ) updates = SSEClient( - os.environ.get('HUB_URL', 'https://localhost/.well-known/mercure'), - params={'topic': ['*']}, - headers={'Authorization': b'Bearer '+token}, + os.environ.get("HUB_URL", "https://localhost/.well-known/mercure"), + params={"topic": ["*"]}, + headers={"Authorization": b"Bearer " + token}, ) for update in updates: print("Update received: ", update) diff --git a/gatling/LoadTest.scala b/gatling/LoadTest.scala index 954fe560..24ae165f 100644 --- a/gatling/LoadTest.scala +++ b/gatling/LoadTest.scala @@ -1,19 +1,24 @@ /** Load test for Mercure. * - * 1. Grab Gatling 3 on https://gatling.io - * 2. Run path/to/gatling/bin/gatling.sh --simulations-folder . + * 1. Grab Gatling 3 on https://gatling.io 2. Run + * path/to/gatling/bin/gatling.sh --simulations-folder . * * Available environment variables (all optional): * - HUB_URL: the URL of the hub to test - * - JWT: the JWT to use for authenticating the publisher, fallbacks to JWT if not set and PRIVATE_UPDATES set - * - INITIAL_SUBSCRIBERS: the number of concurrent subscribers initially connected - * - SUBSCRIBERS_RATE_FROM: minimum rate (per second) of additional subscribers to connect - * - SUBSCRIBERS_RATE_TO: maximum rate (per second) of additional subscribers to connect + * - JWT: the JWT to use for authenticating the publisher, fallbacks to JWT + * if not set and PRIVATE_UPDATES set + * - INITIAL_SUBSCRIBERS: the number of concurrent subscribers initially + * connected + * - SUBSCRIBERS_RATE_FROM: minimum rate (per second) of additional + * subscribers to connect + * - SUBSCRIBERS_RATE_TO: maximum rate (per second) of additional subscribers + * to connect * - PUBLISHERS_RATE_FROM: minimum rate (per second) of publications * - PUBLISHERS_RATE_TO: maximum rate (per second) of publications * - INJECTION_DURATION: duration of the publishers injection * - CONNECTION_DURATION: duration of subscribers' connection - * - RANDOM_CONNECTION_DURATION: to randomize the connection duration (will longs CONNECTION_DURATION at max) + * - RANDOM_CONNECTION_DURATION: to randomize the connection duration (will + * longs CONNECTION_DURATION at max) */ package mercure @@ -35,7 +40,9 @@ class LoadTest extends Simulation { "eyJhbGciOiJIUzI1NiJ9.eyJtZXJjdXJlIjp7InB1Ymxpc2giOlsiKiJdLCJzdWJzY3JpYmUiOlsiaHR0cHM6Ly9leGFtcGxlLmNvbS9teS1wcml2YXRlLXRvcGljIiwie3NjaGVtZX06Ly97K2hvc3R9L2RlbW8vYm9va3Mve2lkfS5qc29ubGQiLCIvLndlbGwta25vd24vbWVyY3VyZS9zdWJzY3JpcHRpb25zey90b3BpY317L3N1YnNjcmliZXJ9Il0sInBheWxvYWQiOnsidXNlciI6Imh0dHBzOi8vZXhhbXBsZS5jb20vdXNlcnMvZHVuZ2xhcyIsInJlbW90ZUFkZHIiOiIxMjcuMC4wLjEifX19.KKPIikwUzRuB3DTpVw6ajzwSChwFw5omBMmMcWKiDcM" ) - /** JWT to use to subscribe, fallbacks to JWT if not set and PRIVATE_UPDATES set */ + /** JWT to use to subscribe, fallbacks to JWT if not set and PRIVATE_UPDATES + * set + */ val SubscriberJwt = Properties.envOrElse("SUBSCRIBER_JWT", null) /** Number of concurrent subscribers initially connected */ @@ -65,13 +72,16 @@ class LoadTest extends Simulation { val RandomConnectionDuration = Properties.envOrElse("RANDOM_CONNECTION_DURATION", "true").toBoolean - /** Send private updates with random topics instead of public ones always with the same topic */ + /** Send private updates with random topics instead of public ones always with + * the same topic + */ var PrivateUpdates = Properties.envOrElse("PRIVATE_UPDATES", "false").toBoolean val rnd = new scala.util.Random - /** Subscriber test as a function to handle conditional Authorization header */ + /** Subscriber test as a function to handle conditional Authorization header + */ def subscriberTest() = { var topic = "https://example.com" if (PrivateUpdates) { diff --git a/handler.go b/handler.go index c0df5454..199be65e 100644 --- a/handler.go +++ b/handler.go @@ -256,7 +256,13 @@ func (h *Hub) chainHandlers() http.Handler { //nolint:funlen } secureHandler := secureMiddleware.Handler(useForwardedHeadersHandlers) - loggingHandler := handlers.CombinedLoggingHandler(os.Stderr, secureHandler) + + var loggingHandler http.Handler + if h.logger != nil && h.logger.Level().Enabled(zap.FatalLevel) { + loggingHandler = handlers.CombinedLoggingHandler(os.Stderr, secureHandler) + } else { + loggingHandler = secureHandler + } recoveryHandler := handlers.RecoveryHandler( handlers.RecoveryLogger(zapRecoveryHandlerLogger{h.logger}), handlers.PrintRecoveryStack(h.debug), diff --git a/hub_test.go b/hub_test.go index 2936b64c..507ee63b 100644 --- a/hub_test.go +++ b/hub_test.go @@ -1,7 +1,6 @@ package mercure import ( - "errors" "fmt" "net/http" "net/http/httptest" @@ -41,7 +40,7 @@ func TestNewHubWithConfig(t *testing.T) { WithSubscriberJWT([]byte("bar"), jwt.SigningMethodHS256.Name), ) require.NotNil(t, h) - require.Nil(t, err) + require.NoError(t, err) } func TestNewHubValidationError(t *testing.T) { @@ -72,7 +71,7 @@ func TestStartCrash(t *testing.T) { err := cmd.Run() var e *exec.ExitError - require.True(t, errors.As(err, &e)) + require.ErrorAs(t, err, &e) assert.False(t, e.Success()) } @@ -123,7 +122,7 @@ func TestWithProtocolVersionCompatibility(t *testing.T) { assert.False(t, op.isBackwardCompatiblyEnabledWith(7)) o := WithProtocolVersionCompatibility(7) - require.Nil(t, o(op)) + require.NoError(t, o(op)) assert.Equal(t, 7, op.protocolVersionCompatibility) assert.True(t, op.isBackwardCompatiblyEnabledWith(7)) assert.True(t, op.isBackwardCompatiblyEnabledWith(8)) @@ -148,7 +147,7 @@ func TestWithProtocolVersionCompatibilityVersions(t *testing.T) { o := WithProtocolVersionCompatibility(tc.version) if tc.ok { - require.Nil(t, o(op)) + require.NoError(t, o(op)) } else { require.Error(t, o(op)) } @@ -187,18 +186,18 @@ func TestOriginsValidator(t *testing.T) { for _, origins := range validOrigins { o := WithPublishOrigins(origins) - require.Nil(t, o(op), "error while not expected for %#v", origins) + require.NoError(t, o(op), "error while not expected for %#v", origins) o = WithCORSOrigins(origins) - require.Nil(t, o(op), "error while not expected for %#v", origins) + require.NoError(t, o(op), "error while not expected for %#v", origins) } for _, origins := range invalidOrigins { o := WithPublishOrigins(origins) - require.NotNil(t, o(op), "no error while expected for %#v", origins) + require.Error(t, o(op), "no error while expected for %#v", origins) o = WithCORSOrigins(origins) - require.NotNil(t, o(op), "no error while expected for %#v", origins) + require.Error(t, o(op), "no error while expected for %#v", origins) } } diff --git a/local_transport_test.go b/local_transport_test.go index 3f72dca0..15852bfa 100644 --- a/local_transport_test.go +++ b/local_transport_test.go @@ -17,11 +17,11 @@ func TestLocalTransportDoNotDispatchUntilListen(t *testing.T) { u := &Update{Topics: []string{"http://example.com/books/1"}} err := transport.Dispatch(u) - require.Nil(t, err) + require.NoError(t, err) s := NewSubscriber("", zap.NewNop()) s.SetTopics(u.Topics, nil) - require.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) var wg sync.WaitGroup wg.Add(1) @@ -43,10 +43,10 @@ func TestLocalTransportDispatch(t *testing.T) { s := NewSubscriber("", zap.NewNop()) s.SetTopics([]string{"http://example.com/foo"}, nil) - assert.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) u := &Update{Topics: s.SubscribedTopics} - require.Nil(t, transport.Dispatch(u)) + require.NoError(t, transport.Dispatch(u)) assert.Equal(t, u, <-s.Receive()) } @@ -56,9 +56,8 @@ func TestLocalTransportClosed(t *testing.T) { assert.Implements(t, (*Transport)(nil), transport) s := NewSubscriber("", zap.NewNop()) - require.Nil(t, transport.AddSubscriber(s)) - - assert.Nil(t, transport.Close()) + require.NoError(t, transport.AddSubscriber(s)) + require.NoError(t, transport.Close()) assert.Equal(t, transport.AddSubscriber(NewSubscriber("", zap.NewNop())), ErrClosedTransport) assert.Equal(t, transport.Dispatch(&Update{}), ErrClosedTransport) @@ -72,10 +71,10 @@ func TestLiveCleanDisconnectedSubscribers(t *testing.T) { defer transport.Close() s1 := NewSubscriber("", zap.NewNop()) - require.Nil(t, transport.AddSubscriber(s1)) + require.NoError(t, transport.AddSubscriber(s1)) s2 := NewSubscriber("", zap.NewNop()) - require.Nil(t, transport.AddSubscriber(s2)) + require.NoError(t, transport.AddSubscriber(s2)) assert.Equal(t, 2, transport.subscribers.Len()) @@ -95,10 +94,10 @@ func TestLiveReading(t *testing.T) { s := NewSubscriber("", zap.NewNop()) s.SetTopics([]string{"https://example.com"}, nil) - require.Nil(t, transport.AddSubscriber(s)) + require.NoError(t, transport.AddSubscriber(s)) u := &Update{Topics: s.SubscribedTopics} - assert.Nil(t, transport.Dispatch(u)) + require.NoError(t, transport.Dispatch(u)) receivedUpdate := <-s.Receive() assert.Equal(t, u, receivedUpdate) @@ -110,15 +109,15 @@ func TestLocalTransportGetSubscribers(t *testing.T) { require.NotNil(t, transport) s1 := NewSubscriber("", zap.NewNop()) - require.Nil(t, transport.AddSubscriber(s1)) + require.NoError(t, transport.AddSubscriber(s1)) s2 := NewSubscriber("", zap.NewNop()) - require.Nil(t, transport.AddSubscriber(s2)) + require.NoError(t, transport.AddSubscriber(s2)) lastEventID, subscribers, err := transport.(TransportSubscribers).GetSubscribers() + require.NoError(t, err) assert.Equal(t, EarliestLastEventID, lastEventID) assert.Len(t, subscribers, 2) assert.Contains(t, subscribers, s1) assert.Contains(t, subscribers, s2) - assert.Nil(t, err) } diff --git a/log.go b/log.go index cc3bcc3b..1e1c9efd 100644 --- a/log.go +++ b/log.go @@ -27,6 +27,6 @@ type CheckedEntry = zapcore.CheckedEntry type Logger interface { Info(msg string, fields ...LogField) Error(msg string, fields ...LogField) - Check(Level, string) *CheckedEntry + Check(level Level, msg string) *CheckedEntry Level() Level } diff --git a/metrics.go b/metrics.go index 9271d67b..e73bc9d7 100644 --- a/metrics.go +++ b/metrics.go @@ -9,6 +9,8 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" ) +const metricsPath = "/metrics" + type Metrics interface { // SubscriberConnected collects metrics about subscriber connections. SubscriberConnected(s *Subscriber) @@ -79,7 +81,7 @@ func (m *PrometheusMetrics) Register(r *mux.Router) { // Go-unrelated process metrics (memory usage, file descriptors, etc.). m.registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) - r.Handle("/metrics", promhttp.HandlerFor(m.registry.(*prometheus.Registry), promhttp.HandlerOpts{})).Methods(http.MethodGet) + r.Handle(metricsPath, promhttp.HandlerFor(m.registry.(*prometheus.Registry), promhttp.HandlerOpts{})).Methods(http.MethodGet) } func (m *PrometheusMetrics) SubscriberConnected(_ *Subscriber) { diff --git a/metrics_test.go b/metrics_test.go index f4564aae..ca183b34 100644 --- a/metrics_test.go +++ b/metrics_test.go @@ -75,7 +75,7 @@ func assertGaugeValue(t *testing.T, v float64, g prometheus.Gauge) { t.Fatal(err) } - assert.Equal(t, v, *metricOut.Gauge.Value) + assert.Equal(t, v, metricOut.GetGauge().GetValue()) //nolint:testifylint } func assertCounterValue(t *testing.T, v float64, c prometheus.Counter) { @@ -86,5 +86,5 @@ func assertCounterValue(t *testing.T, v float64, c prometheus.Counter) { t.Fatal(err) } - assert.Equal(t, v, *metricOut.Counter.Value) + assert.Equal(t, v, metricOut.GetCounter().GetValue()) // nolint:testifylint } diff --git a/publish_test.go b/publish_test.go index 399e0341..842f5d95 100644 --- a/publish_test.go +++ b/publish_test.go @@ -33,7 +33,7 @@ func TestPublishUnauthorizedJWT(t *testing.T) { hub := createDummy() req := httptest.NewRequest(http.MethodPost, defaultHubURL, nil) - req.Header.Add("Authorization", "Bearer "+createDummyUnauthorizedJWT()) + req.Header.Add("Authorization", bearerPrefix+createDummyUnauthorizedJWT()) w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -48,7 +48,7 @@ func TestPublishInvalidAlgJWT(t *testing.T) { hub := createDummy() req := httptest.NewRequest(http.MethodPost, defaultHubURL, nil) - req.Header.Add("Authorization", "Bearer "+createDummyNoneSignedJWT()) + req.Header.Add("Authorization", bearerPrefix+createDummyNoneSignedJWT()) w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -63,7 +63,7 @@ func TestPublishBadContentType(t *testing.T) { hub := createDummy() req := httptest.NewRequest(http.MethodPost, defaultHubURL, nil) - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"})) req.Header.Add("Content-Type", "text/plain; boundary=") w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -78,7 +78,7 @@ func TestPublishNoTopic(t *testing.T) { hub := createDummy() req := httptest.NewRequest(http.MethodPost, defaultHubURL, nil) - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"})) w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -99,7 +99,7 @@ func TestPublishInvalidRetry(t *testing.T) { req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"})) w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -121,7 +121,7 @@ func TestPublishNotAuthorizedTopicSelector(t *testing.T) { req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{"foo"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(hub, rolePublisher, []string{"foo"})) w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -140,7 +140,7 @@ func TestPublishEmptyTopicSelector(t *testing.T) { req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(hub, rolePublisher, []string{})) w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -159,7 +159,7 @@ func TestPublishLegacyAuthorization(t *testing.T) { req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(hub, rolePublisher, []string{})) w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -178,7 +178,7 @@ func TestPublishOK(t *testing.T) { s.SetTopics(topics, topics) s.Claims = &claims{Mercure: mercureClaim{Subscribe: topics}} - require.Nil(t, hub.transport.AddSubscriber(s)) + require.NoError(t, hub.transport.AddSubscriber(s)) var wg sync.WaitGroup wg.Add(1) @@ -201,7 +201,7 @@ func TestPublishOK(t *testing.T) { req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, s.SubscribedTopics)) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(hub, rolePublisher, s.SubscribedTopics)) w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -224,7 +224,7 @@ func TestPublishNoData(t *testing.T) { req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"})) w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -241,7 +241,7 @@ func TestPublishGenerateUUID(t *testing.T) { s := NewSubscriber("", zap.NewNop()) s.SetTopics([]string{"http://example.com/books/1"}, s.SubscribedTopics) - require.Nil(t, h.transport.AddSubscriber(s)) + require.NoError(t, h.transport.AddSubscriber(s)) var wg sync.WaitGroup wg.Add(1) @@ -251,7 +251,7 @@ func TestPublishGenerateUUID(t *testing.T) { require.NotNil(t, u) _, err := uuid.FromString(strings.TrimPrefix(u.ID, "urn:uuid:")) - assert.Nil(t, err) + require.NoError(t, err) }() form := url.Values{} @@ -260,7 +260,7 @@ func TestPublishGenerateUUID(t *testing.T) { req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(h, rolePublisher, []string{"*"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(h, rolePublisher, []string{"*"})) w := httptest.NewRecorder() h.PublishHandler(w, req) @@ -273,7 +273,7 @@ func TestPublishGenerateUUID(t *testing.T) { body := string(bodyBytes) _, err := uuid.FromString(strings.TrimPrefix(body, "urn:uuid:")) - assert.Nil(t, err) + require.NoError(t, err) wg.Wait() } @@ -296,7 +296,7 @@ func TestPublishWithErrorInTransport(t *testing.T) { req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, rolePublisher, []string{"foo", "http://example.com/books/1"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(hub, rolePublisher, []string{"foo", "http://example.com/books/1"})) w := httptest.NewRecorder() hub.PublishHandler(w, req) @@ -311,7 +311,7 @@ func TestPublishWithErrorInTransport(t *testing.T) { func FuzzPublish(f *testing.F) { hub := createDummy() - authorizationHeader := "Bearer " + createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"}) + authorizationHeader := bearerPrefix + createDummyAuthorizedJWT(hub, rolePublisher, []string{"*"}) testCases := [][]interface{}{ {"https://localhost/foo/bar", "baz", "", "", "", "", ""}, diff --git a/server_test.go b/server_test.go index 8c5fccd2..d8ec9c72 100644 --- a/server_test.go +++ b/server_test.go @@ -22,8 +22,11 @@ import ( ) const ( - testURL = "http://" + testAddr + defaultHubURL - testSecureURL = "https://" + testAddr + defaultHubURL + testURLscheme = "http://" + testURL = testURLscheme + testAddr + defaultHubURL + + testSecureURLScheme = "https://" + testSecureURL = testSecureURLScheme + testAddr + defaultHubURL ) func TestForwardedHeaders(t *testing.T) { @@ -46,10 +49,10 @@ func TestForwardedHeaders(t *testing.T) { req, _ := http.NewRequest(http.MethodPost, testURL, strings.NewReader(body.Encode())) req.Header.Add("X-Forwarded-For", "192.0.2.1") req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(h, rolePublisher, []string{"*"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(h, rolePublisher, []string{"*"})) resp2, err := client.Do(req) - require.Nil(t, err) + require.NoError(t, err) defer resp2.Body.Close() assert.Equal(t, 1, logs.FilterField(zap.String("remote_addr", "192.0.2.1")).Len()) @@ -97,7 +100,7 @@ func TestSecurityOptions(t *testing.T) { resp2.Body.Close() // Subscriptions - req, _ = http.NewRequest(http.MethodGet, testSecureURL+"/subscriptions", nil) + req, _ = http.NewRequest(http.MethodGet, testSecureURL+subscriptionsPath, nil) resp3, _ := client.Do(req) require.NotNil(t, resp3) assert.Equal(t, http.StatusUnauthorized, resp3.StatusCode) @@ -134,7 +137,7 @@ func TestSecurityOptionsWithCorsOrigin(t *testing.T) { req, _ := http.NewRequest(http.MethodOptions, testSecureURL, nil) - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(h, roleSubscriber, []string{})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(h, roleSubscriber, []string{})) req.Header.Add("Content-Type", "text/plain; boundary=") req.Header.Add("Origin", "https://subscriber.com") req.Header.Add("Host", "subscriber.com") @@ -162,15 +165,15 @@ func TestServe(t *testing.T) { var resp *http.Response client := http.Client{Timeout: 100 * time.Millisecond} for resp == nil { - resp, _ = client.Get("http://" + testAddr + "/") //nolint:bodyclose + resp, _ = client.Get(testURLscheme + testAddr + "/") //nolint:bodyclose } defer resp.Body.Close() hpBody, _ := io.ReadAll(resp.Body) assert.Contains(t, string(hpBody), "Mercure Hub") - respHealthz, err := client.Get("http://" + testAddr + "/healthz") - require.Nil(t, err) + respHealthz, err := client.Get(testURLscheme + testAddr + "/healthz") + require.NoError(t, err) defer respHealthz.Body.Close() healthzBody, _ := io.ReadAll(respHealthz.Body) assert.Contains(t, string(healthzBody), "ok") @@ -182,7 +185,7 @@ func TestServe(t *testing.T) { go func() { defer wgTested.Done() resp, err := client.Get(testURL + "?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1") - require.Nil(t, err) + require.NoError(t, err) wgConnected.Done() defer resp.Body.Close() @@ -194,7 +197,7 @@ func TestServe(t *testing.T) { go func() { defer wgTested.Done() resp, err := client.Get(testURL + "?topic=http%3A%2F%2Fexample.com%2Falt%2F1") - require.Nil(t, err) + require.NoError(t, err) wgConnected.Done() defer resp.Body.Close() @@ -208,10 +211,10 @@ func TestServe(t *testing.T) { body := url.Values{"topic": {"http://example.com/foo/1", "http://example.com/alt/1"}, "data": {"hello"}, "id": {"first"}} req, _ := http.NewRequest(http.MethodPost, testURL, strings.NewReader(body.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(h, rolePublisher, []string{"*"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(h, rolePublisher, []string{"*"})) resp2, err := client.Do(req) - require.Nil(t, err) + require.NoError(t, err) defer resp2.Body.Close() h.server.Shutdown(context.Background()) @@ -220,20 +223,24 @@ func TestServe(t *testing.T) { func TestClientClosesThenReconnects(t *testing.T) { l := zap.NewNop() - u, _ := url.Parse("bolt://test.db") - bt, _ := NewTransport(u, l) + u, err := url.Parse("bolt://test.db") + require.NoError(t, err) + + bt, err := NewTransport(u, l) + require.NoError(t, err) + defer os.Remove("test.db") + h := createAnonymousDummy(WithLogger(l), WithTransport(bt)) transport := h.transport.(*BoltTransport) - defer os.Remove("test.db") go h.Serve() // loop until the web server is ready var resp *http.Response client := http.Client{Timeout: 10 * time.Second} for resp == nil { - resp, _ = client.Get("http://" + testAddr + "/") //nolint:bodyclose + resp, _ = client.Get(testURLscheme + testAddr) //nolint:bodyclose } - resp.Body.Close() + require.NoError(t, resp.Body.Close()) var wg sync.WaitGroup @@ -242,7 +249,7 @@ func TestClientClosesThenReconnects(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, testURL+"?topic=http%3A%2F%2Fexample.com%2Ffoo%2F1", nil) req = req.WithContext(cx) resp, err := http.DefaultClient.Do(req) - require.Nil(t, err) + require.NoError(t, err) var receivedBody strings.Builder buf := make([]byte, 1024) @@ -260,15 +267,15 @@ func TestClientClosesThenReconnects(t *testing.T) { } } - resp.Body.Close() + require.NoError(t, resp.Body.Close()) wg.Done() } publish := func(data string, waitForSubscribers int) { for { - transport.Lock() + transport.RLock() l := transport.subscribers.Len() - transport.Unlock() + transport.RUnlock() if l >= waitForSubscribers { break } @@ -276,14 +283,14 @@ func TestClientClosesThenReconnects(t *testing.T) { body := url.Values{"topic": {"http://example.com/foo/1"}, "data": {data}, "id": {data}} req, err := http.NewRequest(http.MethodPost, testURL, strings.NewReader(body.Encode())) - require.Nil(t, err) + require.NoError(t, err) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(h, rolePublisher, []string{"*"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(h, rolePublisher, []string{"*"})) resp, err := client.Do(req) - require.Nil(t, err) + require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) - resp.Body.Close() + require.NoError(t, resp.Body.Close()) wg.Done() } @@ -343,7 +350,7 @@ func TestServeAcme(t *testing.T) { resp.Body.Close() resp, err := client.Get("http://0.0.0.0:8080/.well-known/acme-challenge/does-not-exists") - assert.Nil(t, err) + require.NoError(t, err) require.NotNil(t, resp) defer resp.Body.Close() @@ -355,12 +362,12 @@ func TestMetricsAccess(t *testing.T) { server := newTestServer(t) defer server.shutdown() - resp, err := server.client.Get("http://" + testMetricsAddr + "/metrics") - require.Nil(t, err) + resp, err := server.client.Get(testURLscheme + testMetricsAddr + metricsPath) + require.NoError(t, err) defer resp.Body.Close() - resp, err = server.client.Get("http://" + testMetricsAddr + "/healthz") - require.Nil(t, err) + resp, err = server.client.Get(testURLscheme + testMetricsAddr + "/healthz") + require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, 200, resp.StatusCode) @@ -391,12 +398,12 @@ func TestMetricsVersionIsAccessible(t *testing.T) { server := newTestServer(t) defer server.shutdown() - resp, err := server.client.Get("http://" + testMetricsAddr + "/metrics") - assert.Nil(t, err) + resp, err := server.client.Get(testURLscheme + testMetricsAddr + metricsPath) + require.NoError(t, err) defer resp.Body.Close() b, err := io.ReadAll(resp.Body) - assert.Nil(t, err) + require.NoError(t, err) pattern := "mercure_version_info{architecture=\".+\",built_at=\".*\",commit=\".*\",go_version=\".+\",os=\".+\",version=\"dev\"} 1" assert.Regexp(t, regexp.MustCompile(pattern), string(b)) @@ -424,7 +431,7 @@ func newTestServer(t *testing.T) testServer { var resp *http.Response client := http.Client{Timeout: 100 * time.Millisecond} for resp == nil { - resp, _ = client.Get("http://" + testAddr + "/") //nolint:bodyclose + resp, _ = client.Get(testURLscheme + testAddr + "/") //nolint:bodyclose } defer resp.Body.Close() @@ -455,7 +462,7 @@ func (s *testServer) newSubscriber(topic string, keepAlive bool) { go func() { defer s.wgTested.Done() resp, err := s.client.Get(testURL + "?topic=" + url.QueryEscape(topic)) - require.Nil(s.t, err) + require.NoError(s.t, err) defer resp.Body.Close() s.wgConnected.Done() @@ -468,10 +475,10 @@ func (s *testServer) newSubscriber(topic string, keepAlive bool) { func (s *testServer) publish(body url.Values) { req, _ := http.NewRequest(http.MethodPost, testURL, strings.NewReader(body.Encode())) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(s.h, rolePublisher, []string{"*"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(s.h, rolePublisher, []string{"*"})) resp, err := s.client.Do(req) - require.Nil(s.t, err) + require.NoError(s.t, err) defer resp.Body.Close() } @@ -480,12 +487,12 @@ func (s *testServer) waitSubscribers() { } func (s *testServer) assertMetric(metric string) { - resp, err := s.client.Get("http://" + testMetricsAddr + "/metrics") - assert.Nil(s.t, err) + resp, err := s.client.Get(testURLscheme + testMetricsAddr + metricsPath) + require.NoError(s.t, err) defer resp.Body.Close() b, err := io.ReadAll(resp.Body) - assert.Nil(s.t, err) + require.NoError(s.t, err) assert.Contains(s.t, string(b), metric) } diff --git a/spec/mercure.md b/spec/mercure.md index 00bf2528..705d53c6 100644 --- a/spec/mercure.md +++ b/spec/mercure.md @@ -30,7 +30,7 @@ organization = "Les-Tilleuls.coop" Mercure provides a common publish-subscribe mechanism for public and private web resources. Mercure enables the pushing of any web content to web browsers and other clients in a fast, reliable and battery-efficient way. It is especially useful for publishing real-time updates -of resources served through websites and web APIs to web and mobile apps. +of resources served through sites and web APIs to web and mobile apps. Subscription requests are relayed through hubs, which validate and verify the request. When new or updated content becomes available, hubs check if subscribers are authorized to receive it @@ -52,7 +52,7 @@ interpreted as described in [@!RFC2119]. * Topic selector: An expression matching one or several topics. * Publisher: An owner of a topic. Notifies the hub when the topic feed has been updated. As in almost all pubsub systems, the publisher is unaware of the subscribers, if any. Other pubsub - systems might call the publisher the "source". Typically a website or a web API, but can also be + systems might call the publisher the "source". Typically a site or a web API, but can also be a web browser. * Subscriber: A client application that subscribes to real-time updates of topics using topic selectors. Typically a web or a mobile application, but can also be a server. @@ -294,12 +294,12 @@ The request **MUST** be encoded using the `application/x-www-form-urlencoded` fo not authorized to receive it. See (#authorization). It is recommended to set the value to `on` but it **CAN** contain any value including an empty string. * `id` (optional): the topic's revision identifier: it will be used as the SSE's `id` property. - The provided id **MUST NOT** start with the `#` character. The provided id **SHOULD** be a valid + The provided ID **MUST NOT** start with the `#` character. The provided ID **SHOULD** be a valid IRI. If omitted, the hub **MUST** generate a valid IRI [@!RFC3987]. An UUID [@RFC4122] or a [DID](https://www.w3.org/TR/did-core/) **MAY** be used. Alternatively the hub **MAY** generate a relative URI composed of a fragment (starting with `#`). This is convenient to return an offset - or a sequence that is unique for this hub. Even if provided, the hub **MAY** ignore the id - provided by the client and generate its own id. + or a sequence that is unique for this hub. Even if provided, the hub **MAY** ignore the ID + provided by the client and generate its own ID. * `type` (optional): the SSE's `event` property (a specific event type). * `retry` (optional): the SSE's `retry` property (the reconnection time). @@ -493,17 +493,17 @@ UUID [@RFC4122] or a [DID](https://www.w3.org/TR/did-core/) **MAY** be used. According to the server-sent events specification, in case of connection lost the subscriber will try to automatically re-connect. During the -re-connection, the subscriber **MUST** send the last received event id in a +re-connection, the subscriber **MUST** send the last received event ID in a [Last-Event-ID](https://html.spec.whatwg.org/multipage/iana.html#last-event-id) HTTP header. In order to fetch any update dispatched between the initial resource generation by the publisher and -the connection to the hub, the subscriber **MUST** send the event id provided during the discovery +the connection to the hub, the subscriber **MUST** send the event ID provided during the discovery as a `Last-Event-ID` header or a `lastEventID` query parameter. See (#discovery). `EventSource` implementations may not allow to set HTTP headers during the first connection (before a reconnection) and implementations in web browsers don't allow to set it. -To work around this problem, the hub **MUST** also allow to pass the last event id in a query +To work around this problem, the hub **MUST** also allow to pass the last event ID in a query parameter named `lastEventID`. If both the `Last-Event-ID` HTTP header and the `lastEventID` query parameter are present, @@ -519,7 +519,7 @@ subscribed topics. According to its own policy, the hub **MAY** or **MAY NOT** f The hub **MAY** discard some events for operational reasons. When the request contains a `Last-Event-ID` HTTP header or a `lastEventID` query parameter the hub **MUST** set a `Last-Event-ID` header on the HTTP response. -The value of the `Last-Event-ID` response header **MUST** be the id of the event +The value of the `Last-Event-ID` response header **MUST** be the ID of the event preceding the first one sent to the subscriber, or the reserved value `earliest` if there is no preceding event (it happens when the hub history is empty, when the subscriber requests the earliest event or when the subscriber requests an event that doesn't exist). diff --git a/subscribe_test.go b/subscribe_test.go index 7c4ae6b5..d2962e37 100644 --- a/subscribe_test.go +++ b/subscribe_test.go @@ -583,7 +583,7 @@ func TestSubscribeAll(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=http://example.com/reviews/{id}", nil).WithContext(ctx) - req.Header.Add("Authorization", "Bearer "+createDummyAuthorizedJWT(hub, roleSubscriber, []string{"random", "*"})) + req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWT(hub, roleSubscriber, []string{"random", "*"})) w := &responseTester{ expectedStatusCode: http.StatusOK, @@ -928,10 +928,10 @@ func TestSubscribeExpires(t *testing.T) { } jwt, err := token.SignedString(hub.subscriberJWT.key) - require.Nil(t, err) + require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, defaultHubURL+"?topic=foo", nil) - req.Header.Add("Authorization", "Bearer "+jwt) + req.Header.Add("Authorization", bearerPrefix+jwt) w := newSubscribeRecorder() hub.SubscribeHandler(w, req) diff --git a/subscription.go b/subscription.go index ddcddcb3..65127e4a 100644 --- a/subscription.go +++ b/subscription.go @@ -31,9 +31,10 @@ type subscriptionCollection struct { } const ( - subscriptionURL = defaultHubURL + "/subscriptions/{topic}/{subscriber}" - subscriptionsForTopicURL = defaultHubURL + "/subscriptions/{topic}" - subscriptionsURL = defaultHubURL + "/subscriptions" + subscriptionsPath = "/subscriptions" + subscriptionURL = defaultHubURL + subscriptionsPath + "/{topic}/{subscriber}" + subscriptionsForTopicURL = defaultHubURL + subscriptionsPath + "/{topic}" + subscriptionsURL = defaultHubURL + subscriptionsPath ) func (h *Hub) SubscriptionsHandler(w http.ResponseWriter, r *http.Request) { diff --git a/subscription_test.go b/subscription_test.go index 65d5a311..acd936e0 100644 --- a/subscription_test.go +++ b/subscription_test.go @@ -31,7 +31,7 @@ func TestSubscriptionsHandlerAccessDenied(t *testing.T) { assert.Equal(t, http.StatusUnauthorized, res.StatusCode) res.Body.Close() - req = httptest.NewRequest(http.MethodGet, defaultHubURL+"/subscriptions/bar", nil) + req = httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/bar", nil) req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo{/subscriber}"})}) w = httptest.NewRecorder() hub.SubscriptionsHandler(w, req) @@ -43,14 +43,14 @@ func TestSubscriptionsHandlerAccessDenied(t *testing.T) { func TestSubscriptionHandlerAccessDenied(t *testing.T) { hub := createDummy() - req := httptest.NewRequest(http.MethodGet, defaultHubURL+"/subscriptions/bar/baz", nil) + req := httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/bar/baz", nil) w := httptest.NewRecorder() hub.SubscriptionHandler(w, req) res := w.Result() assert.Equal(t, http.StatusUnauthorized, res.StatusCode) res.Body.Close() - req = httptest.NewRequest(http.MethodGet, defaultHubURL+"/subscriptions/bar/baz", nil) + req = httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/bar/baz", nil) req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo{/subscriber}"})}) w = httptest.NewRecorder() hub.SubscriptionHandler(w, req) @@ -62,7 +62,7 @@ func TestSubscriptionHandlerAccessDenied(t *testing.T) { func TestSubscriptionHandlersETag(t *testing.T) { hub := createDummy() - req := httptest.NewRequest(http.MethodGet, defaultHubURL+"/subscriptions", nil) + req := httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath, nil) req.Header.Add("If-None-Match", EarliestLastEventID) req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions"})}) w := httptest.NewRecorder() @@ -71,7 +71,7 @@ func TestSubscriptionHandlersETag(t *testing.T) { assert.Equal(t, http.StatusNotModified, res.StatusCode) res.Body.Close() - req = httptest.NewRequest(http.MethodGet, defaultHubURL+"/subscriptions/foo/bar", nil) + req = httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/foo/bar", nil) req.Header.Add("If-None-Match", EarliestLastEventID) req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions/foo/bar"})}) w = httptest.NewRecorder() @@ -86,13 +86,13 @@ func TestSubscriptionsHandler(t *testing.T) { s1 := NewSubscriber("", zap.NewNop()) s1.SetTopics([]string{"http://example.com/foo"}, nil) - require.Nil(t, hub.transport.AddSubscriber(s1)) + require.NoError(t, hub.transport.AddSubscriber(s1)) s2 := NewSubscriber("", zap.NewNop()) s2.SetTopics([]string{"http://example.com/bar"}, nil) - require.Nil(t, hub.transport.AddSubscriber(s2)) + require.NoError(t, hub.transport.AddSubscriber(s2)) - req := httptest.NewRequest(http.MethodGet, defaultHubURL+"/subscriptions", nil) + req := httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath, nil) req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions"})}) w := httptest.NewRecorder() hub.SubscriptionsHandler(w, req) @@ -125,11 +125,11 @@ func TestSubscriptionsHandlerForTopic(t *testing.T) { s1 := NewSubscriber("", zap.NewNop()) s1.SetTopics([]string{"http://example.com/foo"}, nil) - require.Nil(t, hub.transport.AddSubscriber(s1)) + require.NoError(t, hub.transport.AddSubscriber(s1)) s2 := NewSubscriber("", zap.NewNop()) s2.SetTopics([]string{"http://example.com/bar"}, nil) - require.Nil(t, hub.transport.AddSubscriber(s2)) + require.NoError(t, hub.transport.AddSubscriber(s2)) escapedBarTopic := url.QueryEscape("http://example.com/bar") @@ -138,7 +138,7 @@ func TestSubscriptionsHandlerForTopic(t *testing.T) { router.SkipClean(true) router.HandleFunc(subscriptionsForTopicURL, hub.SubscriptionsHandler) - req := httptest.NewRequest(http.MethodGet, defaultHubURL+"/subscriptions/"+s2.EscapedTopics[0], nil) + req := httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/"+s2.EscapedTopics[0], nil) req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions/" + s2.EscapedTopics[0]})}) w := httptest.NewRecorder() hub.SubscriptionsHandler(w, req) @@ -150,7 +150,7 @@ func TestSubscriptionsHandlerForTopic(t *testing.T) { json.Unmarshal(w.Body.Bytes(), &subscriptions) assert.Equal(t, "https://mercure.rocks/", subscriptions.Context) - assert.Equal(t, defaultHubURL+"/subscriptions/"+escapedBarTopic, subscriptions.ID) + assert.Equal(t, defaultHubURL+subscriptionsPath+"/"+escapedBarTopic, subscriptions.ID) assert.Equal(t, "Subscriptions", subscriptions.Type) lastEventID, subscribers, _ := hub.transport.(TransportSubscribers).GetSubscribers() @@ -170,18 +170,18 @@ func TestSubscriptionHandler(t *testing.T) { otherS := NewSubscriber("", zap.NewNop()) otherS.SetTopics([]string{"http://example.com/other"}, nil) - require.Nil(t, hub.transport.AddSubscriber(otherS)) + require.NoError(t, hub.transport.AddSubscriber(otherS)) s := NewSubscriber("", zap.NewNop()) s.SetTopics([]string{"http://example.com/other", "http://example.com/{foo}"}, nil) - require.Nil(t, hub.transport.AddSubscriber(s)) + require.NoError(t, hub.transport.AddSubscriber(s)) router := mux.NewRouter() router.UseEncodedPath() router.SkipClean(true) router.HandleFunc(subscriptionURL, hub.SubscriptionHandler) - req := httptest.NewRequest(http.MethodGet, defaultHubURL+"/subscriptions/"+s.EscapedTopics[1]+"/"+s.EscapedID, nil) + req := httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/"+s.EscapedTopics[1]+"/"+s.EscapedID, nil) req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions{/topic}{/subscriber}"})}) w := httptest.NewRecorder() router.ServeHTTP(w, req) @@ -195,7 +195,7 @@ func TestSubscriptionHandler(t *testing.T) { expectedSub.LastEventID, _, _ = hub.transport.(TransportSubscribers).GetSubscribers() assert.Equal(t, expectedSub, subscription) - req = httptest.NewRequest(http.MethodGet, defaultHubURL+"/subscriptions/notexist/"+s.EscapedID, nil) + req = httptest.NewRequest(http.MethodGet, defaultHubURL+subscriptionsPath+"/notexist/"+s.EscapedID, nil) req.AddCookie(&http.Cookie{Name: "mercureAuthorization", Value: createDummyAuthorizedJWT(hub, roleSubscriber, []string{"/.well-known/mercure/subscriptions{/topic}{/subscriber}"})}) w = httptest.NewRecorder() router.ServeHTTP(w, req) diff --git a/tests/use-go-deadlock.sh b/tests/use-go-deadlock.sh index 60c23a54..d4e11c1c 100755 --- a/tests/use-go-deadlock.sh +++ b/tests/use-go-deadlock.sh @@ -14,5 +14,5 @@ find . -name "*.go" -exec sed "${args[@]}" -e 's#sync.RWMutex#deadlock.RWMutex#' find . -name "*.go" -exec sed "${args[@]}" -e 's#sync.Mutex#deadlock.Mutex#' {} {} \; goimports -w . go get github.com/sasha-s/go-deadlock/...@master -cd caddy +cd caddy || exit go get github.com/sasha-s/go-deadlock/...@master diff --git a/topic_selector.go b/topic_selector.go index 98c549d9..09f74eb6 100644 --- a/topic_selector.go +++ b/topic_selector.go @@ -8,8 +8,8 @@ import ( ) type TopicSelectorStoreCache interface { - Get(interface{}) (interface{}, bool) - Set(interface{}, interface{}, int64) bool + Get(key interface{}) (interface{}, bool) + Set(key interface{}, value interface{}, n int64) bool } // TopicSelectorStore caches compiled templates to improve memory and CPU usage. diff --git a/topic_selector_lru_test.go b/topic_selector_lru_test.go index f2d8d52d..d0cb158a 100644 --- a/topic_selector_lru_test.go +++ b/topic_selector_lru_test.go @@ -9,7 +9,7 @@ import ( func TestMatchLRU(t *testing.T) { tss, err := NewTopicSelectorStoreLRU(DefaultTopicSelectorStoreLRUMaxEntriesPerShard, DefaultTopicSelectorStoreLRUMaxEntriesPerShard) - require.Nil(t, err) + require.NoError(t, err) assert.False(t, tss.match("foo", "bar")) diff --git a/update_test.go b/update_test.go index c34722e8..65f74cc8 100644 --- a/update_test.go +++ b/update_test.go @@ -6,6 +6,7 @@ import ( "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap" ) @@ -23,7 +24,7 @@ func TestAssignUUID(t *testing.T) { assert.True(t, strings.HasPrefix(u.ID, "urn:uuid:")) _, err := uuid.FromString(strings.TrimPrefix(u.ID, "urn:uuid:")) - assert.Nil(t, err) + require.NoError(t, err) } func TestLogUpdate(t *testing.T) {