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
953 changes: 942 additions & 11 deletions .surface

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions .surface-breaking
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ ARG basecamp message 01 [body]
ARG basecamp react 00 <content>
ARG basecamp reopen 00 <id|url>...
ARG basecamp todo 00 <content>
ARG basecamp todos move 00 <id|url>
ARG basecamp todos position 00 <id|url>
ARG basecamp todos reorder 00 <id|url>
ARG basecamp unassign 00 <id>
ARG basecamp unassign 00 <todo_id>
ARG basecamp upload archive 00 <id|url>
Expand Down Expand Up @@ -1502,3 +1505,11 @@ FLAG basecamp checkin question notify --digest-include-unanswered type=bool
FLAG basecamp checkin question notify --no-digest-include-unanswered type=bool
FLAG basecamp checkins question notify --digest-include-unanswered type=bool
FLAG basecamp checkins question notify --no-digest-include-unanswered type=bool
ARG basecamp docs move 00 <id|url>
ARG basecamp documents move 00 <id|url>
ARG basecamp file move 00 <id|url>
ARG basecamp files move 00 <id|url>
ARG basecamp folders move 00 <id|url>
ARG basecamp vault move 00 <id|url>
ARG basecamp vaults move 00 <id|url>
ARG basecamp tools move 00 <id>
20 changes: 11 additions & 9 deletions API-COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ Coverage of Basecamp 3 API endpoints. Source: [bc3-api/sections](https://github.
**216 of 216 tracked in-scope endpoints.** Moving and copying a recording
into another project (`POST /recordings/:id/moves.json`,
`POST /recordings/:id/copies.json`) and reading the background filing they
start (`GET /filings/:id.json`), modelled by
start (`GET /filings/:id.json`, also read on its own by `filings show`), modelled by
[basecamp/basecamp-sdk#1053](https://github.com/basecamp/basecamp-sdk/pull/1053),
land for cards as `cards move --to-project`/`--to-card-table` and `cards copy`. Uploading and removing a project's
land for cards as `cards move --to-project`/`--to-card-table` and `cards copy`, and for
to-dos, to-do lists and groups as `todos move|copy`, `todolists move|copy` and
`todolistgroups move|copy` with `--to-project`/`--to-list`/`--to-todoset`. Uploading and removing a project's
wallpaper (`PUT`/`DELETE /projects/:id/wallpaper.json`, added by
[bc3#14062](https://github.com/basecamp/bc3/pull/14062) and shipped in
[SDK v0.28.0](https://github.com/basecamp/basecamp-sdk/releases/tag/v0.28.0))
Expand Down Expand Up @@ -211,17 +213,17 @@ cannot faithfully cover at least one endpoint for a reason outside the CLI. A
| **Core** |
| projects | 9 | `projects` | ✅ | BC4 | - | list, show, create, update, delete |
| projects (wallpaper) | 2 | `projects wallpaper` | ✅ | BC5 | - | set (`PUT /projects/:id/wallpaper.json`, multipart `wallpaper` with an optional `mode`), remove (`DELETE` on the same route; 204 with or without a wallpaper). `mode` sends `wallpaper_mode` through the existing `PUT /projects/:id.json`, with the current name the SDK requires. PNG, JPEG, GIF, WebP, AVIF, HEIF or HEIC up to 10 MB, checked before the upload using a local MIME hint; the server applies its shared image validations. People and personal assistants follow project-edit permissions; shared agents are refused with 403. Project JSON carries `wallpaper {url, mode}` when one is attached, so `projects show` shows it. Project-scoped via `--in`/`--project` |
| todos | 12 | `todos`, `todo`, `done`, `reopen` | ✅ | BC4 | - | list, show, create, update, complete, uncomplete, position (BC5: `steps` shown on `todos show`, plus `subtasks_count`/`subtasks_completed_count`/`subtasks_url`; edit via `subtasks`). `todos create --loose` creates on the to-do set, outside any list |
| todos | 12 | `todos`, `todo`, `done`, `reopen` | ✅ | BC4 | - | list, show, create, update, complete, uncomplete, position, copy (BC5: `steps` shown on `todos show`, plus `subtasks_count`/`subtasks_completed_count`/`subtasks_url`; edit via `subtasks`). `todos create --loose` creates on the to-do set, outside any list |
| subtasks | 8 | `subtasks` | ✅ | BC5 | - | list (`GET /recordings/:id/subtasks.json`, paginated), show, create (`POST` on the same list route), update (partial; `--no-due`/`--no-assignees` clear), complete/uncomplete (`POST`/`DELETE /subtasks/:id/completion.json`), move (`PUT /subtasks/:id/position.json`, 1-based), delete. Account-scoped flat routes (bc3#12659), so no `--in`. Parents are to-dos and cards only. Same `Kanban::Step` records as `card_table_steps` |
| todolists | 9 | `todolists` | ✅ | BC4 | - | list, show, create, update, position |
| todolists | 9 | `todolists` | ✅ | BC4 | - | list, show, create, update, position, copy |
| todosets | 3 | `todosets` | ✅ | BC4 | - | Container for todolists, accessed via project dock (BC5: `todos_count`, `completed_loose_todos_count`, `todos_url`, `app_todos_url`) |
| todolist_groups | 8 | `todolistgroups` | ✅ | BC4 | - | list, show, create, update, position |
| dock_tools | 7 | `tools` | ✅ | BC4 | - | Dock tool management: show, update, trash, enable, disable, reposition. `create` is BC5-only (create-by-type: `POST /buckets/{id}/dock/tools.json`), replacing the removed clone call; create-time `visible_to_clients` behind `tools create --visible-to-clients` (chat/kanban only) |
| todolist_groups | 8 | `todolistgroups` | ✅ | BC4 | - | list, show, create, update, position, copy |
| dock_tools | 7 | `tools` | ✅ | BC4 | - | Dock tool management: show, update, trash, enable, disable, reposition (also `tools move --position`; `tools move --to-project` moves a tool to another project through recordings (moves, copies)). `create` is BC5-only (create-by-type: `POST /buckets/{id}/dock/tools.json`), replacing the removed clone call; create-time `visible_to_clients` behind `tools create --visible-to-clients` (chat/kanban only) |
| **Hill Charts** |
| hill_charts | 2 | `hillcharts` | ✅ | BC4 | - | show, track/untrack todolists |
| gauges | 7 | `gauges` | ✅ | BC4 | - | list, needles, needle, create, update, delete, enable/disable |
| **Communication** |
| messages | 10 | `messages`, `message` | ✅ | BC4 | - | list, show, create, update, publish, pin, unpin. Create supports `--subscribe`/`--no-subscribe` and `--draft`. Publish promotes drafts to active |
| messages | 10 | `messages`, `message` | ✅ | BC4 | - | list, show, create, update, publish, pin, unpin. Create supports `--subscribe`/`--no-subscribe` and `--draft`. Publish promotes drafts to active. `move`/`copy` file a message onto a message board in another project (see recordings (moves, copies)) |
| message_boards | 3 | `messageboards` | ✅ | BC4 | - | Container, accessed via project dock |
| message_types | 5 | `messagetypes` | ✅ | BC4 | - | list, show, create, update, delete. Bucket-scoped (`/buckets/{id}/categories…`); commands are project-scoped via `--in`/`--project` |
| campfires | 14 | `chat` | ✅ | BC4 | - | list, messages, post, line show/update/delete. @mentions in content |
Expand All @@ -237,7 +239,7 @@ cannot faithfully cover at least one endpoint for a reason outside the CLI. A
| card_table_columns | 11 | `cards columns` | ✅ | BC4 | - | list columns. SDK v0.12.0 added `Subscribe`/`Unsubscribe`; `cards column watch\|unwatch` already performs the same action through the generic recording-subscription endpoint and returns the resulting subscription details the specific endpoint does not, so the CLI keeps one spelling |
| card_table_steps | 4 | `cards steps` | ✅ | BC4 | - | Workflow steps on cards. The card-scoped aliases of `subtasks`, which bc3 keeps serving; `cards step move --position` is 1-based (the SDK refuses 0 since basecamp/basecamp-sdk#883) |
| card_table_wormholes | 3 | `cards wormholes` | ✅ | BC5 | - | list (via `wormholes[]` on card table), create, update, delete; `cards move --to-wormhole` teleports a card across projects and reports the new card once it lands |
| recordings (moves, copies) | 3 | `cards move --to-project`, `cards copy` | ✅ | BC5 | - | `POST /recordings/:id/moves.json` and `POST /recordings/:id/copies.json` file a card into a card table in this or another project, landing in Triage (Done if completed); `GET /filings/:id.json` reports the background filing, which the commands wait on before showing the new card. `--to-project` picks the project's only card table on its dock, `--to-card-table` one by ID, URL or name. `cards copy --include-comments` sends `include_comments`; a copy is never retried. Moving or copying other recording types is not exposed yet |
| recordings (moves, copies) | 3 | `cards move --to-project`, `cards copy`, `todos move --to-project`/`--to-list`, `todos copy`, `todolists move --to-project`, `todolists copy`, `todolistgroups move --to-list`, `todolistgroups copy`, `messages move`, `messages copy`, `files move --to-project`, `files copy`, `cards column move --to-project`, `cards column copy`, `schedule move`, `schedule copy`, `checkins question move`, `checkins question copy`, `forwards move`, `forwards copy`, `tools move --to-project`, `filings show` | ✅ | BC5 | - | `POST /recordings/:id/moves.json` and `POST /recordings/:id/copies.json` file a card into a card table in this or another project, landing in Triage (Done if completed); `GET /filings/:id.json` reports the background filing, which the commands wait on before showing the new card, and which `filings show <id>` reads when a command's wait ends first (status, type, source, and once completed the recording it made). `--to-project` picks the project's only card table on its dock, `--to-card-table` one by ID, URL or name. `cards copy --include-comments` sends `include_comments`; a copy is never retried. To-dos file into a to-do list in any project (several IDs file one by one, and any failure fails the command naming what moved); a to-do list files into a project's To-dos (`--to-todoset` when it has several); a group files into a to-do list and stays a group. `--to-list`/`--to-todoset` take an ID, URL or a name looked up in `--to-project`; Basecamp refuses a group or an archived list as the destination. `messages move`/`copy` file a message onto a message board (`--to-project`, `--to-board`); a copy with no destination goes onto the message's own board. `files move --to-project`/`--to-folder` and `files copy` (also on `uploads`, `docs`, `vaults` and their aliases) file documents, uploads, cloud files, Google documents and folders into a project's Docs & Files, a folder inside it as `parent_id`; several IDs are filed one by one, as the web's bulk Move/Copy does, and `files show` reads the cloud files and Google documents they make. `cards column move --to-project`/`--to-card-table` and `cards column copy` file a column with its cards onto a card table; Triage, Not now, Done and On hold are refused before the request. `schedule move`/`copy` file an event onto a schedule (`--to-project`, `--to-schedule`); a recurring event, whose read answers with an occurrence, files as its whole series. `checkins question move`/`copy` file a question, with its answers on a move, into a project's Automatic Check-ins (`--to-project`, `--to-questionnaire`). `forwards move`/`copy` file an email forward into a project's Email Forwards (`--to-project`, `--to-inbox`), which must be turned on there; a turned-off one is named with the `tools enable` command. `tools move --to-project` moves a whole dock tool to another project with `destination_project_id`; tools are not copied, as on the web, and doors are refused |
| **Personal (My)** |
| my_bookmarks | 4 | `bookmarks` | ✅ | BC5 | - | list, check, add, remove. Private to the authenticated user; `add`/`remove` are idempotent, and `check` returns a bool reported in the payload rather than through the exit code. Bounded like the account-wide listings |
| drafts | 1 | `drafts` | ✅ | BC5 | - | list unpublished drafts across projects (server caps at 250). Bounded like the account-wide listings; publishing happens through the command for the draft's type |
Expand All @@ -251,7 +253,7 @@ cannot faithfully cover at least one endpoint for a reason outside the CLI. A
| **Files & Documents** |
| uploads | 8 | `files`, `uploads` | ✅ | BC4 | - | list, show, create, update, download, versions (`files versions <id>`), replace (`files replace <id> <file>`); trash/archive/restore go through `recordings`. Create supports `--visible-to-clients` (root vault only) |
| vaults | 8 | `files`, `vaults` | ✅ | BC4 | - | list, show, create |
| vaults (move) | 1 | `files move` | ✅ | BC5 | - | `POST /recordings/:id/filing.json`: moves a document, upload or folder into another folder in the same project, in place (same id and comments). `--to` takes a folder ID or URL; `--position` is 1-indexed. Flat route, so no `--in` |
| vaults (move) | 1 | `files move` | ✅ | BC5 | - | `POST /recordings/:id/filing.json`: moves a document, upload or folder into another folder in the same project, in place (same id and comments). `--to` takes a folder ID or URL; `--position` is 1-indexed. Flat route, so no `--in`. Moves and copies into another project go through recordings (moves, copies) with `--to-project`/`--to-folder` |
| documents | 8 | `files`, `docs` | ✅ | BC4 | - | list, show, create, update. Create supports `--subscribe`/`--no-subscribe`, `--visible-to-clients` (root vault only) |
| attachments | 1 | `uploads`, `attachments` | ✅ | BC4 | - | Upload via `attach`; list embedded attachments via `attachments list` (parses `<bc-attachment>` from content) |
| embeds | 2 | `embeds` | ✅ | BC5 | - | create, show. Rich-text commands also embed a URL that stands alone on its line in Markdown content, through the same create |
Expand Down
9 changes: 0 additions & 9 deletions e2e/cards_columns_steps.bats
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,6 @@ load test_helper
assert_output_contains "ID required"
}

@test "cards column show without project shows error" {
create_credentials
create_global_config '{"account_id": 99999}'

run basecamp cards column show 456
assert_failure
assert_output_contains "project"
}


# Column create errors

Expand Down
2 changes: 1 addition & 1 deletion e2e/errors.bats
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ load test_helper

run basecamp todos position --to 1
assert_failure
assert_json_value '.error' '<id|url> required'
assert_json_value '.error' '<id|url>... required'
assert_json_value '.code' 'usage'
}

Expand Down
4 changes: 0 additions & 4 deletions e2e/smoke/smoke_aliases.bats
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,6 @@ load smoke_helper
mark_out_of_scope "Alias for tools trash — tested via canonical form"
}

@test "tools move is out of scope" {
mark_out_of_scope "Alias for tools reposition — tested via canonical form"
}

@test "tools rename is out of scope" {
mark_out_of_scope "Alias for tools update — tested via canonical form"
}
Expand Down
24 changes: 24 additions & 0 deletions e2e/smoke/smoke_cards_column_write.bats
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,30 @@ setup_file() {
assert_json_value '.ok' 'true'
}

@test "cards column copy copies a column onto its own card table" {
local id_file="$BATS_FILE_TMPDIR/column_id"
[[ -f "$id_file" ]] || mark_unverifiable "No column created in prior test"
local col_id
col_id=$(<"$id_file")

run_smoke basecamp cards column copy "$col_id" --json
assert_success
assert_json_value '.ok' 'true'
assert_json_value '.data.source_id' "$col_id"
assert_json_not_null '.data.id'
}

@test "cards column move refuses the card table the column is on" {
local id_file="$BATS_FILE_TMPDIR/column_id"
[[ -f "$id_file" ]] || mark_unverifiable "No column created in prior test"
local col_id
col_id=$(<"$id_file")

run_smoke basecamp cards column move "$col_id" --to-card-table "$QA_CARDTABLE" --json
assert_failure
assert_json_value '.ok' 'false'
}

@test "cards column watch watches a column" {
local id_file="$BATS_FILE_TMPDIR/column_id"
[[ -f "$id_file" ]] || mark_unverifiable "No column created in prior test"
Expand Down
26 changes: 26 additions & 0 deletions e2e/smoke/smoke_checkins_write.bats
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,29 @@ setup_file() {
assert_success
assert_json_value '.ok' 'true'
}

@test "checkins question copy copies a question into its own check-ins" {
local id_file="$BATS_FILE_TMPDIR/question_id"
[[ -f "$id_file" ]] || mark_unverifiable "No question created in prior test"
local qid
qid=$(<"$id_file")

run_smoke basecamp checkins question copy "$qid" --json
assert_success
assert_json_value '.ok' 'true'
assert_json_value '.data.source_id' "$qid"
assert_json_not_null '.data.id'
}

@test "checkins question move refuses the check-ins the question is in" {
local id_file="$BATS_FILE_TMPDIR/question_id"
[[ -f "$id_file" ]] || mark_unverifiable "No question created in prior test"
local qid
qid=$(<"$id_file")

# A cross-project move would take the QA question away; the move itself
# is covered against local bc3.
run_smoke basecamp checkins question move "$qid" --to-project "$QA_PROJECT" --json
assert_failure
assert_json_value '.ok' 'false'
}
30 changes: 30 additions & 0 deletions e2e/smoke/smoke_communication_write.bats
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,33 @@ setup_file() {
assert_success
assert_json_value '.ok' 'true'
}

# --- Email forwards ---

@test "forwards copy copies a forward into its own inbox" {
ensure_inbox || return 0
run basecamp forwards list --inbox "$QA_INBOX" -p "$QA_PROJECT" --jq '.data[0].id // empty'
local fwd_id="$output"
[[ -n "$fwd_id" ]] || mark_unverifiable "No forwards in project inbox"

run_smoke basecamp forwards copy "$fwd_id" --json
assert_success
assert_json_value '.ok' 'true'
assert_json_value '.data.source_id' "$fwd_id"

run_smoke basecamp recordings trash "$(echo "$output" | jq -r '.data.id')" -p "$QA_PROJECT" --json
assert_success
}

@test "forwards move refuses the inbox the forward is in" {
ensure_inbox || return 0
run basecamp forwards list --inbox "$QA_INBOX" -p "$QA_PROJECT" --jq '.data[0].id // empty'
local fwd_id="$output"
[[ -n "$fwd_id" ]] || mark_unverifiable "No forwards in project inbox"

# A cross-project move would take the QA forward away; the move itself is
# covered against local bc3.
run_smoke basecamp forwards move "$fwd_id" --to-inbox "$QA_INBOX" --json
assert_failure
assert_json_value '.ok' 'false'
}
50 changes: 50 additions & 0 deletions e2e/smoke/smoke_files_write.bats
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,56 @@ setup_file() {
assert_json_value '.data.folder_id' "$folder_id"
}

@test "files copy copies a file into its own folder" {
local tmpfile="$BATS_FILE_TMPDIR/smoke_files_copy.txt"
echo "smoke files copy content $(date +%s)" > "$tmpfile"

run_smoke basecamp files uploads create "$tmpfile" -p "$QA_PROJECT" --json
assert_success
local file_id
file_id=$(echo "$output" | jq -r '.data.id')

run_smoke basecamp files copy "$file_id" --json
assert_success
assert_json_value '.ok' 'true'
assert_json_value '.data.source_id' "$file_id"
assert_json_not_null '.data.id'
}

@test "uploads copy copies an upload into its own folder" {
local tmpfile="$BATS_FILE_TMPDIR/smoke_uploads_copy.txt"
echo "smoke uploads copy content $(date +%s)" > "$tmpfile"

run_smoke basecamp uploads create "$tmpfile" -p "$QA_PROJECT" --json
assert_success
local file_id
file_id=$(echo "$output" | jq -r '.data.id')

run_smoke basecamp uploads copy "$file_id" --json
assert_success
assert_json_value '.ok' 'true'
assert_json_value '.data.source_id' "$file_id"
}

@test "uploads move moves an upload into a folder" {
local tmpfile="$BATS_FILE_TMPDIR/smoke_uploads_move.txt"
echo "smoke uploads move content $(date +%s)" > "$tmpfile"

run_smoke basecamp files folders create "Smoke uploads move target $(date +%s)" -p "$QA_PROJECT" --json
assert_success
local folder_id
folder_id=$(echo "$output" | jq -r '.data.id')

run_smoke basecamp uploads create "$tmpfile" -p "$QA_PROJECT" --json
assert_success
local file_id
file_id=$(echo "$output" | jq -r '.data.id')

run_smoke basecamp uploads move "$file_id" --to "$folder_id" --json
assert_success
assert_json_value '.data.id' "$file_id"
}

@test "files archive archives a file" {
local id_file="$BATS_FILE_TMPDIR/upload_id"
[[ -f "$id_file" ]] || mark_unverifiable "No upload created in prior test"
Expand Down
15 changes: 15 additions & 0 deletions e2e/smoke/smoke_filings.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bats
# smoke_filings.bats - Level 0: Reading a move's or copy's filing

load smoke_helper

setup_file() {
ensure_token || return 1
}

@test "filings show reports a filing it cannot read as not found" {
run_smoke basecamp filings show 999999999999 --json
assert_failure
assert_json_value '.code' 'not_found'
assert_output_contains "Only the person who started a move or copy"
}
Loading
Loading