diff --git a/README.md b/README.md index 3361f54..d35f8d5 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ fm pages request get fm workspace auth login [--token-stdin] fm workspace auth status fm workspace auth logout +fm workspace openapi fm workspace request get ``` @@ -135,13 +136,14 @@ shared operational data. Obtain a personal access token from FeedMob SSO, then authenticate without placing the token in shell history. The token owner must have an approved SSO account. A `401 Unauthorized` response can mean that the token is invalid, expired, or revoked, or that the account is not approved. -Workspace requests are GET-only and must stay under `/api/v1/`. Consult the -SSO-protected API Reference for available resources rather than treating CLI -help as an endpoint catalog. +Workspace requests are GET-only and must stay under `/api/v1/`. Use +`fm workspace openapi` (or `fm workspace schema`) to fetch the machine-readable +OpenAPI specification for discovery and self-description. ```sh fm workspace auth login fm workspace request get /api/v1/me +fm workspace openapi ``` `publish` requires `--owner` and `--html-file`. `update` accepts an HTML diff --git a/lib/feedmob/cli/commands/workspace.rb b/lib/feedmob/cli/commands/workspace.rb new file mode 100644 index 0000000..2fde0a5 --- /dev/null +++ b/lib/feedmob/cli/commands/workspace.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require_relative 'base' + +module FeedMob + module CLI + module Commands + class WorkspaceBase < Base + private + + def service_name = 'workspace' + end + + class WorkspaceOpenapi < WorkspaceBase + desc 'Fetch the OpenAPI schema for FeedMob Workspace' + + def call(**) + credential = credential!(service) + response = runtime.client(service).request( + method: :get, + path: '/api/v1/openapi', + token: credential.value + ) + + output.success( + { + service: service.name, + status: response.status, + response: response.data + }, + message: "GET /api/v1/openapi returned HTTP #{response.status}." + ) + end + end + end + end +end diff --git a/lib/feedmob/cli/registry.rb b/lib/feedmob/cli/registry.rb index f65d131..a660698 100644 --- a/lib/feedmob/cli/registry.rb +++ b/lib/feedmob/cli/registry.rb @@ -6,6 +6,7 @@ require_relative 'commands/pages' require_relative 'commands/request' require_relative 'commands/version' +require_relative 'commands/workspace' module FeedMob module CLI @@ -73,6 +74,8 @@ module Commands auth.register 'status', WorkspaceAuthStatus auth.register 'logout', WorkspaceAuthLogout end + workspace.register 'openapi', WorkspaceOpenapi + workspace.register 'schema', WorkspaceOpenapi workspace.register 'request' do |request| request.register 'get', WorkspaceRequestGet end diff --git a/test/cli_commands_test.rb b/test/cli_commands_test.rb index 1a94629..2a2251b 100644 --- a/test/cli_commands_test.rb +++ b/test/cli_commands_test.rb @@ -219,6 +219,62 @@ def test_workspace_request_get_rejects_paths_outside_the_versioned_workspace_api assert_empty workspace_client.requests end + def test_workspace_openapi_fetches_the_versioned_openapi_schema + credentials = FakeCredentials.new( + credential: FeedMob::CLI::Credential.new(value: 'fmapat_workspace', source: 'keychain') + ) + workspace_client = FakeClient.new( + [FeedMob::CLI::HTTP::Response.new(status: 200, headers: {}, data: { 'openapi' => '3.0.0' })] + ) + use_runtime( + credentials:, + clients: { 'pixel' => FakeClient.new, 'time-off' => FakeClient.new, 'workspace' => workspace_client } + ) + + stdout, = run_cli('workspace', 'openapi', '--json') + + assert_equal( + { method: :get, path: '/api/v1/openapi', token: 'fmapat_workspace' }, + workspace_client.requests.fetch(0) + ) + assert_equal({ 'openapi' => '3.0.0' }, JSON.parse(stdout).dig('data', 'response')) + end + + def test_workspace_schema_alias_fetches_the_versioned_openapi_schema + credentials = FakeCredentials.new( + credential: FeedMob::CLI::Credential.new(value: 'fmapat_workspace', source: 'keychain') + ) + workspace_client = FakeClient.new( + [FeedMob::CLI::HTTP::Response.new(status: 200, headers: {}, data: { 'openapi' => '3.0.0' })] + ) + use_runtime( + credentials:, + clients: { 'pixel' => FakeClient.new, 'time-off' => FakeClient.new, 'workspace' => workspace_client } + ) + + stdout, = run_cli('workspace', 'schema', '--json') + + assert_equal( + { method: :get, path: '/api/v1/openapi', token: 'fmapat_workspace' }, + workspace_client.requests.fetch(0) + ) + assert_equal({ 'openapi' => '3.0.0' }, JSON.parse(stdout).dig('data', 'response')) + end + + def test_workspace_openapi_requires_workspace_credential + credentials = FakeCredentials.new(credential: FeedMob::CLI::Credential.new(value: nil, source: 'missing')) + workspace_client = FakeClient.new + use_runtime( + credentials:, + clients: { 'pixel' => FakeClient.new, 'time-off' => FakeClient.new, 'workspace' => workspace_client } + ) + + _stdout, _stderr, status = run_cli('workspace', 'openapi', '--json') + + assert_equal 1, status + assert_empty workspace_client.requests + end + def test_pixel_logout_revokes_the_remote_token_then_deletes_local_keychain_value credentials = FakeCredentials.new pixel_client = FakeClient.new(