API documentation and spec setup
This commit is contained in:
parent
99ecb75c83
commit
900cc91197
7 changed files with 226 additions and 0 deletions
3
Gemfile
3
Gemfile
|
@ -111,4 +111,7 @@ group :test do
|
||||||
# code coverage
|
# code coverage
|
||||||
gem 'simplecov', require: false
|
gem 'simplecov', require: false
|
||||||
gem 'coveralls', require: false
|
gem 'coveralls', require: false
|
||||||
|
# api
|
||||||
|
gem 'apivore', require: false
|
||||||
|
gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114
|
||||||
end
|
end
|
||||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -94,6 +94,13 @@ GEM
|
||||||
activerecord (>= 3.0.0)
|
activerecord (>= 3.0.0)
|
||||||
addressable (2.5.2)
|
addressable (2.5.2)
|
||||||
public_suffix (>= 2.0.2, < 4.0)
|
public_suffix (>= 2.0.2, < 4.0)
|
||||||
|
apivore (1.6.2)
|
||||||
|
actionpack (>= 4, < 6)
|
||||||
|
hashie (~> 3.3)
|
||||||
|
json-schema (~> 2.5)
|
||||||
|
rspec (~> 3)
|
||||||
|
rspec-expectations (~> 3.1)
|
||||||
|
rspec-mocks (~> 3.1)
|
||||||
arel (6.0.4)
|
arel (6.0.4)
|
||||||
attribute_normalizer (1.2.0)
|
attribute_normalizer (1.2.0)
|
||||||
base32 (0.3.2)
|
base32 (0.3.2)
|
||||||
|
@ -191,6 +198,7 @@ GEM
|
||||||
has_scope (0.7.2)
|
has_scope (0.7.2)
|
||||||
actionpack (>= 4.1)
|
actionpack (>= 4.1)
|
||||||
activesupport (>= 4.1)
|
activesupport (>= 4.1)
|
||||||
|
hashie (3.4.6)
|
||||||
html2haml (2.2.0)
|
html2haml (2.2.0)
|
||||||
erubis (~> 2.7.0)
|
erubis (~> 2.7.0)
|
||||||
haml (>= 4.0, < 6)
|
haml (>= 4.0, < 6)
|
||||||
|
@ -217,6 +225,8 @@ GEM
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
json (2.1.0)
|
json (2.1.0)
|
||||||
|
json-schema (2.8.0)
|
||||||
|
addressable (>= 2.4)
|
||||||
jsonapi-renderer (0.2.0)
|
jsonapi-renderer (0.2.0)
|
||||||
kaminari (1.1.1)
|
kaminari (1.1.1)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
|
@ -499,6 +509,7 @@ DEPENDENCIES
|
||||||
active_model_serializers (~> 0.10.0)
|
active_model_serializers (~> 0.10.0)
|
||||||
acts_as_tree
|
acts_as_tree
|
||||||
acts_as_versioned!
|
acts_as_versioned!
|
||||||
|
apivore
|
||||||
attribute_normalizer
|
attribute_normalizer
|
||||||
better_errors
|
better_errors
|
||||||
binding_of_caller
|
binding_of_caller
|
||||||
|
@ -523,6 +534,7 @@ DEPENDENCIES
|
||||||
gaffe
|
gaffe
|
||||||
haml (~> 4.0)
|
haml (~> 4.0)
|
||||||
haml-rails
|
haml-rails
|
||||||
|
hashie (~> 3.4.6)
|
||||||
i18n-js (~> 3.0.0.rc8)
|
i18n-js (~> 3.0.0.rc8)
|
||||||
i18n-spec
|
i18n-spec
|
||||||
ice_cube
|
ice_cube
|
||||||
|
|
92
doc/API.md
Normal file
92
doc/API.md
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
# Foodsoft API
|
||||||
|
|
||||||
|
Foodsoft provides a JSON REST API that gives access to operations like
|
||||||
|
like listing open orders, updating the ordergroup's order, and listing financial
|
||||||
|
transactions. Not all Foodsoft functionality is available through the API, but
|
||||||
|
we're open for new additions.
|
||||||
|
|
||||||
|
The API is documented using [Open API 2.0](https://github.com/OAI/OpenAPI-Specification)
|
||||||
|
/ [Swagger](https://swagger.io/) in [swagger.v1.yml](swagger.v1.yml).
|
||||||
|
This provides a machine-readable reference that is used to provide documentation.
|
||||||
|
|
||||||
|
## API endpoint documentation
|
||||||
|
|
||||||
|
>> [View API documentation](http://petstore.swagger.io/?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffoodcoops%2Ffoodsoft%2Fmaster%2Fdoc%2Fswagger.v1.yml) <<
|
||||||
|
|
||||||
|
The above documentation can communicate with the API directly on a local development
|
||||||
|
installation of Foodsoft at [http://localhost:3000/f](http://localhost:3000/f).
|
||||||
|
|
||||||
|
You'll need to give access to the application first. This can be done by going to
|
||||||
|
_Administration_ > _Configuration_ > _Apps_ in Foodsoft. Select _New Application_,
|
||||||
|
enter any name, put `http://petstore.swagger.io/oauth2-redirect.html` in _Redirect URI_
|
||||||
|
and disable _Confidential_. After submission, you will have an _Application UID_ that
|
||||||
|
you can enter that as `client_id` after clicking _Authorize_ in the Swagger UI.
|
||||||
|
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Uses the [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper) gem,
|
||||||
|
which provides an OAuth2 provider.
|
||||||
|
|
||||||
|
|
||||||
|
### Authorization code flow
|
||||||
|
|
||||||
|
This is the recommended flow for server-side web applications, where
|
||||||
|
members login with Foodsoft, then redirected to the app, which then obtains
|
||||||
|
an access token using the authorization code supplied at redirection.
|
||||||
|
|
||||||
|
Before you can obtain an access token, the client needs to obtain an id and secret.
|
||||||
|
(You can currently skip this for the password credentials flow.) This needs to be
|
||||||
|
done for each Foodsoft scope by an admin.
|
||||||
|
|
||||||
|
1. Click on the _Apps_ button at the right in Foodsoft's configuration screen.
|
||||||
|
2. Click on _New application_
|
||||||
|
3. Enter any _Name_ and put the website of your app in _Redirect URI_ and _Submit_.
|
||||||
|
4. Click on the new applications' name for the app id and secret.
|
||||||
|
5. To quickly test, logging into the app, press _Authorize_.
|
||||||
|
|
||||||
|
Note that the user doesn't need to confirm that he is giving the app access to his
|
||||||
|
Foodsoft account by default, since apps can only be created by admins. If you
|
||||||
|
want to change that, see disable `skip_authorization` in `config/initializers/doorkeeper.rb`.
|
||||||
|
|
||||||
|
[Read more](https://github.com/doorkeeper-gem/doorkeeper/wiki/Authorization-Code-Flow).
|
||||||
|
|
||||||
|
|
||||||
|
### Implicit flow
|
||||||
|
|
||||||
|
This is the recommended flow for client-side web applications. It looks a lot
|
||||||
|
like the authorization code flow, but when redirecting back to the app, the
|
||||||
|
access token is available directly as part of the url _fragment_ (`window.location.hash`).
|
||||||
|
|
||||||
|
This flow also needs to be registered in Foodsoft as in the authorization code flow,
|
||||||
|
but with _Confidential_ disabled. You only need the `client_id`, not the secret.
|
||||||
|
|
||||||
|
**note** please make sure you understand sections
|
||||||
|
[4.4.2](http://tools.ietf.org/html/rfc6819#section-4.4.2) and
|
||||||
|
[4.4.3](http://tools.ietf.org/html/rfc6819#section-4.4.3) of the OAuth2 Threat
|
||||||
|
Model document before using this flow.
|
||||||
|
|
||||||
|
You may find Doorkeeper's [implicit_grant_test](https://github.com/doorkeeper-gem/doorkeeper/blob/master/spec/requests/flows/implicit_grant_spec.rb) useful.
|
||||||
|
|
||||||
|
|
||||||
|
### Password credentials flow
|
||||||
|
|
||||||
|
To obtain a token using a username/password directly, you can do this:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
require 'oauth2'
|
||||||
|
c = OAuth2::Client.new('client_id', 'secret', site: 'http://localhost:3002/f/', authorize_url: 'oauth/authorize', token_url: 'oauth/token')
|
||||||
|
c.password.get_token('admin', 'secret').token
|
||||||
|
# => "1234567890abcdef1234567890abcdef1234567890abcdef123456790abcdef1"
|
||||||
|
```
|
||||||
|
|
||||||
|
Now use this token as value for the `access_token` when accessing the API, like
|
||||||
|
http://localhost:3002/f/api/v1/financial_transactions/1?access_token=12345...
|
||||||
|
|
||||||
|
[Read more](https://github.com/doorkeeper-gem/doorkeeper/wiki/Client-Credentials-flow).
|
||||||
|
|
||||||
|
|
||||||
|
## Logout
|
||||||
|
|
||||||
|
When the user logs out of Foodsoft, all access tokens are destroyed, except when
|
||||||
|
the token's scope includes `offline_access` (so offline applications are possible).
|
67
doc/swagger.v1.yml
Normal file
67
doc/swagger.v1.yml
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
swagger: '2.0'
|
||||||
|
info:
|
||||||
|
title: Foodsoft API v1
|
||||||
|
version: '1.0.0'
|
||||||
|
description: >
|
||||||
|
[Foodsoft](https://github.com/foodcoops/foodsoft) is web-based software to manage
|
||||||
|
a non-profit food coop (product catalog, ordering, accounting, job scheduling).
|
||||||
|
|
||||||
|
|
||||||
|
This is a description of Foodsoft's API v1.
|
||||||
|
|
||||||
|
|
||||||
|
Note that each food cooperative typically has their own instance (on a shared
|
||||||
|
server or their own installation), and there are just as many APIs (if the Foodsoft
|
||||||
|
version is recent enough).
|
||||||
|
This API description points to the default development url with the default
|
||||||
|
Foodsoft scope - that would be [http://localhost:3000/f](http://localhost:3000/f).
|
||||||
|
externalDocs:
|
||||||
|
description: General Foodsoft API documentation
|
||||||
|
url: https://github.com/foodcoops/foodsoft/blob/master/doc/API.md
|
||||||
|
|
||||||
|
# development url with default scope
|
||||||
|
host: localhost:3000
|
||||||
|
schemes:
|
||||||
|
- 'http'
|
||||||
|
basePath: /f/api/v1
|
||||||
|
|
||||||
|
produces:
|
||||||
|
- 'application/json'
|
||||||
|
|
||||||
|
paths:
|
||||||
|
|
||||||
|
definitions:
|
||||||
|
Error:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: error code
|
||||||
|
error_description:
|
||||||
|
type: string
|
||||||
|
description: human-readable error message (localized)
|
||||||
|
Error404:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: '<tt>not_found</tt>'
|
||||||
|
error_description:
|
||||||
|
$ref: '#/definitions/Error/properties/error_description'
|
||||||
|
Error401:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
description: '<tt>unauthorized</tt>'
|
||||||
|
error_description:
|
||||||
|
$ref: '#/definitions/Error/properties/error_description'
|
||||||
|
|
||||||
|
securityDefinitions:
|
||||||
|
foodsoft_auth:
|
||||||
|
type: oauth2
|
||||||
|
flow: implicit
|
||||||
|
authorizationUrl: http://localhost:3000/f/oauth/authorize
|
||||||
|
scopes:
|
||||||
|
all: full access to user functions
|
||||||
|
offline_access: retain access after user has logged out
|
19
spec/api/v1/swagger_spec.rb
Normal file
19
spec/api/v1/swagger_spec.rb
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require 'apivore'
|
||||||
|
|
||||||
|
# we want to load a local file in YAML-format instead of a served JSON file
|
||||||
|
class SwaggerCheckerFile < Apivore::SwaggerChecker
|
||||||
|
def fetch_swagger!
|
||||||
|
YAML.load(File.read(swagger_path))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'API v1', type: :apivore, order: :defined do
|
||||||
|
include ApiHelper
|
||||||
|
|
||||||
|
subject { SwaggerCheckerFile.instance_for Rails.root.join('doc', 'swagger.v1.yml') }
|
||||||
|
|
||||||
|
it 'tests all documented routes' do
|
||||||
|
is_expected.to validate_all_paths
|
||||||
|
end
|
||||||
|
end
|
15
spec/factories/doorkeeper.rb
Normal file
15
spec/factories/doorkeeper.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
require 'factory_bot'
|
||||||
|
require 'doorkeeper'
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
|
||||||
|
factory :oauth2_application, class: Doorkeeper::Application do
|
||||||
|
name { Faker::App.name }
|
||||||
|
redirect_uri 'https://example.com:1234/app'
|
||||||
|
end
|
||||||
|
|
||||||
|
factory :oauth2_access_token, class: Doorkeeper::AccessToken do
|
||||||
|
application factory: :oauth2_application
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
18
spec/support/api_helper.rb
Normal file
18
spec/support/api_helper.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module ApiHelper
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:api_access_token) { create(:oauth2_access_token, resource_owner_id: user.id).token }
|
||||||
|
let(:api_authorization) { "Bearer #{api_access_token}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Add authentication to parameters for {Swagger::RspecHelpers#validate}
|
||||||
|
# @param params [Hash] Query parameters
|
||||||
|
# @return Query parameters with authentication header
|
||||||
|
# @see Swagger::RspecHelpers#validate
|
||||||
|
def api_auth(params = {})
|
||||||
|
{'_headers' => {'Authorization' => api_authorization }}.deep_merge(params)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in a new issue