API documentation and spec setup

This commit is contained in:
wvengen 2018-10-15 16:47:14 +02:00 committed by wvengen
parent 99ecb75c83
commit 900cc91197
7 changed files with 226 additions and 0 deletions

View file

@ -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

View file

@ -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
View 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
&gt;&gt; [View API documentation](http://petstore.swagger.io/?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffoodcoops%2Ffoodsoft%2Fmaster%2Fdoc%2Fswagger.v1.yml) &lt;&lt;
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
View 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

View 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

View 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

View 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