Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ fm pages request get <path>
fm workspace auth login [--token-stdin]
fm workspace auth status
fm workspace auth logout
fm workspace openapi
fm workspace request get <api-v1-path>
```

Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions lib/feedmob/cli/commands/workspace.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions lib/feedmob/cli/registry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require_relative 'commands/pages'
require_relative 'commands/request'
require_relative 'commands/version'
require_relative 'commands/workspace'

module FeedMob
module CLI
Expand Down Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions test/cli_commands_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading