├── .ruby-version
├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ └── publish.yml
├── .devcontainer
├── setup.sh
└── devcontainer.json
├── spec
├── support
│ ├── webmock.rb
│ ├── fixtures
│ │ ├── example_com.html
│ │ ├── example_com_link_element_empty_href.html
│ │ ├── example_com_link_element_relative_url.html
│ │ ├── example_com_link_element_query_string.html
│ │ ├── example_com_multiple_endpoints.html
│ │ ├── example_com_link_element_relative_path.html
│ │ ├── example_com_link_element_multiple_rel_values.html
│ │ ├── example_com_link_element_absolute_url.html
│ │ ├── example_com_link_element_no_href.html
│ │ ├── example_com_link_element_fragment.html
│ │ ├── example_com_link_element_exact_match.html
│ │ └── example_com_link_element_html_comment.html
│ ├── fixture_helpers.rb
│ └── webmention_rocks.rb
├── lib
│ └── indieweb
│ │ ├── endpoints
│ │ ├── client_spec.rb
│ │ ├── client_endpoints_spec.rb
│ │ └── client_response_spec.rb
│ │ └── endpoints_get_spec.rb
└── spec_helper.rb
├── Rakefile
├── .irbrc
├── .editorconfig
├── bin
├── rspec
├── rubocop
└── console
├── CODE_OF_CONDUCT.md
├── lib
└── indieweb
│ ├── endpoints
│ ├── exceptions.rb
│ ├── client.rb
│ └── parser.rb
│ └── endpoints.rb
├── .rubocop.yml
├── .simplecov
├── Gemfile
├── .git-blame-ignore-revs
├── .gitignore
├── LICENSE
├── indieweb-endpoints.gemspec
├── CONTRIBUTING.md
├── CHANGELOG.md
└── README.md
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.4.6
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * jgarber623
2 |
--------------------------------------------------------------------------------
/.devcontainer/setup.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 | IFS=$'\n\t'
5 | set -vx
6 |
7 | bundle install
8 |
--------------------------------------------------------------------------------
/spec/support/webmock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | WebMock.disable_net_connect!(allow: ["webmention.rocks"])
4 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/gem_tasks"
4 |
5 | require "rspec/core/rake_task"
6 |
7 | RSpec::Core::RakeTask.new
8 |
9 | task default: :spec
10 |
--------------------------------------------------------------------------------
/.irbrc:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "irb/completion"
4 |
5 | IRB.conf[:HISTORY_FILE] = ".irb_history"
6 | IRB.conf[:USE_AUTOCOMPLETE] = false
7 | IRB.conf[:USE_PAGER] = false
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_size = 2
8 | indent_style = space
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "monthly"
8 | assignees:
9 | - "jgarber623"
10 |
--------------------------------------------------------------------------------
/bin/rspec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "rubygems"
5 | require "bundler/setup"
6 |
7 | ARGV.unshift("--require", "spec_helper")
8 |
9 | load Gem.bin_path("rspec-core", "rspec")
10 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | By contributing to and participating in the development of indieweb-endpoints-ruby, you acknowledge that you have read and agree to the [IndieWeb Code of Conduct](https://indieweb.org/code-of-conduct).
4 |
--------------------------------------------------------------------------------
/bin/rubocop:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "rubygems"
5 | require "bundler/setup"
6 |
7 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
8 |
9 | load Gem.bin_path("rubocop", "rubocop")
10 |
--------------------------------------------------------------------------------
/spec/support/fixture_helpers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module FixtureHelpers
4 | def read_fixture(url)
5 | file_name = "#{url.gsub(%r{^https?://}, "").gsub(%r{[/.]}, "_")}.html"
6 |
7 | File.read(File.join(Dir.pwd, "spec", "support", "fixtures", file_name))
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/indieweb/endpoints/exceptions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module IndieWeb
4 | module Endpoints
5 | class Error < StandardError; end
6 |
7 | class HttpError < Error; end
8 |
9 | class InvalidURIError < Error; end
10 |
11 | class SSLError < Error; end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_from:
2 | - https://rubocop.jgarber.cc/rubocop.yml
3 | - https://rubocop.jgarber.cc/rubocop-rspec.yml
4 |
5 | plugins: rubocop-packaging
6 |
7 | AllCops:
8 | TargetRubyVersion: "2.7"
9 |
10 | # Checks that spec file paths are consistent and well-formed.
11 | RSpec/SpecFilePathFormat:
12 | CustomTransform:
13 | IndieWeb: "indieweb"
14 |
--------------------------------------------------------------------------------
/.simplecov:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | formatters = SimpleCov::Formatter.from_env(ENV)
4 |
5 | if RSpec.configuration.files_to_run.length > 1
6 | require "simplecov-console"
7 |
8 | formatters << SimpleCov::Formatter::Console
9 | end
10 |
11 | SimpleCov.start do
12 | enable_coverage :branch
13 |
14 | formatter SimpleCov::Formatter::MultiFormatter.new(formatters)
15 | end
16 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | # Specify your gem's dependencies in indieweb-endpoints.gemspec
6 | gemspec
7 |
8 | gem "irb"
9 | gem "rake"
10 | gem "rspec"
11 | gem "rubocop"
12 | gem "rubocop-packaging"
13 | gem "rubocop-performance"
14 | gem "rubocop-rake"
15 | gem "rubocop-rspec"
16 | gem "simplecov"
17 | gem "simplecov-console"
18 | gem "webmock"
19 |
--------------------------------------------------------------------------------
/spec/lib/indieweb/endpoints/client_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe IndieWeb::Endpoints::Client do
4 | subject(:client) { described_class.new(url) }
5 |
6 | context "when given invalid arguments" do
7 | let(:url) { "1:" }
8 |
9 | it "raises an IndieWeb::Endpoints::InvalidURIError" do
10 | expect { client }.to raise_error(IndieWeb::Endpoints::InvalidURIError)
11 | end
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_link_element_empty_href.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "bundler/setup"
5 | require "indieweb/endpoints"
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | ENV["IRBRC"] = ".irbrc"
15 |
16 | require "irb"
17 | IRB.start(__FILE__)
18 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_link_element_relative_url.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/spec/lib/indieweb/endpoints/client_endpoints_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe IndieWeb::Endpoints::Client, "#endpoints" do
4 | # TODO: Rework these specs to use WebMock: https://github.com/bblimke/webmock
5 | context "when running the webmention.rocks Endpoint Discovery tests" do
6 | WebmentionRocks::ENDPOINT_DISCOVERY_TESTS.each do |url, regexp|
7 | describe url do
8 | subject { described_class.new(url).endpoints[:webmention] }
9 |
10 | it { is_expected.to match(regexp) }
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_link_element_query_string.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_multiple_endpoints.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_link_element_relative_path.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_link_element_multiple_rel_values.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_link_element_absolute_url.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_link_element_no_href.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/lib/indieweb/endpoints.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "http"
4 | require "link-header-parser"
5 | require "nokogiri/html-ext"
6 |
7 | require_relative "endpoints/exceptions"
8 |
9 | require_relative "endpoints/client"
10 | require_relative "endpoints/parser"
11 |
12 | module IndieWeb
13 | module Endpoints
14 | # Discover a URL's IndieAuth, Micropub, Microsub, and Webmention endpoints.
15 | #
16 | # Convenience method for {Client#endpoints}.
17 | #
18 | # @example
19 | # IndieWeb::Endpoints.get("https://aaronparecki.com")
20 | #
21 | # @param (see Client#initialize)
22 | #
23 | # @return (see Client#endpoints)
24 | def self.get(url)
25 | Client.new(url).endpoints
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Since git version 2.23, git-blame has a feature to ignore certain commits.
2 | #
3 | # This file contains a list of commits that are not likely what you are looking
4 | # for in `git blame`. You can set this file as a default ignore file for blame
5 | # by running the following command:
6 | #
7 | # $ git config blame.ignoreRevsFile .git-blame-ignore-revs
8 |
9 | 79f63885decf438df76e211bfc3409ceb6a1b853
10 | 32735aa57b6a08a2e48187e7f3e31fbef442098f
11 | 0f3ca43203c7d68d25554060a9c35d17b5c671da
12 | b5924c963141411f86022a5d039432154c2635d1
13 | 037ef3744eea392abcee4bd200a46d54e2b8427b
14 | c6378d3d3fbeec6b024027bc43807182d4b2fac2
15 | 12614ff03be549e61b6dd0b0e6bf324c2a094903
16 | cb72804ad4cc0380f0e36c17931602d56bbfe924
17 | 98a94d2d76ddfa65a9e2e34d561f83f9ecf64244
18 | 49b30c91e5809649fe580d4da9f76ff6051231e6
19 |
--------------------------------------------------------------------------------
/spec/lib/indieweb/endpoints/client_response_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe IndieWeb::Endpoints::Client, "#response" do
4 | subject(:response) { described_class.new(url).response }
5 |
6 | let(:url) { "https://example.com" }
7 |
8 | context "when rescuing from an HTTP::Error" do
9 | it "raises an IndieWeb::Endpoints::HttpError" do
10 | stub_request(:get, url).to_raise(HTTP::Error)
11 |
12 | expect { response }.to raise_error(IndieWeb::Endpoints::HttpError)
13 | end
14 | end
15 |
16 | context "when rescuing from an OpenSSL::SSL::SSLError" do
17 | it "raises an IndieWeb::Endpoints::SSLError" do
18 | stub_request(:get, url).to_raise(OpenSSL::SSL::SSLError)
19 |
20 | expect { response }.to raise_error(IndieWeb::Endpoints::SSLError)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches: [main]
7 | workflow_call:
8 | workflow_dispatch:
9 |
10 | jobs:
11 | lint:
12 | name: Lint
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v6
16 | - uses: ruby/setup-ruby@v1
17 | with:
18 | bundler-cache: true
19 | - run: bin/rubocop --color
20 | test:
21 | name: Test (Ruby ${{ matrix.ruby }})
22 | runs-on: ubuntu-latest
23 | needs: lint
24 | strategy:
25 | fail-fast: false
26 | matrix:
27 | ruby: ["2.7", "3.0", "3.1", "3.2", "3.3", "3.4"]
28 | steps:
29 | - uses: actions/checkout@v6
30 | - uses: ruby/setup-ruby@v1
31 | with:
32 | bundler-cache: true
33 | ruby-version: ${{ matrix.ruby }}
34 | - run: bin/rspec --force-color --format documentation
35 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_link_element_fragment.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_link_element_exact_match.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/spec/support/fixtures/example_com_link_element_html_comment.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | /.config
4 | /.ruby-lsp
5 | /coverage/
6 | /InstalledFiles
7 | /pkg/
8 | /spec/reports/
9 | /spec/examples.txt
10 | /test/tmp/
11 | /test/version_tmp/
12 | /tmp/
13 |
14 | # Used by dotenv library to load environment variables.
15 | # .env
16 |
17 | # Ignore history files.
18 | .irb_history
19 | .rdbg_history
20 |
21 | # Documentation cache and generated files:
22 | /.yardoc/
23 | /_yardoc/
24 | /doc/
25 | /rdoc/
26 |
27 | # Environment normalization:
28 | /.bundle/
29 | /vendor/bundle
30 | /lib/bundler/man/
31 |
32 | # for a library or gem, you might want to ignore these files since the code is
33 | # intended to run in multiple environments; otherwise, check them in:
34 | Gemfile.lock
35 | # .ruby-version
36 | # .ruby-gemset
37 |
38 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
39 | .rvmrc
40 |
41 | # Used by RuboCop. Remote config files pulled in from inherit_from directive.
42 | .rubocop-https?--*
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Jason Garber
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ruby
3 | {
4 | "name": "indieweb-endpoints-ruby",
5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6 | "image": "mcr.microsoft.com/devcontainers/base:1-bookworm",
7 |
8 | // Features to add to the dev container. More info: https://containers.dev/features.
9 | "features": {
10 | "ghcr.io/rails/devcontainer/features/ruby:2": {
11 | "version": "3.4.6"
12 | }
13 | },
14 |
15 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
16 | // "forwardPorts": [],
17 |
18 | // An array of Docker CLI arguments that should be used when running the container.
19 | "runArgs": ["--name", "indieweb-endpoints-ruby"],
20 |
21 | // Use 'postCreateCommand' to run commands after the container is created.
22 | "postCreateCommand": ".devcontainer/setup.sh",
23 |
24 | // Configure tool-specific properties.
25 | "customizations": {
26 | "vscode": {
27 | "extensions": [
28 | "EditorConfig.EditorConfig"
29 | ]
30 | }
31 | }
32 |
33 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
34 | // "remoteUser": "root"
35 | }
36 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | ci:
9 | name: CI
10 | uses: ./.github/workflows/ci.yml
11 | publish-to-rubygems:
12 | name: Publish to RubyGems
13 | permissions:
14 | contents: write
15 | id-token: write
16 | needs: ci
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v6
20 | - uses: ruby/setup-ruby@v1
21 | with:
22 | bundler-cache: true
23 | - uses: rubygems/release-gem@v1
24 | with:
25 | await-release: false
26 | publish-to-github-packages:
27 | name: Publish to GitHub Packages
28 | permissions:
29 | contents: read
30 | packages: write
31 | needs: ci
32 | runs-on: ubuntu-latest
33 | steps:
34 | - uses: actions/checkout@v6
35 | - uses: ruby/setup-ruby@v1
36 | with:
37 | bundler-cache: true
38 | - run: |
39 | mkdir -p $HOME/.gem
40 | touch $HOME/.gem/credentials
41 | chmod 0600 $HOME/.gem/credentials
42 | printf -- "---\n:github: Bearer ${{ secrets.GITHUB_TOKEN }}\n" > $HOME/.gem/credentials
43 | - run: bundle exec rake release
44 | env:
45 | BUNDLE_GEM__PUSH_KEY: github
46 | RUBYGEMS_HOST: "https://rubygems.pkg.github.com/${{ github.repository_owner }}"
47 |
--------------------------------------------------------------------------------
/indieweb-endpoints.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Gem::Specification.new do |spec|
4 | spec.required_ruby_version = ">= 2.7"
5 |
6 | spec.name = "indieweb-endpoints"
7 | spec.version = "10.0.2"
8 | spec.authors = ["Jason Garber"]
9 | spec.email = ["jason@sixtwothree.org"]
10 |
11 | spec.summary = "Discover a URL’s IndieAuth, Micropub, Microsub, and Webmention endpoints."
12 | spec.description = spec.summary
13 | spec.homepage = "https://github.com/indieweb/indieweb-endpoints-ruby"
14 | spec.license = "MIT"
15 |
16 | spec.files = Dir["lib/**/*"].reject { |f| File.directory?(f) }
17 | spec.files += ["LICENSE", "CHANGELOG.md", "CODE_OF_CONDUCT.md", "CONTRIBUTING.md", "README.md"]
18 | spec.files += ["indieweb-endpoints.gemspec"]
19 |
20 | spec.require_paths = ["lib"]
21 |
22 | spec.metadata = {
23 | "bug_tracker_uri" => "#{spec.homepage}/issues",
24 | "changelog_uri" => "#{spec.homepage}/releases/tag/v#{spec.version}",
25 | "documentation_uri" => "https://rubydoc.info/gems/#{spec.name}/#{spec.version}",
26 | "homepage_uri" => spec.homepage,
27 | "rubygems_mfa_required" => "true",
28 | "source_code_uri" => "#{spec.homepage}/tree/v#{spec.version}",
29 | }
30 |
31 | spec.add_dependency "http", "~> 5.2"
32 | spec.add_dependency "link-header-parser", "~> 7.0", ">= 7.0.1"
33 | spec.add_dependency "nokogiri-html-ext", "~> 1.6"
34 | end
35 |
--------------------------------------------------------------------------------
/lib/indieweb/endpoints/client.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module IndieWeb
4 | module Endpoints
5 | class Client
6 | # Create a new client with a URL to parse for IndieWeb endpoints.
7 | #
8 | # @example
9 | # client = IndieWeb::Endpoints::Client.new("https://aaronparecki.com")
10 | #
11 | # @param url [String, HTTP::URI, #to_s] an absolute URL
12 | #
13 | # @raise [InvalidURIError]
14 | def initialize(url)
15 | @uri = HTTP::URI.parse(url)
16 | rescue Addressable::URI::InvalidURIError => e
17 | raise InvalidURIError, e
18 | end
19 |
20 | # :nocov:
21 | # @return [String]
22 | def inspect
23 | format "#<%s:%#0x @uri=%s>",
24 | class: self.class,
25 | id: object_id << 1,
26 | uri: uri.inspect
27 | end
28 | # :nocov:
29 |
30 | # A Hash of the discovered IndieWeb endpoints from the provided URL.
31 | #
32 | # @return [Hash{Symbol => String, Array, nil}]
33 | def endpoints
34 | @endpoints ||= Parser.new(response).to_h
35 | end
36 |
37 | # The +HTTP::Response+ object.
38 | #
39 | # @return [HTTP::Response]
40 | #
41 | # @raise [HttpError, SSLError]
42 | def response
43 | @response ||=
44 | HTTP
45 | .follow(max_hops: 20)
46 | .headers(accept: "*/*", user_agent: user_agent)
47 | .timeout(connect: 5, read: 5)
48 | .get(uri)
49 | rescue HTTP::Error => e
50 | raise HttpError, e
51 | rescue OpenSSL::SSL::SSLError => e
52 | raise SSLError, e
53 | end
54 |
55 | private
56 |
57 | # @return [HTTP::URI]
58 | attr_reader :uri
59 |
60 | # @return [String]
61 | def user_agent
62 | "Mozilla/5.0 (compatible; IndieWebEndpointsDiscovery/1.0; +https://rubygems.org/gems/indieweb-endpoints)"
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/spec/support/webmention_rocks.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module WebmentionRocks
4 | ENDPOINT_DISCOVERY_TESTS = [
5 | ["https://webmention.rocks/test/1", %r{^https://webmention.rocks/test/1/webmention$}],
6 | ["https://webmention.rocks/test/2", %r{^https://webmention.rocks/test/2/webmention$}],
7 | ["https://webmention.rocks/test/3", %r{^https://webmention.rocks/test/3/webmention$}],
8 | ["https://webmention.rocks/test/4", %r{^https://webmention.rocks/test/4/webmention$}],
9 | ["https://webmention.rocks/test/5", %r{^https://webmention.rocks/test/5/webmention$}],
10 | ["https://webmention.rocks/test/6", %r{^https://webmention.rocks/test/6/webmention$}],
11 | ["https://webmention.rocks/test/7", %r{^https://webmention.rocks/test/7/webmention$}],
12 | ["https://webmention.rocks/test/8", %r{^https://webmention.rocks/test/8/webmention$}],
13 | ["https://webmention.rocks/test/9", %r{^https://webmention.rocks/test/9/webmention$}],
14 | ["https://webmention.rocks/test/10", %r{^https://webmention.rocks/test/10/webmention$}],
15 | ["https://webmention.rocks/test/11", %r{^https://webmention.rocks/test/11/webmention$}],
16 | ["https://webmention.rocks/test/12", %r{^https://webmention.rocks/test/12/webmention$}],
17 | ["https://webmention.rocks/test/13", %r{^https://webmention.rocks/test/13/webmention$}],
18 | ["https://webmention.rocks/test/14", %r{^https://webmention.rocks/test/14/webmention$}],
19 | ["https://webmention.rocks/test/15", %r{^https://webmention.rocks/test/15$}],
20 | ["https://webmention.rocks/test/16", %r{^https://webmention.rocks/test/16/webmention$}],
21 | ["https://webmention.rocks/test/17", %r{^https://webmention.rocks/test/17/webmention$}],
22 | ["https://webmention.rocks/test/18", %r{^https://webmention.rocks/test/18/webmention$}],
23 | ["https://webmention.rocks/test/19", %r{^https://webmention.rocks/test/19/webmention$}],
24 | ["https://webmention.rocks/test/20", %r{^https://webmention.rocks/test/20/webmention$}],
25 | ["https://webmention.rocks/test/21", %r{^https://webmention.rocks/test/21/webmention\?query=yes$}],
26 | ["https://webmention.rocks/test/22", %r{^https://webmention.rocks/test/22/webmention$}],
27 | ["https://webmention.rocks/test/23/page", %r{^https://webmention.rocks/test/23/page/webmention-endpoint/.*$}],
28 | ].freeze
29 | end
30 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to indieweb-endpoints-ruby
2 |
3 | There are a couple ways you can help improve indieweb-endpoints-ruby:
4 |
5 | 1. Fix an existing [Issue][issues] and submit a [Pull Request][pulls].
6 | 1. Review open [Pull Requests][pulls].
7 | 1. Report a new [Issue][issues]. _Only do this after you've made sure the behavior or problem you're observing isn't already documented in an open issue._
8 |
9 | ## Getting Started
10 |
11 | indieweb-endpoints-ruby is developed using Ruby 3.4 and is tested against additional Ruby versions using [GitHub Actions](https://github.com/indieweb/indieweb-endpoints-ruby/actions).
12 |
13 | > [!TIP]
14 | > This project is configured with a [Dev Container](https://containers.dev) which includes everything you'd need to contribute to indieweb-endpoints-ruby. If you use a supported code editor or IDE, you're encouraged to use the existing Dev Container setup.
15 |
16 | Before making changes to indieweb-endpoints-ruby, you'll want to install Ruby 3.4. Using a Ruby version managment tool like [rbenv](https://github.com/rbenv/rbenv), [chruby](https://github.com/postmodern/chruby), or [rvm](https://github.com/rvm/rvm) is recommended. Once you've installed Ruby 3.4 using your method of choice, install the project's gems by running:
17 |
18 | ```sh
19 | bundle install
20 | ```
21 |
22 | ## Making Changes
23 |
24 | 1. Fork and clone the project's repo.
25 | 2. Install development dependencies as outlined above.
26 | 3. Create a feature branch for the code changes you're looking to make: `git checkout -b my-new-feature`.
27 | 4. _Write some code!_
28 | 5. If your changes would benefit from testing, add the necessary tests and verify everything passes by running `bin/rspec`.
29 | 6. Commit your changes: `git commit -am 'Add some new feature or fix some issue'`. _(See [this excellent article](https://cbea.ms/git-commit/) for tips on writing useful Git commit messages.)_
30 | 7. Push the branch to your fork: `git push -u origin my-new-feature`.
31 | 8. Create a new [Pull Request][pulls] and we'll review your changes.
32 |
33 | ## Code Style
34 |
35 | Code formatting conventions are defined in the `.editorconfig` file which uses the [EditorConfig](https://editorconfig.org) syntax. There are [plugins for a variety of editors](https://editorconfig.org/#download) that utilize the settings in the `.editorconfig` file. We recommended installing the EditorConfig plugin for your editor of choice.
36 |
37 | Your bug fix or feature addition won't be rejected if it runs afoul of any (or all) of these guidelines, but following the guidelines will definitely make everyone's lives a little easier.
38 |
39 | [issues]: https://github.com/indieweb/indieweb-endpoints-ruby/issues
40 | [pulls]: https://github.com/indieweb/indieweb-endpoints-ruby/pulls
41 |
--------------------------------------------------------------------------------
/lib/indieweb/endpoints/parser.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module IndieWeb
4 | module Endpoints
5 | # @api private
6 | class Parser
7 | # @param response [HTTP::Response]
8 | def initialize(response)
9 | @response = response
10 | end
11 |
12 | # @param identifier [String]
13 | # @param node_names [Array]
14 | #
15 | # @return [Array]
16 | #
17 | # @raise [InvalidURIError]
18 | def matches(identifier, node_names: ["link"])
19 | results = (matches_from_headers(identifier) + matches_from_body(identifier, node_names)).compact
20 |
21 | results.uniq!
22 | results.sort!
23 |
24 | results
25 | end
26 |
27 | # @param (see #matches)
28 | #
29 | # @return [String]
30 | def match(identifier, **kwargs)
31 | matches(identifier, **kwargs).first
32 | end
33 |
34 | # @return [Hash{Symbol => String, Array, nil}]
35 | def to_h
36 | {
37 | authorization_endpoint: match("authorization_endpoint"),
38 | "indieauth-metadata": match("indieauth-metadata"),
39 | micropub: match("micropub"),
40 | microsub: match("microsub"),
41 | redirect_uri: (redirect_uri = matches("redirect_uri")).any? ? redirect_uri : nil,
42 | token_endpoint: match("token_endpoint"),
43 | webmention: match("webmention", node_names: ["link", "a"]),
44 | }
45 | end
46 |
47 | private
48 |
49 | # @return [HTTP::Response]
50 | attr_reader :response
51 |
52 | # @return [Nokogiri::HTML5::Document]
53 | def body
54 | @body ||= Nokogiri::HTML5(response.body, response.uri.to_s).resolve_urls!
55 | end
56 |
57 | # @return [Hash{Symbol => Array}]
58 | def headers
59 | @headers ||= LinkHeaderParser.parse(response.headers.get("link"), base_uri: response.uri).group_by_relation_type
60 | end
61 |
62 | # Reject URLs with fragment identifiers per the IndieAuth specification.
63 | #
64 | # @param identifier [String, #to_s]
65 | # @param node_names [Array]
66 | #
67 | # @return [Array]
68 | def matches_from_body(identifier, node_names)
69 | return [] unless response.mime_type == "text/html"
70 |
71 | body
72 | .css(*node_names.map { |node| %(#{node}[rel~="#{identifier}"][href]:not([href*="#"]) / @href) })
73 | .map(&:value)
74 | end
75 |
76 | # Reject URLs with fragment identifiers per the IndieAuth specification.
77 | #
78 | # @param identifier [String, #to_sym]
79 | #
80 | # @return [Array]
81 | def matches_from_headers(identifier)
82 | Array(headers[identifier]).filter_map do |header|
83 | header.target_uri unless HTTP::URI.parse(header.target_uri).fragment
84 | end
85 | end
86 | end
87 | end
88 | end
89 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | > [!NOTE]
4 | > From v9.0.0, changes are documented using [GitHub Releases](https://github.com/indieweb/indieweb-endpoints-ruby/releases). For a given release, metadata on RubyGems.org will link to that version's Release page.
5 |
6 | ## 8.0.0 / 2022-11-09
7 |
8 | - Refactor `ResponseHeadersParser#results_for` to use `Enumerable#filter_map` (946ff3d)
9 | - Update link-header-parser dependency constraint (4f093fb)
10 | - **Breaking change:** Update development Ruby to 2.7.6 and minimum Ruby to 2.7 (593455d)
11 |
12 | ## 7.2.0 / 2022-10-04
13 |
14 | - Add support for `indieauth-metadata` endpoint (35cc950)
15 | - Switch from pry-byebug to Ruby's debug gem (7ad8925)
16 | - Update development Ruby version to 2.6.10 (f105752)
17 |
18 | ## 7.1.0 / 2022-03-08
19 |
20 | - Refactor gem code (eba8115)
21 | - Refactor specs (5b92119)
22 | - Rescue from `OpenSSL::SSL::SSLError` (4ea38e5)
23 |
24 | ## 7.0.0 / 2022-01-06
25 |
26 | - Update runtime dependency versions (5c9430f)
27 | - **Breaking change:** Update development Ruby version to 2.6.9 and minimum Ruby version to 2.6 (ed17ab1 and 7e4a621)
28 | - Migrate to GitHub Actions from Travis CI (c019756)
29 | - Add `rubygems_mfa_required` to gemspec metadata (1f0a40f)
30 |
31 | ## 6.1.0 / 2021-05-25
32 |
33 | - Add support for Ruby 3.0 (3394252)
34 |
35 | ## 6.0.0 / 2021-05-25
36 |
37 | - Refactor parsers (e22e0af)
38 | - Simplify exception handling (65361ee)
39 | - Update http dependency to 5.0 (44f2d23)
40 | - **Breaking change:** Favor Addressable::URI.join over Absolutely (0bc5049)
41 | - Update development Ruby version to 2.5.9 (3439cce)
42 |
43 | ## 5.0.0 / 2020-11-11
44 |
45 | - Update absolutely and link-header-parser dependencies (064696e)
46 |
47 | ## 4.0.0 / 2020-07-21
48 |
49 | - **Breaking change:** Return a Hash of endpoints instead of an OpenStruct (15dc387)
50 | - Update [link-header-parser](https://rubygems.org/gems/link-header-parser) dependency to v2.0.0 (2255e6b)
51 | - **Breaking change:** Update development Ruby version to 2.5.8 and minimum Ruby version to 2.5 (dd5a142)
52 | - Refactor response headers/body parsers into a single class (ee02da3)
53 | - Refactor `IndieWeb::Endpoints::Client` and remove `HttpRequestService` (2732616)
54 | - Add offending url to exception message (#5) (4bf7a54)
55 |
56 | ## 3.0.0 / 2020-05-14
57 |
58 | - Update Absolutely and LinkHeaderParser dependencies (9e0a64a)
59 | - Move development dependencies to `Gemfile` (67067f3)
60 | - Update development Ruby version to 2.4.10 (f5430f9)
61 |
62 | ## 2.0.0 / 2020-01-25
63 |
64 | - Downgrade HTTP gem version constraint to ~> 4.3 (cb63230)
65 |
66 | ## 1.1.0 / 2020-01-20
67 |
68 | - Expand supported Ruby versions to include 2.7 (ae63ed0)
69 | - Update project Ruby version to 2.4.9 (e576ad6)
70 |
71 | ## 1.0.2 / 2019-08-31
72 |
73 | - Update WebMock and Addressable gems (298da63)
74 | - Update development dependencies (5663e3a)
75 |
76 | ## 1.0.1 / 2019-07-17
77 |
78 | - Safely combine, flatten, and compact result set (5f2d9c5)
79 |
80 | ## 1.0.0 / 2019-07-08
81 |
82 | - Refactor gem code using service objects approach (78511d7 and 9d2fee0)
83 |
84 | ## 0.7.0 / 2019-07-03
85 |
86 | - Update runtime and development dependencies (d99214b and 7692ab3)
87 |
88 | ## 0.6.0 / 2019-06-15
89 |
90 | - Update Parser-related classes to resolve #2
91 |
92 | ## 0.5.0 / 2019-05-09
93 |
94 | - Add support for Microsub endpoint discovery (5e81d9f)
95 | - Refactor parsers to ignore URLs with fragments (797b376)
96 | - Rescue `NoMethodError` (for `nil`) and `TypeError` (for non-`String`) (e33522e)
97 | - Raise `ArgumentError` if url scheme is not `http` or `https` (8eb1b1a)
98 | - Shorten up User Agent string (f9717b4)
99 | - Refactor `HTTPRequest` class using specification defaults (feef2ba)
100 |
101 | ## 0.4.0 / 2019-05-01
102 |
103 | - Add `IndieWeb::Endpoints.client` method (c4d42d0)
104 | - Rename base `Error` class to `IndieWebEndpointsError` (d6d6f98)
105 | - Add `HttpRequest` class (7864cbd)
106 |
107 | ## 0.3.0 / 2019-04-30
108 |
109 | - `IndieWeb::Endpoints::Client#endpoints` returns an `OpenStruct` instead of a `Hash` (c209b0b).
110 |
111 | ## 0.2.0 / 2019-04-25
112 |
113 | - Subclass exceptions under `IndieWeb::Endpoints::Error` (667eec7)
114 | - Refactor parsers and `Registerable` module (3b96858)
115 | - Refactor `Client#response` method (c36fda3)
116 |
117 | ## 0.1.0 / 2019-04-24
118 |
119 | - Initial release!
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # indieweb-endpoints-ruby
2 |
3 | **A Ruby gem for discovering a URL's [IndieAuth](https://indieweb.org/IndieAuth), [Micropub](https://indieweb.org/Micropub), [Microsub](https://indieweb.org/Microsub), and [Webmention](https://indieweb.org/Webmention) endpoints.**
4 |
5 | [](https://rubygems.org/gems/indieweb-endpoints)
6 | [](https://rubygems.org/gems/indieweb-endpoints)
7 | [](https://github.com/indieweb/indieweb-endpoints-ruby/actions/workflows/ci.yml)
8 |
9 | ## Key Features
10 |
11 | - Compliant with [Section 4.1](https://www.w3.org/TR/indieauth/#discovery-by-clients) and [Section 4.2.2](https://www.w3.org/TR/indieauth/#redirect-url) of [the W3C's IndieAuth Working Group Note](https://www.w3.org/TR/indieauth/), [Section 5.3](https://www.w3.org/TR/micropub/#endpoint-discovery) of [the W3C's Micropub Recommendation](https://www.w3.org/TR/micropub/), and [Section 3.1.2](https://www.w3.org/TR/webmention/#sender-discovers-receiver-webmention-endpoint) of [the W3C's Webmention Recommendation](https://www.w3.org/TR/webmention/).
12 | - Passes all Endpoint Discovery tests on [webmention.rocks](https://webmention.rocks).
13 | - Supports Ruby 2.7 and newer.
14 |
15 | ## Getting Started
16 |
17 | Before installing and using indieweb-endpoints-ruby, you'll want to have [Ruby](https://www.ruby-lang.org) 2.7 (or newer) installed. Using a Ruby version managment tool like [rbenv](https://github.com/rbenv/rbenv), [chruby](https://github.com/postmodern/chruby), or [rvm](https://github.com/rvm/rvm) is recommended.
18 |
19 | indieweb-endpoints-ruby is developed using Ruby 3.4 and is tested against additional Ruby versions using [GitHub Actions](https://github.com/indieweb/indieweb-endpoints-ruby/actions).
20 |
21 | ## Installation
22 |
23 | Add indieweb-endpoints-ruby to your project's `Gemfile` and run `bundle install`:
24 |
25 | ```ruby
26 | source "https://rubygems.org"
27 |
28 | gem "indieweb-endpoints"
29 | ```
30 |
31 | ## Usage
32 |
33 | ### Basic Usage
34 |
35 | With indieweb-endpoints-ruby added to your project's `Gemfile` and installed, you may discover a URL's IndieWeb-relevant endpoints by doing:
36 |
37 | ```ruby
38 | require "indieweb/endpoints"
39 |
40 | IndieWeb::Endpoints.get("https://aaronparecki.com")
41 | #=> { authorization_endpoint: "https://aaronparecki.com/auth", "indieauth-metadata": "https://aaronparecki.com/.well-known/oauth-authorization-server", micropub: "https://aaronparecki.com/micropub", microsub: "https://aperture.p3k.io/microsub/1", redirect_uri: nil, token_endpoint: "https://aaronparecki.com/auth/token", webmention: "https://webmention.io/aaronpk/webmention" }
42 | ```
43 |
44 | This example will search [Aaron's website](https://aaronparecki.com) for valid IndieAuth, Micropub, and Webmention endpoints and return a `Hash` of results. Each key in the returned `Hash` will have a value of either a `String` representing a URL or `nil`. The `redirect_uri` key's value will be either an `Array` or `nil` since a given URL may register multiple callback URLs.
45 |
46 | ### Advanced Usage
47 |
48 | Should the need arise, you may work with the `IndieWeb::Endpoints::Client` class:
49 |
50 | ```ruby
51 | require "indieweb/endpoints"
52 |
53 | client = IndieWeb::Endpoints::Client.new("https://aaronparecki.com")
54 | #=> #
55 |
56 | client.response
57 | #=> #
58 |
59 | client.endpoints
60 | #=> { authorization_endpoint: "https://aaronparecki.com/auth", micropub: "https://aaronparecki.com/micropub", microsub: "https://aperture.p3k.io/microsub/1", redirect_uri: nil, token_endpoint: "https://aaronparecki.com/auth/token", webmention: "https://webmention.io/aaronpk/webmention" }
61 | ```
62 |
63 | ### Exception Handling
64 |
65 | indieweb-endpoints-ruby may raise the following exceptions which are subclasses of `IndieWeb::Endpoints::Error` (which itself is a subclass of `StandardError`).
66 |
67 | - `IndieWeb::Endpoints::InvalidURIError`
68 | - `IndieWeb::Endpoints::HttpError`
69 | - `IndieWeb::Endpoints::SSLError`
70 |
71 | ## Contributing
72 |
73 | See [CONTRIBUTING.md](https://github.com/indieweb/indieweb-endpoints-ruby/blob/main/CONTRIBUTING.md) for more on how to contribute to indieweb-endpoints-ruby. Your help is greatly appreciated!
74 |
75 | By contributing to and participating in the development of indieweb-endpoints-ruby, you acknowledge that you have read and agree to the [IndieWeb Code of Conduct](https://indieweb.org/code-of-conduct).
76 |
77 | ## Acknowledgments
78 |
79 | indieweb-endpoints-ruby wouldn't exist without IndieAuth, Micropub, and Webmention and the hard work put in by everyone involved in the [IndieWeb](https://indieweb.org) movement. Additionally, the comprehensive Webmention Endpoint Discovery test suite at [webmention.rocks](https://webmention.rocks) was invaluable in the development of this Ruby gem.
80 |
81 | indieweb-endpoints-ruby is written and maintained by [Jason Garber](https://sixtwothree.org).
82 |
83 | ## License
84 |
85 | indieweb-endpoints-ruby is freely available under the [MIT License](https://opensource.org/licenses/MIT).
86 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file was generated by the `rspec --init` command. Conventionally, all
4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5 | # The generated `.rspec` file contains `--require spec_helper` which will cause
6 | # this file to always be loaded, without a need to explicitly require it in any
7 | # files.
8 | #
9 | # Given that it is always loaded, you are encouraged to keep this file as
10 | # light-weight as possible. Requiring heavyweight dependencies from this file
11 | # will add to the boot time of your test suite on EVERY test run, even for an
12 | # individual file that may not need all of that loaded. Instead, consider making
13 | # a separate helper file that requires the additional dependencies and performs
14 | # the additional setup, and require it from the spec files that actually need
15 | # it.
16 | #
17 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
18 |
19 | require "simplecov"
20 | require "webmock/rspec"
21 |
22 | require "indieweb/endpoints"
23 |
24 | Dir.glob("support/**/*.rb", base: __dir__).sort.each { |f| require_relative f }
25 |
26 | RSpec.configure do |config|
27 | config.include FixtureHelpers
28 |
29 | # rspec-expectations config goes here. You can use an alternate
30 | # assertion/expectation library such as wrong or the stdlib/minitest
31 | # assertions if you prefer.
32 | config.expect_with :rspec do |expectations|
33 | # This option will default to `true` in RSpec 4. It makes the `description`
34 | # and `failure_message` of custom matchers include text for helper methods
35 | # defined using `chain`, e.g.:
36 | # be_bigger_than(2).and_smaller_than(4).description
37 | # # => "be bigger than 2 and smaller than 4"
38 | # ...rather than:
39 | # # => "be bigger than 2"
40 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
41 | end
42 |
43 | # rspec-mocks config goes here. You can use an alternate test double
44 | # library (such as bogus or mocha) by changing the `mock_with` option here.
45 | config.mock_with :rspec do |mocks|
46 | # Prevents you from mocking or stubbing a method that does not exist on
47 | # a real object. This is generally recommended, and will default to
48 | # `true` in RSpec 4.
49 | mocks.verify_partial_doubles = true
50 | end
51 |
52 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
53 | # have no way to turn it off -- the option exists only for backwards
54 | # compatibility in RSpec 3). It causes shared context metadata to be
55 | # inherited by the metadata hash of host groups and examples, rather than
56 | # triggering implicit auto-inclusion in groups with matching metadata.
57 | config.shared_context_metadata_behavior = :apply_to_host_groups
58 |
59 | # The settings below are suggested to provide a good initial experience
60 | # with RSpec, but feel free to customize to your heart's content.
61 |
62 | # This allows you to limit a spec run to individual examples or groups
63 | # you care about by tagging them with `:focus` metadata. When nothing
64 | # is tagged with `:focus`, all examples get run. RSpec also provides
65 | # aliases for `it`, `describe`, and `context` that include `:focus`
66 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
67 | config.filter_run_when_matching :focus
68 |
69 | # Allows RSpec to persist some state between runs in order to support
70 | # the `--only-failures` and `--next-failure` CLI options. We recommend
71 | # you configure your source control system to ignore this file.
72 | # config.example_status_persistence_file_path = 'spec/examples.txt'
73 |
74 | # Limits the available syntax to the non-monkey patched syntax that is
75 | # recommended. For more details, see:
76 | # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode
77 | config.disable_monkey_patching!
78 |
79 | # This setting enables warnings. It's recommended, but in some cases may
80 | # be too noisy due to issues in dependencies.
81 | config.warnings = true
82 |
83 | # Many RSpec users commonly either run the entire suite or an individual
84 | # file, and it's useful to allow more verbose output when running an
85 | # individual spec file.
86 | if config.files_to_run.one?
87 | # Use the documentation formatter for detailed output,
88 | # unless a formatter has already been configured
89 | # (e.g. via a command-line flag).
90 | config.default_formatter = "doc"
91 | end
92 |
93 | # Print the 10 slowest examples and example groups at the
94 | # end of the spec run, to help surface which specs are running
95 | # particularly slow.
96 | # config.profile_examples = 10
97 |
98 | # Run specs in random order to surface order dependencies. If you find an
99 | # order dependency and want to debug it, you can fix the order by providing
100 | # the seed, which is printed after each run.
101 | # --seed 1234
102 | config.order = :random
103 |
104 | # Seed global randomization in this process using the `--seed` CLI option.
105 | # Setting this allows you to use `--seed` to deterministically reproduce
106 | # test failures related to randomization by passing the same `--seed` value
107 | # as the one that triggered the failure.
108 | Kernel.srand config.seed
109 | end
110 |
--------------------------------------------------------------------------------
/spec/lib/indieweb/endpoints_get_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.shared_examples "a Hash of endpoints" do
4 | subject { described_class.get(url) }
5 |
6 | before do
7 | stub_request(:get, url).to_return(response)
8 | end
9 |
10 | it { is_expected.to eq(endpoints) }
11 | end
12 |
13 | RSpec.describe IndieWeb::Endpoints, ".get" do
14 | context "when given a URL that publishes no endpoints" do
15 | it_behaves_like "a Hash of endpoints" do
16 | let(:url) { "https://example.com" }
17 |
18 | let(:response) do
19 | {
20 | headers: {
21 | "Content-Type": "text/html",
22 | },
23 | body: read_fixture(url),
24 | }
25 | end
26 |
27 | let(:endpoints) do
28 | {
29 | authorization_endpoint: nil,
30 | "indieauth-metadata": nil,
31 | micropub: nil,
32 | microsub: nil,
33 | redirect_uri: nil,
34 | token_endpoint: nil,
35 | webmention: nil,
36 | }
37 | end
38 | end
39 | end
40 |
41 | context "when given a URL that publishes endpoints in HTTP headers" do
42 | let(:url) { "https://example.com" }
43 |
44 | let(:endpoints) do
45 | {
46 | authorization_endpoint: "https://example.com/authorization_endpoint",
47 | "indieauth-metadata": "https://example.com/indieauth-metadata",
48 | micropub: "https://example.com/micropub",
49 | microsub: "https://example.com/microsub",
50 | redirect_uri: ["https://example.com/redirect_uri"],
51 | token_endpoint: "https://example.com/token_endpoint",
52 | webmention: "https://example.com/webmention",
53 | }
54 | end
55 |
56 | # Similar to https://webmention.rocks/test/1
57 | context "when the HTTP Link header references a relative URL and the `rel` parameter is unquoted" do
58 | it_behaves_like "a Hash of endpoints" do
59 | let(:response) do
60 | {
61 | headers: {
62 | Link: [
63 | "; rel=authorization_endpoint",
64 | "; rel=indieauth-metadata",
65 | "; rel=micropub",
66 | "; rel=microsub",
67 | "; rel=redirect_uri",
68 | "; rel=token_endpoint",
69 | "; rel=webmention",
70 | ],
71 | },
72 | }
73 | end
74 | end
75 | end
76 |
77 | # Similar to https://webmention.rocks/test/2
78 | context "when the HTTP Link header references an absolute URL and the `rel` parameter is unquoted" do
79 | it_behaves_like "a Hash of endpoints" do
80 | let(:url) { "https://example.com" }
81 |
82 | let(:response) do
83 | {
84 | headers: {
85 | Link: [
86 | %(<#{url}/authorization_endpoint>; rel=authorization_endpoint),
87 | %(<#{url}/indieauth-metadata>; rel=indieauth-metadata),
88 | %(<#{url}/micropub>; rel=micropub),
89 | %(<#{url}/microsub>; rel=microsub),
90 | %(<#{url}/redirect_uri>; rel=redirect_uri),
91 | %(<#{url}/token_endpoint>; rel=token_endpoint),
92 | %(<#{url}/webmention>; rel=webmention),
93 | ],
94 | },
95 | }
96 | end
97 | end
98 | end
99 |
100 | # Similar to https://webmention.rocks/test/7
101 | context "when the HTTP Link header has strange casing" do
102 | it_behaves_like "a Hash of endpoints" do
103 | let(:url) { "https://example.com" }
104 |
105 | let(:response) do
106 | {
107 | headers: {
108 | "LinK" => [
109 | %(<#{url}/authorization_endpoint>; rel=authorization_endpoint),
110 | %(<#{url}/indieauth-metadata>; rel=indieauth-metadata),
111 | %(<#{url}/micropub>; rel=micropub),
112 | %(<#{url}/microsub>; rel=microsub),
113 | %(<#{url}/redirect_uri>; rel=redirect_uri),
114 | %(<#{url}/token_endpoint>; rel=token_endpoint),
115 | %(<#{url}/webmention>; rel=webmention),
116 | ],
117 | },
118 | }
119 | end
120 | end
121 | end
122 |
123 | # Similar to https://webmention.rocks/test/8
124 | context "when the `rel` parameter is quoted" do
125 | it_behaves_like "a Hash of endpoints" do
126 | let(:response) do
127 | {
128 | headers: {
129 | Link: [
130 | %(<#{url}/authorization_endpoint>; rel="authorization_endpoint"),
131 | %(<#{url}/indieauth-metadata>; rel="indieauth-metadata"),
132 | %(<#{url}/micropub>; rel="micropub"),
133 | %(<#{url}/microsub>; rel="microsub"),
134 | %(<#{url}/redirect_uri>; rel="redirect_uri"),
135 | %(<#{url}/token_endpoint>; rel="token_endpoint"),
136 | %(<#{url}/webmention>; rel="webmention"),
137 | ],
138 | },
139 | }
140 | end
141 | end
142 | end
143 |
144 | # Similar to https://webmention.rocks/test/10
145 | context "when the `rel` parameter contains multiple space-separated values" do
146 | it_behaves_like "a Hash of endpoints" do
147 | let(:response) do
148 | {
149 | headers: {
150 | Link: [
151 | %(<#{url}/authorization_endpoint>; rel="authorization_endpoint somethingelse"),
152 | %(<#{url}/indieauth-metadata>; rel="indieauth-metadata somethingelse"),
153 | %(<#{url}/micropub>; rel="micropub somethingelse"),
154 | %(<#{url}/microsub>; rel="microsub somethingelse"),
155 | %(<#{url}/redirect_uri>; rel="redirect_uri somethingelse"),
156 | %(<#{url}/token_endpoint>; rel="token_endpoint somethingelse"),
157 | %(<#{url}/webmention>; rel="webmention somethingelse"),
158 | ],
159 | },
160 | }
161 | end
162 | end
163 | end
164 |
165 | # Similar to https://webmention.rocks/test/18
166 | context "when the response includes multiple HTTP Link headers" do
167 | it_behaves_like "a Hash of endpoints" do
168 | let(:response) do
169 | {
170 | headers: {
171 | Link: [
172 | %(<#{url}/authorization_endpoint#error>; rel="authorization_endpoint"),
173 | %(; rel="authorization_endpoint_error"),
174 | %(<#{url}/authorization_endpoint>; rel="authorization_endpoint"),
175 | '; rel="other"',
176 | %(<#{url}/redirect_uri>; rel="redirect_uri"),
177 | '; rel="redirect_uri"',
178 | ],
179 | },
180 | }
181 | end
182 |
183 | let(:endpoints) do
184 | {
185 | authorization_endpoint: "https://example.com/authorization_endpoint",
186 | "indieauth-metadata": nil,
187 | micropub: nil,
188 | microsub: nil,
189 | redirect_uri: ["https://example.com/callback", "https://example.com/redirect_uri"],
190 | token_endpoint: nil,
191 | webmention: nil,
192 | }
193 | end
194 | end
195 | end
196 |
197 | # Similar to https://webmention.rocks/test/19
198 | context "when the HTTP Link header contains multiple comma-separated values" do
199 | it_behaves_like "a Hash of endpoints" do
200 | let(:response) do
201 | {
202 | headers: {
203 | Link: [
204 | %(; rel="other", \
205 | <#{url}/authorization_endpoint>; rel="authorization_endpoint"),
206 | %(; rel="other", ; rel="indieauth-metadata"),
207 | %(; rel="other", <#{url}/micropub>; rel="micropub"),
208 | %(; rel="other", <#{url}/microsub>; rel="microsub"),
209 | %(; rel="other", <#{url}/redirect_uri>; rel="redirect_uri"),
210 | %(; rel="other", <#{url}/token_endpoint>; rel="token_endpoint"),
211 | %(; rel="other", <#{url}/webmention>; rel="webmention"),
212 | ],
213 | },
214 | }
215 | end
216 | end
217 | end
218 |
219 | # Similar to https://webmention.rocks/test/23
220 | context "when the HTTP Link header redirects to a relative URL" do
221 | it_behaves_like "a Hash of endpoints" do
222 | let(:response) do
223 | {
224 | headers: {
225 | Location: "page/authorization_endpoint",
226 | },
227 | status: 302,
228 | }
229 | end
230 |
231 | let(:endpoints) do
232 | {
233 | authorization_endpoint: "https://example.com/page/authorization_endpoint/endpoint",
234 | "indieauth-metadata": nil,
235 | micropub: nil,
236 | microsub: nil,
237 | redirect_uri: nil,
238 | token_endpoint: nil,
239 | webmention: nil,
240 | }
241 | end
242 |
243 | # Note that this executes after the shared example's before block
244 | before do
245 | redirected_url = "https://example.com/page/authorization_endpoint"
246 |
247 | stub_request(:get, redirected_url).to_return(
248 | headers: {
249 | "Content-Type": "text/html",
250 | Link: "<#{redirected_url}/endpoint>; rel=authorization_endpoint",
251 | }
252 | )
253 | end
254 | end
255 | end
256 | end
257 |
258 | context "when given a URL that publishes endpoints in HTML elements" do
259 | let(:response) do
260 | {
261 | headers: {
262 | "Content-Type": "text/html",
263 | },
264 | body: read_fixture(url),
265 | }
266 | end
267 |
268 | let(:endpoints) do
269 | {
270 | authorization_endpoint: "https://example.com/authorization_endpoint",
271 | "indieauth-metadata": "https://example.com/indieauth-metadata",
272 | micropub: "https://example.com/micropub",
273 | microsub: "https://example.com/microsub",
274 | redirect_uri: ["https://example.com/redirect"],
275 | token_endpoint: "https://example.com/token_endpoint",
276 | webmention: nil,
277 | }
278 | end
279 |
280 | # Similar to https://webmention.rocks/test/3
281 | context "when the `link` element references a relative URL" do
282 | it_behaves_like "a Hash of endpoints" do
283 | let(:url) { "https://example.com/link_element_relative_url" }
284 | end
285 | end
286 |
287 | # Similar to https://webmention.rocks/test/4
288 | context "when the `link` element references an absolute URL" do
289 | it_behaves_like "a Hash of endpoints" do
290 | let(:url) { "https://example.com/link_element_absolute_url" }
291 | end
292 | end
293 |
294 | # Similar to https://webmention.rocks/test/9
295 | context "when the `rel` attribute contains multiple space-separated values" do
296 | it_behaves_like "a Hash of endpoints" do
297 | let(:url) { "https://example.com/link_element_multiple_rel_values" }
298 | end
299 | end
300 |
301 | # Similar to https://webmention.rocks/test/12
302 | context "when the `rel` attribute contains similar values" do
303 | it_behaves_like "a Hash of endpoints" do
304 | let(:url) { "https://example.com/link_element_exact_match" }
305 | end
306 | end
307 |
308 | # Similar to https://webmention.rocks/test/13
309 | context "when the HTML contains an endpoint in an HTML comment" do
310 | it_behaves_like "a Hash of endpoints" do
311 | let(:url) { "https://example.com/link_element_html_comment" }
312 | end
313 | end
314 |
315 | # Similar to https://webmention.rocks/test/15
316 | context "when the `href` attribute is empty" do
317 | it_behaves_like "a Hash of endpoints" do
318 | let(:url) { "https://example.com/link_element_empty_href" }
319 |
320 | let(:endpoints) do
321 | {
322 | authorization_endpoint: "https://example.com/link_element_empty_href",
323 | "indieauth-metadata": "https://example.com/link_element_empty_href",
324 | micropub: "https://example.com/link_element_empty_href",
325 | microsub: "https://example.com/link_element_empty_href",
326 | redirect_uri: ["https://example.com/link_element_empty_href"],
327 | token_endpoint: "https://example.com/link_element_empty_href",
328 | webmention: nil,
329 | }
330 | end
331 | end
332 | end
333 |
334 | # Similar to https://webmention.rocks/test/20
335 | context "when the `href` attribute does not exist" do
336 | it_behaves_like "a Hash of endpoints" do
337 | let(:url) { "https://example.com/link_element_no_href" }
338 | end
339 | end
340 |
341 | # Similar to https://webmention.rocks/test/21
342 | context "when `link` element references a URL with a query string" do
343 | it_behaves_like "a Hash of endpoints" do
344 | let(:url) { "https://example.com/link_element_query_string" }
345 |
346 | let(:endpoints) do
347 | {
348 | authorization_endpoint: "https://example.com/authorization_endpoint?query=yes",
349 | "indieauth-metadata": "https://example.com/indieauth-metadata?query=yes",
350 | micropub: "https://example.com/micropub?query=yes",
351 | microsub: "https://example.com/microsub?query=yes",
352 | redirect_uri: ["https://example.com/redirect?query=yes"],
353 | token_endpoint: "https://example.com/token_endpoint?query=yes",
354 | webmention: nil,
355 | }
356 | end
357 | end
358 | end
359 |
360 | # Similar to https://webmention.rocks/test/22
361 | context "when the `link` element references a URL relative to the page" do
362 | it_behaves_like "a Hash of endpoints" do
363 | let(:url) { "https://example.com/link_element/relative_path" }
364 |
365 | let(:endpoints) do
366 | {
367 | authorization_endpoint: "https://example.com/link_element/relative_path/authorization_endpoint",
368 | "indieauth-metadata": "https://example.com/link_element/relative_path/indieauth-metadata",
369 | micropub: "https://example.com/link_element/relative_path/micropub",
370 | microsub: "https://example.com/link_element/relative_path/microsub",
371 | redirect_uri: ["https://example.com/link_element/relative_path/redirect"],
372 | token_endpoint: "https://example.com/link_element/relative_path/token_endpoint",
373 | webmention: nil,
374 | }
375 | end
376 | end
377 | end
378 |
379 | context "when the `link` element references a URL with a fragment" do
380 | it_behaves_like "a Hash of endpoints" do
381 | let(:url) { "https://example.com/link_element_fragment" }
382 |
383 | let(:endpoints) do
384 | {
385 | authorization_endpoint: "https://example.com/authorization_endpoint",
386 | "indieauth-metadata": "https://example.com/indieauth-metadata",
387 | micropub: "https://example.com/micropub",
388 | microsub: "https://example.com/microsub",
389 | redirect_uri: ["https://example.com/redirect_uri"],
390 | token_endpoint: "https://example.com/token_endpoint",
391 | webmention: nil,
392 | }
393 | end
394 | end
395 | end
396 | end
397 | end
398 |
--------------------------------------------------------------------------------