diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 12f980fa8..b9ba48301 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -11,10 +11,11 @@ servers: description: Local development components: securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT + apiKeyAuth: + type: apiKey + name: X-Api-Key + in: header + description: API key for authentication. Generate one from your account settings. schemas: Pagination: type: object @@ -222,6 +223,41 @@ components: type: string account_type: 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: type: object required: @@ -328,6 +364,32 @@ components: type: string color: 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: type: object required: @@ -596,20 +658,41 @@ components: data: "$ref": "#/components/schemas/ImportDetail" 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": get: summary: List categories tags: - Categories security: - - bearerAuth: [] + - apiKeyAuth: [] parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with read scope - name: page in: query required: false @@ -653,12 +736,6 @@ paths: "$ref": "#/components/schemas/CategoryCollection" "/api/v1/categories/{id}": parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with read scope - name: id in: path required: true @@ -670,7 +747,7 @@ paths: tags: - Categories security: - - bearerAuth: [] + - apiKeyAuth: [] responses: '200': description: subcategory retrieved with parent @@ -690,14 +767,7 @@ paths: tags: - Chats security: - - bearerAuth: [] - parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with read scope + - apiKeyAuth: [] responses: '200': description: chats listed @@ -716,14 +786,8 @@ paths: tags: - Chats security: - - bearerAuth: [] - parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with write scope + - apiKeyAuth: [] + parameters: [] responses: '201': description: chat created @@ -757,12 +821,6 @@ paths: required: true "/api/v1/chats/{id}": parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with read scope - name: id in: path required: true @@ -774,7 +832,7 @@ paths: tags: - Chats security: - - bearerAuth: [] + - apiKeyAuth: [] responses: '200': description: chat retrieved @@ -793,7 +851,7 @@ paths: tags: - Chats security: - - bearerAuth: [] + - apiKeyAuth: [] parameters: [] responses: '200': @@ -829,7 +887,7 @@ paths: tags: - Chats security: - - bearerAuth: [] + - apiKeyAuth: [] responses: '204': description: chat deleted @@ -837,12 +895,6 @@ paths: description: chat not found "/api/v1/chats/{chat_id}/messages": parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with write scope - name: chat_id in: path required: true @@ -854,7 +906,7 @@ paths: tags: - Chat Messages security: - - bearerAuth: [] + - apiKeyAuth: [] parameters: [] responses: '201': @@ -890,12 +942,6 @@ paths: required: true "/api/v1/chats/{chat_id}/messages/retry": parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with write scope - name: chat_id in: path required: true @@ -907,7 +953,7 @@ paths: tags: - Chat Messages security: - - bearerAuth: [] + - apiKeyAuth: [] responses: '202': description: retry started @@ -934,14 +980,8 @@ paths: tags: - Imports security: - - bearerAuth: [] + - apiKeyAuth: [] parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with read scope - name: page in: query required: false @@ -993,14 +1033,8 @@ paths: tags: - Imports security: - - bearerAuth: [] - parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with write scope + - apiKeyAuth: [] + parameters: [] responses: '201': description: import created @@ -1085,12 +1119,6 @@ paths: required: true "/api/v1/imports/{id}": parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with read scope - name: id in: path required: true @@ -1104,7 +1132,7 @@ paths: tags: - Imports security: - - bearerAuth: [] + - apiKeyAuth: [] responses: '200': description: import retrieved @@ -1118,20 +1146,141 @@ paths: application/json: schema: "$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": get: summary: List transactions tags: - Transactions security: - - bearerAuth: [] + - apiKeyAuth: [] parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with read scope - name: page in: query required: false @@ -1247,14 +1396,8 @@ paths: tags: - Transactions security: - - bearerAuth: [] - parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token with write scope + - apiKeyAuth: [] + parameters: [] responses: '201': description: transaction created @@ -1332,12 +1475,6 @@ paths: required: true "/api/v1/transactions/{id}": parameters: - - name: Authorization - in: header - required: true - schema: - type: string - description: Bearer token - name: id in: path required: true @@ -1349,7 +1486,7 @@ paths: tags: - Transactions security: - - bearerAuth: [] + - apiKeyAuth: [] responses: '200': description: transaction retrieved @@ -1368,7 +1505,7 @@ paths: tags: - Transactions security: - - bearerAuth: [] + - apiKeyAuth: [] parameters: [] responses: '200': @@ -1431,7 +1568,7 @@ paths: tags: - Transactions security: - - bearerAuth: [] + - apiKeyAuth: [] responses: '200': description: transaction deleted diff --git a/spec/requests/api/v1/accounts_spec.rb b/spec/requests/api/v1/accounts_spec.rb new file mode 100644 index 000000000..00528841d --- /dev/null +++ b/spec/requests/api/v1/accounts_spec.rb @@ -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 diff --git a/spec/requests/api/v1/categories_spec.rb b/spec/requests/api/v1/categories_spec.rb index ded5ca080..98584e8af 100644 --- a/spec/requests/api/v1/categories_spec.rb +++ b/spec/requests/api/v1/categories_spec.rb @@ -20,25 +20,18 @@ RSpec.describe 'API V1 Categories', type: :request do ) end - let(:oauth_application) do - Doorkeeper::Application.create!( - name: 'API Docs', - redirect_uri: 'https://example.com/callback', - scopes: 'read read_write' + 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(:access_token) do - 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(:'X-Api-Key') { api_key.plain_key } let!(:parent_category) do family.categories.create!( @@ -71,10 +64,8 @@ RSpec.describe 'API V1 Categories', type: :request do path '/api/v1/categories' do get 'List categories' do tags 'Categories' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] 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, description: 'Page number (default: 1)' parameter name: :per_page, in: :query, type: :integer, required: false, @@ -141,13 +132,11 @@ RSpec.describe 'API V1 Categories', type: :request do end 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' get 'Retrieve a category' do tags 'Categories' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] produces 'application/json' let(:id) { parent_category.id } diff --git a/spec/requests/api/v1/chats_spec.rb b/spec/requests/api/v1/chats_spec.rb index 62434895b..96af30f17 100644 --- a/spec/requests/api/v1/chats_spec.rb +++ b/spec/requests/api/v1/chats_spec.rb @@ -21,25 +21,18 @@ RSpec.describe 'API V1 Chats', type: :request do ) end - let(:oauth_application) do - Doorkeeper::Application.create!( - name: 'API Docs', - redirect_uri: 'https://example.com/callback', - scopes: 'read read_write' + 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(:access_token) do - 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(:'X-Api-Key') { api_key.plain_key } let!(:chat) do 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 get 'List chats' do tags 'Chats' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] produces 'application/json' - parameter name: :Authorization, in: :header, required: true, schema: { type: :string }, - description: 'Bearer token with read scope' response '200', 'chats listed' do schema '$ref' => '#/components/schemas/ChatCollection' @@ -117,11 +108,9 @@ RSpec.describe 'API V1 Chats', type: :request do post 'Create chat' do tags 'Chats' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] consumes '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: { type: :object, properties: { @@ -161,13 +150,11 @@ RSpec.describe 'API V1 Chats', type: :request do end 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' get 'Retrieve a chat' do tags 'Chats' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] produces 'application/json' let(:id) { chat.id } @@ -192,7 +179,7 @@ RSpec.describe 'API V1 Chats', type: :request do patch 'Update a chat' do tags 'Chats' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] consumes 'application/json' produces 'application/json' @@ -235,7 +222,7 @@ RSpec.describe 'API V1 Chats', type: :request do delete 'Delete a chat' do tags 'Chats' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] produces 'application/json' let(:id) { another_chat.id } @@ -253,13 +240,11 @@ RSpec.describe 'API V1 Chats', type: :request do end 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' post 'Create a message' do tags 'Chat Messages' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] consumes 'application/json' produces 'application/json' @@ -309,13 +294,11 @@ RSpec.describe 'API V1 Chats', type: :request do end 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' post 'Retry the last assistant response' do tags 'Chat Messages' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] produces 'application/json' let(:chat_id) { chat.id } diff --git a/spec/requests/api/v1/imports_spec.rb b/spec/requests/api/v1/imports_spec.rb index c5d01d499..4ed8e7431 100644 --- a/spec/requests/api/v1/imports_spec.rb +++ b/spec/requests/api/v1/imports_spec.rb @@ -20,25 +20,18 @@ RSpec.describe 'API V1 Imports', type: :request do ) end - let(:oauth_application) do - Doorkeeper::Application.create!( - name: 'API Docs', - redirect_uri: 'https://example.com/callback', - scopes: 'read read_write' + 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(:access_token) do - 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(:'X-Api-Key') { api_key.plain_key } let(:account) do Account.create!( @@ -72,10 +65,8 @@ RSpec.describe 'API V1 Imports', type: :request do get 'List imports' do description 'List all imports for the user\'s family with pagination and filtering.' tags 'Imports' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] 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, description: 'Page number (default: 1)' 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 description 'Create a new import from raw CSV content.' tags 'Imports' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] consumes '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: { type: :object, @@ -238,14 +227,12 @@ RSpec.describe 'API V1 Imports', type: :request do end 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' get 'Retrieve an import' do description 'Retrieve detailed information about a specific import, including configuration and row statistics.' tags 'Imports' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] produces 'application/json' let(:id) { pending_import.id } diff --git a/spec/requests/api/v1/tags_spec.rb b/spec/requests/api/v1/tags_spec.rb new file mode 100644 index 000000000..3e723489f --- /dev/null +++ b/spec/requests/api/v1/tags_spec.rb @@ -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 diff --git a/spec/requests/api/v1/transactions_spec.rb b/spec/requests/api/v1/transactions_spec.rb index befcbf0c7..c9d8823e2 100644 --- a/spec/requests/api/v1/transactions_spec.rb +++ b/spec/requests/api/v1/transactions_spec.rb @@ -20,25 +20,18 @@ RSpec.describe 'API V1 Transactions', type: :request do ) end - let(:oauth_application) do - Doorkeeper::Application.create!( - name: 'API Docs', - redirect_uri: 'https://example.com/callback', - scopes: 'read read_write' + 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(:access_token) do - 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(:'X-Api-Key') { api_key.plain_key } let(:account) do Account.create!( @@ -96,10 +89,8 @@ RSpec.describe 'API V1 Transactions', type: :request do path '/api/v1/transactions' do get 'List transactions' do tags 'Transactions' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] 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, description: 'Page number (default: 1)' 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 tags 'Transactions' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] consumes '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: { type: :object, properties: { @@ -260,13 +249,11 @@ RSpec.describe 'API V1 Transactions', type: :request do end 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' get 'Retrieve a transaction' do tags 'Transactions' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] produces 'application/json' let(:id) { transaction.id } @@ -295,7 +282,7 @@ RSpec.describe 'API V1 Transactions', type: :request do patch 'Update a transaction' do tags 'Transactions' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] consumes 'application/json' produces 'application/json' @@ -352,7 +339,7 @@ RSpec.describe 'API V1 Transactions', type: :request do delete 'Delete a transaction' do tags 'Transactions' - security [ { bearerAuth: [] } ] + security [ { apiKeyAuth: [] } ] produces 'application/json' let(:id) { another_transaction.id } diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index 6bdc27146..5e54cfefd 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -25,10 +25,11 @@ RSpec.configure do |config| ], components: { securitySchemes: { - bearerAuth: { - type: :http, - scheme: :bearer, - bearerFormat: :JWT + apiKeyAuth: { + type: :apiKey, + name: 'X-Api-Key', + in: :header, + description: 'API key for authentication. Generate one from your account settings.' } }, schemas: { @@ -171,6 +172,29 @@ RSpec.configure do |config| 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: { type: :object, required: %w[id name classification color icon], @@ -233,6 +257,21 @@ RSpec.configure do |config| 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: { type: :object, required: %w[id amount currency],