Skip to content

Commit

Permalink
Mercure component (#353)
Browse files Browse the repository at this point in the history
  • Loading branch information
Blacksmoke16 authored Mar 4, 2024
2 parents c0ef2eb + 2b368c2 commit d71344e
Show file tree
Hide file tree
Showing 38 changed files with 965 additions and 0 deletions.
1 change: 1 addition & 0 deletions scripts/diff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ diff dependency-injection
diff dotenv
diff event-dispatcher
diff image-size
diff mercure
diff framework
diff negotiation
diff routing
Expand Down
1 change: 1 addition & 0 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ function tag()
componentNameMap[dependency-injection]="Dependency Injection"
componentNameMap[dotenv]="Dotenv"
componentNameMap[event-dispatcher]="Event Dispatcher"
componentNameMap[mercure]=Mercure
componentNameMap[framework]=Framework
componentNameMap[image-size]="Image Size"
componentNameMap[negotiation]=Negotiation
Expand Down
1 change: 1 addition & 0 deletions scripts/sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ maybeSync "src/components/dotenv" dotenv https://github.com/athena-framework/dot
maybeSync "src/components/event_dispatcher" event-dispatcher https://github.com/athena-framework/event-dispatcher.git
maybeSync "src/components/image_size" image-size https://github.com/athena-framework/image-size.git
maybeSync "src/components/framework" framework https://github.com/athena-framework/framework.git
maybeSync "src/components/mercure" mercure https://github.com/athena-framework/mercure.git
maybeSync "src/components/negotiation" negotiation https://github.com/athena-framework/negotiation.git
maybeSync "src/components/routing" routing https://github.com/athena-framework/routing.git
maybeSync "src/components/serializer" serializer https://github.com/athena-framework/serializer.git
Expand Down
2 changes: 2 additions & 0 deletions shard.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencies:
path: ./src/components/event_dispatcher
athena-image_size:
path: ./src/components/image_size
athena-mercure:
path: ./src/components/mercure
athena-negotiation:
path: ./src/components/negotiation
athena-routing:
Expand Down
2 changes: 2 additions & 0 deletions shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ dependencies:
github: athena-framework/event-dispatcher
athena-image_size:
github: athena-framework/image-size
athena-mercure:
github: athena-framework/mercure
athena-negotiation:
github: athena-framework/negotiation
athena-routing:
Expand Down
9 changes: 9 additions & 0 deletions src/components/mercure/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*.cr]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
9 changes: 9 additions & 0 deletions src/components/mercure/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/docs/
/lib/
/bin/
/.shards/
*.dwarf

# Libraries don't need dependency lock
# Dependencies will be locked in applications that use them
/shard.lock
7 changes: 7 additions & 0 deletions src/components/mercure/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

## [0.1.0] - YYYY-MM-DD

_Initial release._

[0.1.0]: https://github.com/athena-framework/mercure/releases/tag/v0.1.0
3 changes: 3 additions & 0 deletions src/components/mercure/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Contributing

This repository is a read-only mirror. Please refer the [main Athena repository](https://github.com/athena-framework/athena/blob/master/CONTRIBUTING.md) on how to start contributing.
21 changes: 21 additions & 0 deletions src/components/mercure/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2024 George Dietrich

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
29 changes: 29 additions & 0 deletions src/components/mercure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Mercure

[![Common Changelog](https://common-changelog.org/badge.svg)](https://common-changelog.org)
[![CI](https://github.com/athena-framework/athena/workflows/CI/badge.svg)](https://github.com/athena-framework/athena/actions/workflows/ci.yml)
[![Latest release](https://img.shields.io/github/release/athena-framework/mercure.svg)](https://github.com/athena-framework/mercure/releases)

Allows easily pushing updates to web browsers and other HTTP clients using the Mercure protocol.

## Installation

1. Add the dependency to your `shard.yml`:

```yaml
dependencies:
athena-mercure:
github: athena-framework/mercure
version: ~> 0.1.0
```
2. Run `shards install`

## Documentation

If using the component on its own, checkout the [API documentation](https://athenaframework.org/Mercure).
If using the component as part of Athena, also checkout the [external documentation](https://athenaframework.org/components/mercure).

## Contributing

Read the general [Contributing Guide](./CONTRIBUTING.md) for information on how to get started.
22 changes: 22 additions & 0 deletions src/components/mercure/shard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: athena-mercure

version: 0.1.0

crystal: '>= 0.36.0'

license: MIT

repository: https://github.com/athena-framework/mercure

documentation: https://athenaframework.org/Mercure

description: |
Allows easily pushing updates to web browsers and other HTTP clients using the Mercure protocol.
authors:
- George Dietrich <[email protected]>

dependencies:
jwt:
github: crystal-community/jwt
version: ~> 1.6
187 changes: 187 additions & 0 deletions src/components/mercure/spec/authorization_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
require "./spec_helper"

# @[ASPEC::TestCase::Focus]
struct AuthorizationTest < ASPEC::TestCase
def test_jwt_lifetime : Nil
registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(
"https://example.com/.well-known/mercure",
AMC::TokenProvider::Static.new("JWT"),
token_factory: AMC::TokenFactory::JWT.new("looooooooooooongenoughtestsecret", jwt_lifetime: 4000)
) { "ID" })

authorization = AMC::Authorization.new registry
cookie = authorization.create_cookie HTTP::Request.new("GET", "https://example.com", headers: HTTP::Headers{"host" => "example.com"})

payload, _ = JWT.decode(cookie.value, verify: false, validate: false)
payload["exp"].as_i?.should be_a Int32
end

def test_set_cookie_zero_expiration : Nil
token_factory = AMC::Spec::AssertingTokenFactory.new(
"JWT",
["foo"],
["bar"],
{"x-foo" => "baz"},
)

registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(
"https://example.com/.well-known/mercure",
AMC::TokenProvider::Static.new("JWT"),
token_factory: token_factory
) { "ID" })

request = HTTP::Request.new("GET", "https://example.com", headers: HTTP::Headers{"host" => "example.com"})
response = HTTP::Server::Response.new IO::Memory.new

authorization = AMC::Authorization.new registry, Time::Span.zero, :lax
authorization.set_cookie request, response, ["foo"], ["bar"], {"x-foo" => "baz"}
token_factory.called?.should be_true

cookie = response.cookies.first
cookie.max_age.should eq Time::Span.zero
cookie.value.should_not be_empty
cookie.samesite.try &.lax?.should be_true
end

def test_set_cookie_default_expiration : Nil
token_factory = AMC::Spec::AssertingTokenFactory.new(
"JWT",
["foo"],
["bar"],
{"x-foo" => "baz"},
)

registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(
"https://example.com/.well-known/mercure",
AMC::TokenProvider::Static.new("JWT"),
token_factory: token_factory
) { "ID" })

request = HTTP::Request.new("GET", "https://example.com", headers: HTTP::Headers{"host" => "example.com"})
response = HTTP::Server::Response.new IO::Memory.new

authorization = AMC::Authorization.new registry, cookie_samesite: :lax
authorization.set_cookie request, response, ["foo"], ["bar"], {"x-foo" => "baz"}
token_factory.called?.should be_true

cookie = response.cookies.first
cookie.max_age.should eq 1.hour
cookie.value.should_not be_nil
cookie.samesite.try &.lax?.should be_true
end

def test_clear_cookie : Nil
token_factory = AMC::Spec::AssertingTokenFactory.new("JWT")

registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(
"https://example.com/.well-known/mercure",
AMC::TokenProvider::Static.new("JWT"),
token_factory: token_factory
) { "ID" })

request = HTTP::Request.new("GET", "https://example.com", headers: HTTP::Headers{"host" => "example.com"})
response = HTTP::Server::Response.new IO::Memory.new

authorization = AMC::Authorization.new registry
authorization.clear_cookie request, response

cookie = response.cookies.first
cookie.value.should be_empty
cookie.max_age.should eq 1.second
end

@[DataProvider("applicable_cookie_domains")]
def test_applicable_cookie_domains(expected : String?, hub_url : String, request_url : String) : Nil
registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(
hub_url,
AMC::TokenProvider::Static.new("JWT"),
token_factory: AMC::TokenFactory::JWT.new("looooooooooooongenoughtestsecret", jwt_lifetime: 4000)
) { "ID" })

uri = URI.parse request_url
request = HTTP::Request.new("GET", uri.path, headers: HTTP::Headers{"host" => uri.hostname || ""})

authorization = AMC::Authorization.new registry

cookie = authorization.create_cookie request
cookie.domain.should eq expected
end

def applicable_cookie_domains : Tuple
{
{".example.com", "https://foo.bar.baz.example.com", "https://foo.bar.baz.qux.example.com"},
{".foo.bar.baz.example.com", "https://mercure.foo.bar.baz.example.com", "https://app.foo.bar.baz.example.com"},
{"example.com", "https://demo.example.com", "https://example.com"},
{".example.com", "https://mercure.example.com", "https://app.example.com"},
{".example.com", "https://example.com/.well-known/mercure", "https://app.example.com"},
{nil, "https://example.com/.well-known/mercure", "https://example.com"},
}
end

@[DataProvider("nonapplicable_cookie_domains")]
def test_nonapplicable_cookie_domains(hub_url : String, request_url : String) : Nil
registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(
hub_url,
AMC::TokenProvider::Static.new("JWT"),
token_factory: AMC::TokenFactory::JWT.new("looooooooooooongenoughtestsecret", jwt_lifetime: 4000)
) { "ID" })

uri = URI.parse request_url
request = HTTP::Request.new("GET", uri.path, headers: HTTP::Headers{"host" => uri.hostname || ""})

authorization = AMC::Authorization.new registry

expect_raises AMC::Exceptions::InvalidArgument, "Unable to create authorization cookie for a hub on the different second-level domain" do
authorization.create_cookie request
end
end

def nonapplicable_cookie_domains : Tuple
{
{"https://demo.mercure.com", "https://example.com"},
{"https://mercure.internal.com", "https://external.com"},
}
end

def test_set_multiple_cookies : Nil
registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(
"https://example.com/.well-known/mercure",
AMC::TokenProvider::Static.new("JWT"),
token_factory: AMC::TokenFactory::JWT.new("looooooooooooongenoughtestsecret", jwt_lifetime: 4000)
) { "ID" })

request = HTTP::Request.new("GET", "https://example.com", headers: HTTP::Headers{"host" => "example.com"})
response = HTTP::Server::Response.new IO::Memory.new

authorization = AMC::Authorization.new registry

expect_raises AMC::Exceptions::Runtime, "The 'mercureAuthorization' cookie for the 'default hub' has already been set. You cannot set it two times during the same request." do
authorization.set_cookie request, response
authorization.clear_cookie request, response
end
end

def test_nil_cookie_topics : Nil
token_factory = AMC::Spec::AssertingTokenFactory.new(
"JWT",
nil,
nil,
{"x-foo" => "baz"},
)

registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(
"https://example.com/.well-known/mercure",
AMC::TokenProvider::Static.new("JWT"),
token_factory: token_factory
) { "ID" })

request = HTTP::Request.new("GET", "https://example.com", headers: HTTP::Headers{"host" => "example.com"})
response = HTTP::Server::Response.new IO::Memory.new

authorization = AMC::Authorization.new registry
authorization.set_cookie request, response, nil, nil, {"x-foo" => "baz"}

cookie = response.cookies.first
cookie.value.should_not be_empty
end
end
35 changes: 35 additions & 0 deletions src/components/mercure/spec/discovery_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require "./spec_helper"

describe AMC::Discovery do
it "preflight request" do
registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(
"https://example.com/.well-known/mercure",
AMC::TokenProvider::Static.new("JWT"),
token_factory: AMC::TokenFactory::JWT.new("looooooooooooongenoughtestsecret", jwt_lifetime: 4000)
) { "ID" })

request = HTTP::Request.new("OPTIONS", "/", headers: HTTP::Headers{"access-control-request-method" => "GET"})
response = HTTP::Server::Response.new IO::Memory.new

discovery = AMC::Discovery.new registry
discovery.add_link request, response

response.headers["link"]?.should be_nil
end

it "non-preflight request" do
registry = AMC::Hub::Registry.new(AMC::Spec::MockHub.new(
"https://example.com/.well-known/mercure",
AMC::TokenProvider::Static.new("JWT"),
token_factory: AMC::TokenFactory::JWT.new("looooooooooooongenoughtestsecret", jwt_lifetime: 4000)
) { "ID" })

request = HTTP::Request.new("POST", "/")
response = HTTP::Server::Response.new IO::Memory.new

discovery = AMC::Discovery.new registry
discovery.add_link request, response

response.headers.get("link").should eq ["<https://example.com/.well-known/mercure>; rel=\"mercure\""]
end
end
Loading

0 comments on commit d71344e

Please sign in to comment.