FIX OpenAPI auth specs (#722)

* FIX auth specs

* FIX header params are not required with auth spec

* Add missing endpoints
This commit is contained in:
soky srm
2026-01-21 11:10:03 +01:00
committed by GitHub
parent d8cdced662
commit ae61df4978
8 changed files with 673 additions and 214 deletions

View File

@@ -11,10 +11,11 @@ servers:
description: Local development description: Local development
components: components:
securitySchemes: securitySchemes:
bearerAuth: apiKeyAuth:
type: http type: apiKey
scheme: bearer name: X-Api-Key
bearerFormat: JWT in: header
description: API key for authentication. Generate one from your account settings.
schemas: schemas:
Pagination: Pagination:
type: object type: object
@@ -222,6 +223,41 @@ components:
type: string type: string
account_type: account_type:
type: string type: string
AccountDetail:
type: object
required:
- id
- name
- balance
- currency
- classification
- account_type
properties:
id:
type: string
format: uuid
name:
type: string
balance:
type: string
currency:
type: string
classification:
type: string
account_type:
type: string
AccountCollection:
type: object
required:
- accounts
- pagination
properties:
accounts:
type: array
items:
"$ref": "#/components/schemas/AccountDetail"
pagination:
"$ref": "#/components/schemas/Pagination"
Category: Category:
type: object type: object
required: required:
@@ -328,6 +364,32 @@ components:
type: string type: string
color: color:
type: string type: string
TagDetail:
type: object
required:
- id
- name
- color
- created_at
- updated_at
properties:
id:
type: string
format: uuid
name:
type: string
color:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
TagCollection:
type: array
items:
"$ref": "#/components/schemas/TagDetail"
Transfer: Transfer:
type: object type: object
required: required:
@@ -596,20 +658,41 @@ components:
data: data:
"$ref": "#/components/schemas/ImportDetail" "$ref": "#/components/schemas/ImportDetail"
paths: paths:
"/api/v1/accounts":
get:
summary: List accounts
tags:
- Accounts
security:
- apiKeyAuth: []
parameters:
- name: page
in: query
required: false
description: 'Page number (default: 1)'
schema:
type: integer
- name: per_page
in: query
required: false
description: 'Items per page (default: 25, max: 100)'
schema:
type: integer
responses:
'200':
description: accounts paginated
content:
application/json:
schema:
"$ref": "#/components/schemas/AccountCollection"
"/api/v1/categories": "/api/v1/categories":
get: get:
summary: List categories summary: List categories
tags: tags:
- Categories - Categories
security: security:
- bearerAuth: [] - apiKeyAuth: []
parameters: parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with read scope
- name: page - name: page
in: query in: query
required: false required: false
@@ -653,12 +736,6 @@ paths:
"$ref": "#/components/schemas/CategoryCollection" "$ref": "#/components/schemas/CategoryCollection"
"/api/v1/categories/{id}": "/api/v1/categories/{id}":
parameters: parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with read scope
- name: id - name: id
in: path in: path
required: true required: true
@@ -670,7 +747,7 @@ paths:
tags: tags:
- Categories - Categories
security: security:
- bearerAuth: [] - apiKeyAuth: []
responses: responses:
'200': '200':
description: subcategory retrieved with parent description: subcategory retrieved with parent
@@ -690,14 +767,7 @@ paths:
tags: tags:
- Chats - Chats
security: security:
- bearerAuth: [] - apiKeyAuth: []
parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with read scope
responses: responses:
'200': '200':
description: chats listed description: chats listed
@@ -716,14 +786,8 @@ paths:
tags: tags:
- Chats - Chats
security: security:
- bearerAuth: [] - apiKeyAuth: []
parameters: parameters: []
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with write scope
responses: responses:
'201': '201':
description: chat created description: chat created
@@ -757,12 +821,6 @@ paths:
required: true required: true
"/api/v1/chats/{id}": "/api/v1/chats/{id}":
parameters: parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with read scope
- name: id - name: id
in: path in: path
required: true required: true
@@ -774,7 +832,7 @@ paths:
tags: tags:
- Chats - Chats
security: security:
- bearerAuth: [] - apiKeyAuth: []
responses: responses:
'200': '200':
description: chat retrieved description: chat retrieved
@@ -793,7 +851,7 @@ paths:
tags: tags:
- Chats - Chats
security: security:
- bearerAuth: [] - apiKeyAuth: []
parameters: [] parameters: []
responses: responses:
'200': '200':
@@ -829,7 +887,7 @@ paths:
tags: tags:
- Chats - Chats
security: security:
- bearerAuth: [] - apiKeyAuth: []
responses: responses:
'204': '204':
description: chat deleted description: chat deleted
@@ -837,12 +895,6 @@ paths:
description: chat not found description: chat not found
"/api/v1/chats/{chat_id}/messages": "/api/v1/chats/{chat_id}/messages":
parameters: parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with write scope
- name: chat_id - name: chat_id
in: path in: path
required: true required: true
@@ -854,7 +906,7 @@ paths:
tags: tags:
- Chat Messages - Chat Messages
security: security:
- bearerAuth: [] - apiKeyAuth: []
parameters: [] parameters: []
responses: responses:
'201': '201':
@@ -890,12 +942,6 @@ paths:
required: true required: true
"/api/v1/chats/{chat_id}/messages/retry": "/api/v1/chats/{chat_id}/messages/retry":
parameters: parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with write scope
- name: chat_id - name: chat_id
in: path in: path
required: true required: true
@@ -907,7 +953,7 @@ paths:
tags: tags:
- Chat Messages - Chat Messages
security: security:
- bearerAuth: [] - apiKeyAuth: []
responses: responses:
'202': '202':
description: retry started description: retry started
@@ -934,14 +980,8 @@ paths:
tags: tags:
- Imports - Imports
security: security:
- bearerAuth: [] - apiKeyAuth: []
parameters: parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with read scope
- name: page - name: page
in: query in: query
required: false required: false
@@ -993,14 +1033,8 @@ paths:
tags: tags:
- Imports - Imports
security: security:
- bearerAuth: [] - apiKeyAuth: []
parameters: parameters: []
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with write scope
responses: responses:
'201': '201':
description: import created description: import created
@@ -1085,12 +1119,6 @@ paths:
required: true required: true
"/api/v1/imports/{id}": "/api/v1/imports/{id}":
parameters: parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with read scope
- name: id - name: id
in: path in: path
required: true required: true
@@ -1104,7 +1132,7 @@ paths:
tags: tags:
- Imports - Imports
security: security:
- bearerAuth: [] - apiKeyAuth: []
responses: responses:
'200': '200':
description: import retrieved description: import retrieved
@@ -1118,20 +1146,141 @@ paths:
application/json: application/json:
schema: schema:
"$ref": "#/components/schemas/ErrorResponse" "$ref": "#/components/schemas/ErrorResponse"
"/api/v1/tags":
get:
summary: List tags
tags:
- Tags
security:
- apiKeyAuth: []
responses:
'200':
description: tags listed
content:
application/json:
schema:
"$ref": "#/components/schemas/TagCollection"
post:
summary: Create tag
tags:
- Tags
security:
- apiKeyAuth: []
parameters: []
responses:
'201':
description: tag created with auto-assigned color
content:
application/json:
schema:
"$ref": "#/components/schemas/TagDetail"
'422':
description: validation error - missing name
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
requestBody:
content:
application/json:
schema:
type: object
properties:
tag:
type: object
properties:
name:
type: string
description: Tag name (required)
color:
type: string
description: Hex color code (optional, auto-assigned if not
provided)
required:
- name
required:
- tag
required: true
"/api/v1/tags/{id}":
parameters:
- name: id
in: path
required: true
description: Tag ID
schema:
type: string
get:
summary: Retrieve a tag
tags:
- Tags
security:
- apiKeyAuth: []
responses:
'200':
description: tag retrieved
content:
application/json:
schema:
"$ref": "#/components/schemas/TagDetail"
'404':
description: tag not found
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
patch:
summary: Update a tag
tags:
- Tags
security:
- apiKeyAuth: []
parameters: []
responses:
'200':
description: tag updated
content:
application/json:
schema:
"$ref": "#/components/schemas/TagDetail"
'404':
description: tag not found
content:
application/json:
schema:
"$ref": "#/components/schemas/ErrorResponse"
requestBody:
content:
application/json:
schema:
type: object
properties:
tag:
type: object
properties:
name:
type: string
color:
type: string
required: true
delete:
summary: Delete a tag
tags:
- Tags
security:
- apiKeyAuth: []
responses:
'204':
description: tag deleted
'404':
description: tag not found
"/api/v1/transactions": "/api/v1/transactions":
get: get:
summary: List transactions summary: List transactions
tags: tags:
- Transactions - Transactions
security: security:
- bearerAuth: [] - apiKeyAuth: []
parameters: parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with read scope
- name: page - name: page
in: query in: query
required: false required: false
@@ -1247,14 +1396,8 @@ paths:
tags: tags:
- Transactions - Transactions
security: security:
- bearerAuth: [] - apiKeyAuth: []
parameters: parameters: []
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token with write scope
responses: responses:
'201': '201':
description: transaction created description: transaction created
@@ -1332,12 +1475,6 @@ paths:
required: true required: true
"/api/v1/transactions/{id}": "/api/v1/transactions/{id}":
parameters: parameters:
- name: Authorization
in: header
required: true
schema:
type: string
description: Bearer token
- name: id - name: id
in: path in: path
required: true required: true
@@ -1349,7 +1486,7 @@ paths:
tags: tags:
- Transactions - Transactions
security: security:
- bearerAuth: [] - apiKeyAuth: []
responses: responses:
'200': '200':
description: transaction retrieved description: transaction retrieved
@@ -1368,7 +1505,7 @@ paths:
tags: tags:
- Transactions - Transactions
security: security:
- bearerAuth: [] - apiKeyAuth: []
parameters: [] parameters: []
responses: responses:
'200': '200':
@@ -1431,7 +1568,7 @@ paths:
tags: tags:
- Transactions - Transactions
security: security:
- bearerAuth: [] - apiKeyAuth: []
responses: responses:
'200': '200':
description: transaction deleted description: transaction deleted

View File

@@ -0,0 +1,102 @@
# frozen_string_literal: true
require 'swagger_helper'
RSpec.describe 'API V1 Accounts', type: :request do
let(:family) do
Family.create!(
name: 'API Family',
currency: 'USD',
locale: 'en',
date_format: '%m-%d-%Y'
)
end
let(:user) do
family.users.create!(
email: 'api-user@example.com',
password: 'password123',
password_confirmation: 'password123'
)
end
let(:api_key) do
key = ApiKey.generate_secure_key
ApiKey.create!(
user: user,
name: 'API Docs Key',
key: key,
scopes: %w[read_write],
source: 'web'
)
end
let(:'X-Api-Key') { api_key.plain_key }
let!(:checking_account) do
Account.create!(
family: family,
name: 'Checking Account',
balance: 1500.50,
currency: 'USD',
accountable: Depository.create!
)
end
let!(:savings_account) do
Account.create!(
family: family,
name: 'Savings Account',
balance: 10000.00,
currency: 'USD',
accountable: Depository.create!
)
end
let!(:credit_card) do
Account.create!(
family: family,
name: 'Credit Card',
balance: -500.00,
currency: 'USD',
accountable: CreditCard.create!
)
end
path '/api/v1/accounts' do
get 'List accounts' do
tags 'Accounts'
security [ { apiKeyAuth: [] } ]
produces 'application/json'
parameter name: :page, in: :query, type: :integer, required: false,
description: 'Page number (default: 1)'
parameter name: :per_page, in: :query, type: :integer, required: false,
description: 'Items per page (default: 25, max: 100)'
response '200', 'accounts listed' do
schema '$ref' => '#/components/schemas/AccountCollection'
run_test! do |response|
payload = JSON.parse(response.body)
expect(payload.fetch('accounts')).to be_present
expect(payload.fetch('accounts').length).to eq(3)
expect(payload.fetch('pagination')).to include('page', 'per_page', 'total_count', 'total_pages')
end
end
response '200', 'accounts paginated' do
schema '$ref' => '#/components/schemas/AccountCollection'
let(:page) { 1 }
let(:per_page) { 2 }
run_test! do |response|
payload = JSON.parse(response.body)
expect(payload.fetch('accounts').length).to eq(2)
expect(payload.dig('pagination', 'per_page')).to eq(2)
expect(payload.dig('pagination', 'total_count')).to eq(3)
end
end
end
end
end

View File

@@ -20,25 +20,18 @@ RSpec.describe 'API V1 Categories', type: :request do
) )
end end
let(:oauth_application) do let(:api_key) do
Doorkeeper::Application.create!( key = ApiKey.generate_secure_key
name: 'API Docs', ApiKey.create!(
redirect_uri: 'https://example.com/callback', user: user,
scopes: 'read read_write' name: 'API Docs Key',
key: key,
scopes: %w[read_write],
source: 'web'
) )
end end
let(:access_token) do let(:'X-Api-Key') { api_key.plain_key }
Doorkeeper::AccessToken.create!(
application: oauth_application,
resource_owner_id: user.id,
scopes: 'read_write',
expires_in: 2.hours,
token: SecureRandom.hex(32)
)
end
let(:Authorization) { "Bearer #{access_token.token}" }
let!(:parent_category) do let!(:parent_category) do
family.categories.create!( family.categories.create!(
@@ -71,10 +64,8 @@ RSpec.describe 'API V1 Categories', type: :request do
path '/api/v1/categories' do path '/api/v1/categories' do
get 'List categories' do get 'List categories' do
tags 'Categories' tags 'Categories'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with read scope'
parameter name: :page, in: :query, type: :integer, required: false, parameter name: :page, in: :query, type: :integer, required: false,
description: 'Page number (default: 1)' description: 'Page number (default: 1)'
parameter name: :per_page, in: :query, type: :integer, required: false, parameter name: :per_page, in: :query, type: :integer, required: false,
@@ -141,13 +132,11 @@ RSpec.describe 'API V1 Categories', type: :request do
end end
path '/api/v1/categories/{id}' do path '/api/v1/categories/{id}' do
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with read scope'
parameter name: :id, in: :path, type: :string, required: true, description: 'Category ID' parameter name: :id, in: :path, type: :string, required: true, description: 'Category ID'
get 'Retrieve a category' do get 'Retrieve a category' do
tags 'Categories' tags 'Categories'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
let(:id) { parent_category.id } let(:id) { parent_category.id }

View File

@@ -21,25 +21,18 @@ RSpec.describe 'API V1 Chats', type: :request do
) )
end end
let(:oauth_application) do let(:api_key) do
Doorkeeper::Application.create!( key = ApiKey.generate_secure_key
name: 'API Docs', ApiKey.create!(
redirect_uri: 'https://example.com/callback', user: user,
scopes: 'read read_write' name: 'API Docs Key',
key: key,
scopes: %w[read_write],
source: 'web'
) )
end end
let(:access_token) do let(:'X-Api-Key') { api_key.plain_key }
Doorkeeper::AccessToken.create!(
application: oauth_application,
resource_owner_id: user.id,
scopes: 'read_write',
expires_in: 2.hours,
token: SecureRandom.hex(32)
)
end
let(:Authorization) { "Bearer #{access_token.token}" }
let!(:chat) do let!(:chat) do
user.chats.create!(title: 'Budget planning').tap do |record| user.chats.create!(title: 'Budget planning').tap do |record|
@@ -84,10 +77,8 @@ RSpec.describe 'API V1 Chats', type: :request do
path '/api/v1/chats' do path '/api/v1/chats' do
get 'List chats' do get 'List chats' do
tags 'Chats' tags 'Chats'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with read scope'
response '200', 'chats listed' do response '200', 'chats listed' do
schema '$ref' => '#/components/schemas/ChatCollection' schema '$ref' => '#/components/schemas/ChatCollection'
@@ -117,11 +108,9 @@ RSpec.describe 'API V1 Chats', type: :request do
post 'Create chat' do post 'Create chat' do
tags 'Chats' tags 'Chats'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
consumes 'application/json' consumes 'application/json'
produces 'application/json' produces 'application/json'
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with write scope'
parameter name: :chat_params, in: :body, required: true, schema: { parameter name: :chat_params, in: :body, required: true, schema: {
type: :object, type: :object,
properties: { properties: {
@@ -161,13 +150,11 @@ RSpec.describe 'API V1 Chats', type: :request do
end end
path '/api/v1/chats/{id}' do path '/api/v1/chats/{id}' do
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with read scope'
parameter name: :id, in: :path, type: :string, required: true, description: 'Chat ID' parameter name: :id, in: :path, type: :string, required: true, description: 'Chat ID'
get 'Retrieve a chat' do get 'Retrieve a chat' do
tags 'Chats' tags 'Chats'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
let(:id) { chat.id } let(:id) { chat.id }
@@ -192,7 +179,7 @@ RSpec.describe 'API V1 Chats', type: :request do
patch 'Update a chat' do patch 'Update a chat' do
tags 'Chats' tags 'Chats'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
consumes 'application/json' consumes 'application/json'
produces 'application/json' produces 'application/json'
@@ -235,7 +222,7 @@ RSpec.describe 'API V1 Chats', type: :request do
delete 'Delete a chat' do delete 'Delete a chat' do
tags 'Chats' tags 'Chats'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
let(:id) { another_chat.id } let(:id) { another_chat.id }
@@ -253,13 +240,11 @@ RSpec.describe 'API V1 Chats', type: :request do
end end
path '/api/v1/chats/{chat_id}/messages' do path '/api/v1/chats/{chat_id}/messages' do
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with write scope'
parameter name: :chat_id, in: :path, type: :string, required: true, description: 'Chat ID' parameter name: :chat_id, in: :path, type: :string, required: true, description: 'Chat ID'
post 'Create a message' do post 'Create a message' do
tags 'Chat Messages' tags 'Chat Messages'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
consumes 'application/json' consumes 'application/json'
produces 'application/json' produces 'application/json'
@@ -309,13 +294,11 @@ RSpec.describe 'API V1 Chats', type: :request do
end end
path '/api/v1/chats/{chat_id}/messages/retry' do path '/api/v1/chats/{chat_id}/messages/retry' do
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with write scope'
parameter name: :chat_id, in: :path, type: :string, required: true, description: 'Chat ID' parameter name: :chat_id, in: :path, type: :string, required: true, description: 'Chat ID'
post 'Retry the last assistant response' do post 'Retry the last assistant response' do
tags 'Chat Messages' tags 'Chat Messages'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
let(:chat_id) { chat.id } let(:chat_id) { chat.id }

View File

@@ -20,25 +20,18 @@ RSpec.describe 'API V1 Imports', type: :request do
) )
end end
let(:oauth_application) do let(:api_key) do
Doorkeeper::Application.create!( key = ApiKey.generate_secure_key
name: 'API Docs', ApiKey.create!(
redirect_uri: 'https://example.com/callback', user: user,
scopes: 'read read_write' name: 'API Docs Key',
key: key,
scopes: %w[read_write],
source: 'web'
) )
end end
let(:access_token) do let(:'X-Api-Key') { api_key.plain_key }
Doorkeeper::AccessToken.create!(
application: oauth_application,
resource_owner_id: user.id,
scopes: 'read_write',
expires_in: 2.hours,
token: SecureRandom.hex(32)
)
end
let(:Authorization) { "Bearer #{access_token.token}" }
let(:account) do let(:account) do
Account.create!( Account.create!(
@@ -72,10 +65,8 @@ RSpec.describe 'API V1 Imports', type: :request do
get 'List imports' do get 'List imports' do
description 'List all imports for the user\'s family with pagination and filtering.' description 'List all imports for the user\'s family with pagination and filtering.'
tags 'Imports' tags 'Imports'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with read scope'
parameter name: :page, in: :query, type: :integer, required: false, parameter name: :page, in: :query, type: :integer, required: false,
description: 'Page number (default: 1)' description: 'Page number (default: 1)'
parameter name: :per_page, in: :query, type: :integer, required: false, parameter name: :per_page, in: :query, type: :integer, required: false,
@@ -127,11 +118,9 @@ RSpec.describe 'API V1 Imports', type: :request do
post 'Create import' do post 'Create import' do
description 'Create a new import from raw CSV content.' description 'Create a new import from raw CSV content.'
tags 'Imports' tags 'Imports'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
consumes 'application/json' consumes 'application/json'
produces 'application/json' produces 'application/json'
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with write scope'
parameter name: :body, in: :body, required: true, schema: { parameter name: :body, in: :body, required: true, schema: {
type: :object, type: :object,
@@ -238,14 +227,12 @@ RSpec.describe 'API V1 Imports', type: :request do
end end
path '/api/v1/imports/{id}' do path '/api/v1/imports/{id}' do
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with read scope'
parameter name: :id, in: :path, type: :string, required: true, description: 'Import ID' parameter name: :id, in: :path, type: :string, required: true, description: 'Import ID'
get 'Retrieve an import' do get 'Retrieve an import' do
description 'Retrieve detailed information about a specific import, including configuration and row statistics.' description 'Retrieve detailed information about a specific import, including configuration and row statistics.'
tags 'Imports' tags 'Imports'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
let(:id) { pending_import.id } let(:id) { pending_import.id }

View File

@@ -0,0 +1,235 @@
# frozen_string_literal: true
require 'swagger_helper'
RSpec.describe 'API V1 Tags', type: :request do
let(:family) do
Family.create!(
name: 'API Family',
currency: 'USD',
locale: 'en',
date_format: '%m-%d-%Y'
)
end
let(:user) do
family.users.create!(
email: 'api-user@example.com',
password: 'password123',
password_confirmation: 'password123'
)
end
let(:api_key) do
key = ApiKey.generate_secure_key
ApiKey.create!(
user: user,
name: 'API Docs Key',
key: key,
scopes: %w[read_write],
source: 'web'
)
end
let(:'X-Api-Key') { api_key.plain_key }
let!(:essential_tag) do
family.tags.create!(name: 'Essential', color: '#22c55e')
end
let!(:discretionary_tag) do
family.tags.create!(name: 'Discretionary', color: '#f97316')
end
let!(:recurring_tag) do
family.tags.create!(name: 'Recurring', color: '#3b82f6')
end
path '/api/v1/tags' do
get 'List tags' do
tags 'Tags'
security [ { apiKeyAuth: [] } ]
produces 'application/json'
response '200', 'tags listed' do
schema '$ref' => '#/components/schemas/TagCollection'
run_test! do |response|
payload = JSON.parse(response.body)
expect(payload).to be_an(Array)
expect(payload.length).to eq(3)
expect(payload.first).to include('id', 'name', 'color', 'created_at', 'updated_at')
end
end
end
post 'Create tag' do
tags 'Tags'
security [ { apiKeyAuth: [] } ]
consumes 'application/json'
produces 'application/json'
parameter name: :body, in: :body, required: true, schema: {
type: :object,
properties: {
tag: {
type: :object,
properties: {
name: { type: :string, description: 'Tag name (required)' },
color: { type: :string, description: 'Hex color code (optional, auto-assigned if not provided)' }
},
required: %w[name]
}
},
required: %w[tag]
}
response '201', 'tag created' do
schema '$ref' => '#/components/schemas/TagDetail'
let(:body) do
{
tag: {
name: 'Business',
color: '#8b5cf6'
}
}
end
run_test! do |response|
payload = JSON.parse(response.body)
expect(payload.fetch('name')).to eq('Business')
expect(payload.fetch('color')).to eq('#8b5cf6')
end
end
response '201', 'tag created with auto-assigned color' do
schema '$ref' => '#/components/schemas/TagDetail'
let(:body) do
{
tag: {
name: 'Travel'
}
}
end
run_test! do |response|
payload = JSON.parse(response.body)
expect(payload.fetch('name')).to eq('Travel')
expect(payload.fetch('color')).to be_present
end
end
response '422', 'validation error - missing name' do
schema '$ref' => '#/components/schemas/ErrorResponse'
let(:body) do
{
tag: {
color: '#8b5cf6'
}
}
end
run_test!
end
end
end
path '/api/v1/tags/{id}' do
parameter name: :id, in: :path, type: :string, required: true, description: 'Tag ID'
get 'Retrieve a tag' do
tags 'Tags'
security [ { apiKeyAuth: [] } ]
produces 'application/json'
let(:id) { essential_tag.id }
response '200', 'tag retrieved' do
schema '$ref' => '#/components/schemas/TagDetail'
run_test! do |response|
payload = JSON.parse(response.body)
expect(payload.fetch('id')).to eq(essential_tag.id)
expect(payload.fetch('name')).to eq('Essential')
expect(payload.fetch('color')).to eq('#22c55e')
end
end
response '404', 'tag not found' do
schema '$ref' => '#/components/schemas/ErrorResponse'
let(:id) { SecureRandom.uuid }
run_test!
end
end
patch 'Update a tag' do
tags 'Tags'
security [ { apiKeyAuth: [] } ]
consumes 'application/json'
produces 'application/json'
let(:id) { essential_tag.id }
parameter name: :body, in: :body, required: true, schema: {
type: :object,
properties: {
tag: {
type: :object,
properties: {
name: { type: :string },
color: { type: :string }
}
}
}
}
let(:body) do
{
tag: {
name: 'Must Have',
color: '#10b981'
}
}
end
response '200', 'tag updated' do
schema '$ref' => '#/components/schemas/TagDetail'
run_test! do |response|
payload = JSON.parse(response.body)
expect(payload.fetch('name')).to eq('Must Have')
expect(payload.fetch('color')).to eq('#10b981')
end
end
response '404', 'tag not found' do
schema '$ref' => '#/components/schemas/ErrorResponse'
let(:id) { SecureRandom.uuid }
run_test!
end
end
delete 'Delete a tag' do
tags 'Tags'
security [ { apiKeyAuth: [] } ]
let(:id) { recurring_tag.id }
response '204', 'tag deleted' do
run_test!
end
response '404', 'tag not found' do
let(:id) { SecureRandom.uuid }
run_test!
end
end
end
end

View File

@@ -20,25 +20,18 @@ RSpec.describe 'API V1 Transactions', type: :request do
) )
end end
let(:oauth_application) do let(:api_key) do
Doorkeeper::Application.create!( key = ApiKey.generate_secure_key
name: 'API Docs', ApiKey.create!(
redirect_uri: 'https://example.com/callback', user: user,
scopes: 'read read_write' name: 'API Docs Key',
key: key,
scopes: %w[read_write],
source: 'web'
) )
end end
let(:access_token) do let(:'X-Api-Key') { api_key.plain_key }
Doorkeeper::AccessToken.create!(
application: oauth_application,
resource_owner_id: user.id,
scopes: 'read_write',
expires_in: 2.hours,
token: SecureRandom.hex(32)
)
end
let(:Authorization) { "Bearer #{access_token.token}" }
let(:account) do let(:account) do
Account.create!( Account.create!(
@@ -96,10 +89,8 @@ RSpec.describe 'API V1 Transactions', type: :request do
path '/api/v1/transactions' do path '/api/v1/transactions' do
get 'List transactions' do get 'List transactions' do
tags 'Transactions' tags 'Transactions'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with read scope'
parameter name: :page, in: :query, type: :integer, required: false, parameter name: :page, in: :query, type: :integer, required: false,
description: 'Page number (default: 1)' description: 'Page number (default: 1)'
parameter name: :per_page, in: :query, type: :integer, required: false, parameter name: :per_page, in: :query, type: :integer, required: false,
@@ -174,11 +165,9 @@ RSpec.describe 'API V1 Transactions', type: :request do
post 'Create transaction' do post 'Create transaction' do
tags 'Transactions' tags 'Transactions'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
consumes 'application/json' consumes 'application/json'
produces 'application/json' produces 'application/json'
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token with write scope'
parameter name: :body, in: :body, required: true, schema: { parameter name: :body, in: :body, required: true, schema: {
type: :object, type: :object,
properties: { properties: {
@@ -260,13 +249,11 @@ RSpec.describe 'API V1 Transactions', type: :request do
end end
path '/api/v1/transactions/{id}' do path '/api/v1/transactions/{id}' do
parameter name: :Authorization, in: :header, required: true, schema: { type: :string },
description: 'Bearer token'
parameter name: :id, in: :path, type: :string, required: true, description: 'Transaction ID' parameter name: :id, in: :path, type: :string, required: true, description: 'Transaction ID'
get 'Retrieve a transaction' do get 'Retrieve a transaction' do
tags 'Transactions' tags 'Transactions'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
let(:id) { transaction.id } let(:id) { transaction.id }
@@ -295,7 +282,7 @@ RSpec.describe 'API V1 Transactions', type: :request do
patch 'Update a transaction' do patch 'Update a transaction' do
tags 'Transactions' tags 'Transactions'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
consumes 'application/json' consumes 'application/json'
produces 'application/json' produces 'application/json'
@@ -352,7 +339,7 @@ RSpec.describe 'API V1 Transactions', type: :request do
delete 'Delete a transaction' do delete 'Delete a transaction' do
tags 'Transactions' tags 'Transactions'
security [ { bearerAuth: [] } ] security [ { apiKeyAuth: [] } ]
produces 'application/json' produces 'application/json'
let(:id) { another_transaction.id } let(:id) { another_transaction.id }

View File

@@ -25,10 +25,11 @@ RSpec.configure do |config|
], ],
components: { components: {
securitySchemes: { securitySchemes: {
bearerAuth: { apiKeyAuth: {
type: :http, type: :apiKey,
scheme: :bearer, name: 'X-Api-Key',
bearerFormat: :JWT in: :header,
description: 'API key for authentication. Generate one from your account settings.'
} }
}, },
schemas: { schemas: {
@@ -171,6 +172,29 @@ RSpec.configure do |config|
account_type: { type: :string } account_type: { type: :string }
} }
}, },
AccountDetail: {
type: :object,
required: %w[id name balance currency classification account_type],
properties: {
id: { type: :string, format: :uuid },
name: { type: :string },
balance: { type: :string },
currency: { type: :string },
classification: { type: :string },
account_type: { type: :string }
}
},
AccountCollection: {
type: :object,
required: %w[accounts pagination],
properties: {
accounts: {
type: :array,
items: { '$ref' => '#/components/schemas/AccountDetail' }
},
pagination: { '$ref' => '#/components/schemas/Pagination' }
}
},
Category: { Category: {
type: :object, type: :object,
required: %w[id name classification color icon], required: %w[id name classification color icon],
@@ -233,6 +257,21 @@ RSpec.configure do |config|
color: { type: :string } color: { type: :string }
} }
}, },
TagDetail: {
type: :object,
required: %w[id name color created_at updated_at],
properties: {
id: { type: :string, format: :uuid },
name: { type: :string },
color: { type: :string },
created_at: { type: :string, format: :'date-time' },
updated_at: { type: :string, format: :'date-time' }
}
},
TagCollection: {
type: :array,
items: { '$ref' => '#/components/schemas/TagDetail' }
},
Transfer: { Transfer: {
type: :object, type: :object,
required: %w[id amount currency], required: %w[id amount currency],