mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 22:24:31 +08:00
Compare commits
2 Commits
v1.0.58
...
feat/apps-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4c313c8f1 | ||
|
|
a2c820643d |
125
tests/cli_e2e/apps/apps_list_workflow_test.go
Normal file
125
tests/cli_e2e/apps/apps_list_workflow_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package apps
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// TestAppsListWorkflowAsUser exercises `apps +list` against the live service.
|
||||
// +list is the only apps shortcut that is read-only AND requires no pre-existing
|
||||
// app_id fixture, so it is the sole command in the domain that can be live-tested
|
||||
// without leaking tenant state (apps has no +delete endpoint). All assertions are
|
||||
// tenant-data-independent: the envelope/array shape is checked unconditionally,
|
||||
// field-level contracts only when items are present. An empty app list is valid.
|
||||
func TestAppsListWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
// assertListEnvelope checks the shared contract for every +list invocation:
|
||||
// exit 0, ok:true envelope, and data.items is a JSON array. It returns the
|
||||
// items result for scenario-specific follow-up assertions. Failure messages
|
||||
// reference field paths only (not the full data envelope) so real tenant app
|
||||
// names are not written into test logs.
|
||||
assertListEnvelope := func(t *testing.T, result *clie2e.Result) gjson.Result {
|
||||
t.Helper()
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
items := gjson.Get(result.Stdout, "data.items")
|
||||
require.True(t, items.IsArray(), "data.items should be a JSON array")
|
||||
return items
|
||||
}
|
||||
|
||||
t.Run("default list", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"apps", "+list"},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
items := assertListEnvelope(t, result)
|
||||
|
||||
// Field-level contract only when the tenant has at least one app: every
|
||||
// item carries app_id + name, and the shortcut strips icon_url/created_at
|
||||
// (apps_list.go projects them away before output). The loop is a no-op on
|
||||
// an empty tenant, keeping the test non-flaky.
|
||||
for _, item := range items.Array() {
|
||||
assert.NotEmpty(t, item.Get("app_id").String(), "each item should have app_id")
|
||||
assert.NotEmpty(t, item.Get("name").String(), "each item should have name")
|
||||
assert.False(t, item.Get("icon_url").Exists(), "icon_url should be stripped from list output")
|
||||
assert.False(t, item.Get("created_at").Exists(), "created_at should be stripped from list output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("page size honored", func(t *testing.T) {
|
||||
// Baseline uncapped list first: the page-size=1 cap is only a meaningful
|
||||
// assertion when the tenant actually has >= 2 apps. On a near-empty tenant
|
||||
// the cap is vacuously satisfied, so we skip the comparison rather than
|
||||
// claim coverage we don't have.
|
||||
baseline, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"apps", "+list"},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
baselineItems := assertListEnvelope(t, baseline)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"apps", "+list", "--page-size", "1"},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
items := assertListEnvelope(t, result)
|
||||
|
||||
if len(baselineItems.Array()) >= 2 {
|
||||
assert.LessOrEqual(t, len(items.Array()), 1, "page-size 1 should cap items at 1 when the tenant has multiple apps")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keyword no match", func(t *testing.T) {
|
||||
// A high-entropy keyword that cannot match any real app name; proves the
|
||||
// keyword filter is accepted and returns a well-formed (typically empty)
|
||||
// list rather than erroring.
|
||||
keyword := "lark-cli-e2e-nomatch-" + clie2e.GenerateSuffix()
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"apps", "+list", "--keyword", keyword},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assertListEnvelope(t, result)
|
||||
})
|
||||
|
||||
t.Run("ownership filter mine", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"apps", "+list", "--ownership", "mine"},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assertListEnvelope(t, result)
|
||||
})
|
||||
|
||||
t.Run("app-type filter html", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"apps", "+list", "--app-type", "html"},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
items := assertListEnvelope(t, result)
|
||||
// When the server echoes app_type on items, the html filter must return
|
||||
// exactly html apps. Conditional so the test does not assume the field is
|
||||
// always present in the list response.
|
||||
for _, item := range items.Array() {
|
||||
if at := item.Get("app_type"); at.Exists() {
|
||||
assert.Equal(t, "html", at.String(), "html filter should only return html apps when app_type is present")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
- Command coverage: 100% (9/9)
|
||||
- API dry-run coverage: 100% (7/7 API-backed commands)
|
||||
- Local E2E coverage: 100% (2/2 local-only commands)
|
||||
- Live coverage: 0%
|
||||
- Live coverage: 1/1 live-capable command (`apps +list`); all other commands blocked — see "Blocked" below
|
||||
|
||||
## Summary
|
||||
- `TestAppsCreateDryRun`: happy path with `--app-type html`, all-fields shape, rejection paths (missing name, missing app-type, invalid app-type, legacy uppercase `HTML`). `--app-type` is a strict lowercase enum (`html`/`full_stack`); the CLI does not normalize case — legacy uppercase compatibility is a server concern.
|
||||
@@ -17,8 +17,9 @@
|
||||
- `TestAppsGitCredentialInitDryRun`: URL shape for issuing a Miaoda Git PAT; no body; `app_id` query metadata included.
|
||||
- `TestAppsGitCredentialListLocalE2E`: local-only command scans every app storage directory and reports repository URL and status without exposing PAT or expiry details.
|
||||
- `TestAppsGitCredentialRemoveLocalE2E`: local cleanup command removes app-scoped metadata under an isolated config dir.
|
||||
- `TestAppsListWorkflowAsUser`: live `apps +list` against the service. Five `t.Run` proof points — default list (envelope `ok:true`, `data.items` array, and when non-empty each item has `app_id`/`name` with `icon_url`/`created_at` stripped), `--page-size 1` caps items when the tenant has multiple apps, high-entropy `--keyword` returns a well-formed empty list, `--ownership mine` accepted, `--app-type html` never returns `full_stack`. All assertions are tenant-data-independent (empty list is valid); skips via `SkipWithoutUserToken` when no user credentials are present.
|
||||
|
||||
Blocked: Live E2E intentionally not implemented yet. Apps has no `+delete` endpoint (OAPI doc explicitly defers archive/delete), so a create-and-cleanup workflow would leak tenant state. Revisit when the server exposes `DELETE /apps/{appId}`.
|
||||
Blocked (live): every command except `apps +list` remains without live coverage. Apps has no `+delete` endpoint (OAPI doc explicitly defers archive/delete), so any create-and-cleanup workflow would leak tenant state. Every read command other than `+list` requires a `--app-id` (or a session/release id), and obtaining a real one means creating an app that cannot be cleaned up. A shared per-run fixture app was considered and rejected: with no delete path, each CI run would leave an orphaned app — a real and accumulating drain on Miaoda tenant resources. Only `apps +list` is read-only AND fixture-independent, so it is the sole live-covered command. Revisit the rest when the server exposes `DELETE /apps/{appId}`.
|
||||
|
||||
## Command Table
|
||||
|
||||
@@ -26,7 +27,7 @@ Blocked: Live E2E intentionally not implemented yet. Apps has no `+delete` endpo
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✓ | apps +create | shortcut | apps_create_dryrun_test.go::TestAppsCreateDryRun | `--name`, `--app-type` (required, case-sensitive, `html`/`full_stack`), `--description`, `--icon-url` | live blocked: no +delete to clean up |
|
||||
| ✓ | apps +update | shortcut | apps_update_dryrun_test.go::TestAppsUpdateDryRun | `--app-id`; at least one of `--name`/`--description` | live blocked: no +delete |
|
||||
| ✓ | apps +list | shortcut | apps_list_dryrun_test.go::TestAppsListDryRun | `--keyword`; `--ownership` (enum all/mine/shared); `--app-type` (enum html/full_stack); `--page-size` default 20; `--page-token` cursor | live blocked: needs tenant fixtures |
|
||||
| ✓ | apps +list | shortcut | apps_list_dryrun_test.go::TestAppsListDryRun; apps_list_workflow_test.go::TestAppsListWorkflowAsUser | `--keyword`; `--ownership` (enum all/mine/shared); `--app-type` (enum html/full_stack); `--page-size` default 20; `--page-token` cursor | live covered: read-only, no app_id fixture needed; tenant-data-independent assertions |
|
||||
| ✓ | apps +access-scope-set | shortcut | apps_access_scope_set_dryrun_test.go::TestAppsAccessScopeSetDryRun | `--scope specific/public/tenant`; `--targets` JSON; `--apply-enabled --approver`; `--require-login` | live blocked: needs real open_ids |
|
||||
| ✓ | apps +access-scope-get | shortcut | apps_access_scope_get_dryrun_test.go::TestAppsAccessScopeGetDryRun | `--app-id` | live blocked: depends on +access-scope-set state |
|
||||
| ✓ | apps +html-publish | shortcut | apps_html_publish_dryrun_test.go::TestAppsHTMLPublishDryRun | `--app-id`, `--path` (file or directory containing `index.html`) | live blocked: real upload has side effects; no rollback API |
|
||||
|
||||
Reference in New Issue
Block a user