mirror of
https://github.com/we-promise/sure.git
synced 2026-05-12 15:15:01 +00:00
feat(api): allow creating categories via API (#1676)
* feat(api): allow creating categories via API Adds POST /api/v1/categories so external integrations (e.g. bulk classification scripts that import already-categorized data from another system) can create categories without going through the web UI. Mirrors the existing tags create endpoint: requires the read_write scope, accepts name/color/icon/parent_id, auto-suggests an icon when omitted, and rejects parent_ids from other families. Also adds Minitest behavioural coverage, an rswag docs spec, a CategoryCreateRequest schema, and regenerates docs/api/openapi.yaml. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): address review feedback on POST /api/v1/categories - Re-raise ActionController::ParameterMissing in #create so the BaseController rescue_from handles it as a 400 instead of the generic 500 from the broad rescue inside the action. - Add a 403 'insufficient scope' response block to the rswag POST example so the generated OpenAPI documents read-only key rejection. - Switch the new create-action Minitest cases to API key auth via X-Api-Key + api_headers (using the existing api_keys fixtures), matching the project's API endpoint consistency rule. - Add Minitest coverage for two more 4xx paths: rejecting third-level nesting (parent_id pointing at a depth-2 subcategory) and rejecting requests without the category payload (400). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(test): migrate categories API index/show tests to X-Api-Key The pre-existing index and show tests in this file authenticated via Doorkeeper bearer tokens. Per the project's API endpoint consistency rule (CLAUDE.md, .cursor/rules/api-endpoint-consistency.mdc) Minitest controller tests under test/controllers/api/v1/ must use ApiKey + X-Api-Key auth. Drops the Doorkeeper application/access-token setup and routes every request through the existing api_keys fixtures and the api_headers helper, matching the create-action tests already in this file (and the pattern used in sync/users/family_settings tests). No behavioural change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): address second-round review on POST /api/v1/categories - Add a 400 response block to the POST rswag example so the generated OpenAPI documents the missing-category-payload contract that BaseController#handle_bad_request already returns. Regenerate docs/api/openapi.yaml. - Replace fixture-backed read_write_api_key / read_only_api_key helpers with explicit ApiKey.create! calls (matching the pattern in sync_controller_test, users_controller_test, and family_settings_controller_test). Setup now destroys active keys for the test user so the one-active-key-per-source validation does not collide with fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(api): tighten 422 create-category cases - Pass color and icon explicitly in the duplicate-name and third-level-nesting tests so each case is self-documenting about which validation it isolates (the model's color presence check is satisfied by the column default today, but reviewers — human and bot — flagged the implicit reliance). - Assert the JSON error envelope (error key + present message) on every 422 path so the response shape stays consistent and a regression in the rendered error body is caught uniformly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): tighten POST /api/v1/categories per review - Drop the no-op `rescue ActionController::ParameterMissing; raise` and the broad `rescue => e` from the create action. The BaseController already has rescue_from ActionController::ParameterMissing → 400, and unexpected exceptions are best left to Rails' default 500 handling (which logs identically). Keeps the action focused on its happy path and the two real error branches. - Stop accepting `lucide_icon` as a request key. The OpenAPI schema documents only `icon`; the dual permit was undocumented and pointless. `icon` is now the single canonical request key, mapped to `lucide_icon` on the model in category_params. - Migrate the Minitest helpers to the project's documented API key pattern: ApiKey.generate_secure_key + api_key.plain_key in the X-Api-Key header (matching the rswag spec in this PR and the rule in .cursor/rules/api-endpoint-consistency.mdc), instead of hand-built display_key strings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Botched conflict merge --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <jjmata@jjmata.com>
This commit is contained in:
@@ -96,6 +96,93 @@ RSpec.describe 'API V1 Categories', type: :request do
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
|
||||
post 'Create category' do
|
||||
tags 'Categories'
|
||||
security [ { apiKeyAuth: [] } ]
|
||||
consumes 'application/json'
|
||||
produces 'application/json'
|
||||
parameter name: :body, in: :body, required: true, schema: {
|
||||
'$ref' => '#/components/schemas/CategoryCreateRequest'
|
||||
}
|
||||
|
||||
response '201', 'category created' do
|
||||
schema '$ref' => '#/components/schemas/CategoryDetail'
|
||||
|
||||
let(:body) do
|
||||
{
|
||||
category: {
|
||||
name: 'Imported / Coffee',
|
||||
color: '#22c55e',
|
||||
icon: 'coffee'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '201', 'subcategory created with parent' do
|
||||
schema '$ref' => '#/components/schemas/CategoryDetail'
|
||||
|
||||
let(:body) do
|
||||
{
|
||||
category: {
|
||||
name: 'Imported / Espresso',
|
||||
color: '#22c55e',
|
||||
icon: 'coffee',
|
||||
parent_id: parent_category.id
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '422', 'validation error - duplicate name' do
|
||||
schema '$ref' => '#/components/schemas/ErrorResponse'
|
||||
|
||||
let(:body) { { category: { name: parent_category.name } } }
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '403', 'forbidden - api key missing read_write scope' do
|
||||
schema '$ref' => '#/components/schemas/ErrorResponse'
|
||||
|
||||
let(:read_only_api_key) do
|
||||
key = ApiKey.generate_secure_key
|
||||
ApiKey.create!(
|
||||
user: user,
|
||||
name: 'API Docs Read Key',
|
||||
key: key,
|
||||
scopes: %w[read],
|
||||
source: 'mobile'
|
||||
)
|
||||
end
|
||||
let(:'X-Api-Key') { read_only_api_key.plain_key }
|
||||
let(:body) { { category: { name: 'Anything' } } }
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '400', 'bad request - missing category payload' do
|
||||
schema '$ref' => '#/components/schemas/ErrorResponse'
|
||||
|
||||
let(:body) { {} }
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '401', 'unauthorized - missing api key' do
|
||||
schema '$ref' => '#/components/schemas/ErrorResponse'
|
||||
|
||||
let(:'X-Api-Key') { nil }
|
||||
let(:body) { { category: { name: 'Anything' } } }
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
path '/api/v1/categories/{id}' do
|
||||
|
||||
Reference in New Issue
Block a user