bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
165 lines
5.7 KiB
Go
165 lines
5.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package auth
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
larkauth "github.com/larksuite/cli/internal/auth"
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/zalando/go-keyring"
|
|
)
|
|
|
|
// `lark-cli auth check` is a predicate command: its README contract is
|
|
// `exit 0 = ok, 1 = missing`. The JSON answer goes to stdout; stderr stays
|
|
// empty so callers can write `if lark-cli auth check ...; then ... fi`
|
|
// without their logs getting polluted by an error envelope on the negative
|
|
// branch. These tests pin that contract end-to-end through the dispatcher.
|
|
|
|
func TestAuthCheckRun_NotLoggedIn_ExitOneWithStdoutOnly(t *testing.T) {
|
|
f, stdout, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
// UserOpenId left empty: triggers the not_logged_in branch.
|
|
})
|
|
|
|
err := authCheckRun(&CheckOptions{Factory: f, Scope: "calendar:calendar:read"})
|
|
|
|
if got := output.ExitCodeOf(err); got != 1 {
|
|
t.Errorf("exit code = %d, want 1 (predicate 'missing' signal)", got)
|
|
}
|
|
var bare *output.BareError
|
|
if !errors.As(err, &bare) {
|
|
t.Fatalf("expected *output.BareError (ErrBare), got %T: %v", err, err)
|
|
}
|
|
|
|
if stderr.Len() != 0 {
|
|
t.Errorf("stderr must stay empty for predicate negative answer, got:\n%s", stderr.String())
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
|
|
t.Fatalf("stdout must be valid JSON: %v\nstdout=%s", err, stdout.String())
|
|
}
|
|
if payload["ok"] != false {
|
|
t.Errorf("stdout.ok = %v, want false", payload["ok"])
|
|
}
|
|
if payload["error"] != "not_logged_in" {
|
|
t.Errorf("stdout.error = %v, want 'not_logged_in'", payload["error"])
|
|
}
|
|
}
|
|
|
|
func TestAuthCheckRun_NoStoredToken_ExitOneWithStdoutOnly(t *testing.T) {
|
|
f, stdout, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
UserOpenId: "ou_user", UserName: "tester",
|
|
})
|
|
|
|
err := authCheckRun(&CheckOptions{Factory: f, Scope: "calendar:calendar:read"})
|
|
|
|
if got := output.ExitCodeOf(err); got != 1 {
|
|
t.Errorf("exit code = %d, want 1", got)
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Errorf("stderr must stay empty, got:\n%s", stderr.String())
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
|
|
t.Fatalf("stdout must be valid JSON: %v", err)
|
|
}
|
|
if payload["ok"] != false {
|
|
t.Errorf("stdout.ok = %v, want false", payload["ok"])
|
|
}
|
|
if payload["error"] != "no_token" {
|
|
t.Errorf("stdout.error = %v, want 'no_token'", payload["error"])
|
|
}
|
|
}
|
|
|
|
func TestAuthCheckRun_ScopedTokenPresent_ExitZero(t *testing.T) {
|
|
// Predicate command happy path: stored token covers every required
|
|
// scope. Exit must be 0 (nil error, not ErrBare), stdout carries the
|
|
// `{"ok":true,...}` JSON answer, and stderr stays empty so shell
|
|
// callers can rely on `if lark-cli auth check ...; then` without log
|
|
// pollution. Pairs with the two exit-1 negatives above so both
|
|
// branches of the predicate contract are pinned.
|
|
keyring.MockInit()
|
|
t.Setenv("HOME", t.TempDir())
|
|
t.Setenv("LARKSUITE_CLI_DATA_DIR", t.TempDir())
|
|
|
|
cfg := &core.CliConfig{
|
|
AppID: "test-app",
|
|
AppSecret: "test-secret",
|
|
Brand: core.BrandFeishu,
|
|
UserOpenId: "ou_user",
|
|
UserName: "tester",
|
|
}
|
|
now := time.Now()
|
|
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
|
|
AppId: cfg.AppID,
|
|
UserOpenId: cfg.UserOpenId,
|
|
AccessToken: "user-access-token",
|
|
RefreshToken: "refresh-token",
|
|
ExpiresAt: now.Add(time.Hour).UnixMilli(),
|
|
RefreshExpiresAt: now.Add(24 * time.Hour).UnixMilli(),
|
|
GrantedAt: now.Add(-time.Hour).UnixMilli(),
|
|
Scope: "im:message docx:document",
|
|
}); err != nil {
|
|
t.Fatalf("SetStoredToken() error = %v", err)
|
|
}
|
|
|
|
f, stdout, stderr, _ := cmdutil.TestFactory(t, cfg)
|
|
|
|
err := authCheckRun(&CheckOptions{Factory: f, Scope: "im:message"})
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected nil error for happy path (exit 0), got %v", err)
|
|
}
|
|
if got := output.ExitCodeOf(err); got != 0 {
|
|
t.Errorf("exit code = %d, want 0", got)
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Errorf("stderr must stay empty for predicate exit-0 answer, got:\n%s", stderr.String())
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
|
|
t.Fatalf("stdout must be valid JSON: %v\nstdout=%s", err, stdout.String())
|
|
}
|
|
if payload["ok"] != true {
|
|
t.Errorf("stdout.ok = %v, want true", payload["ok"])
|
|
}
|
|
granted, ok := payload["granted"].([]any)
|
|
if !ok || len(granted) != 1 || granted[0] != "im:message" {
|
|
t.Errorf("stdout.granted = %v, want [im:message]", payload["granted"])
|
|
}
|
|
if payload["missing"] != nil {
|
|
t.Errorf("stdout.missing = %v, want nil/absent on happy path", payload["missing"])
|
|
}
|
|
if _, has := payload["suggestion"]; has {
|
|
t.Errorf("stdout.suggestion must be absent on happy path; got %v", payload["suggestion"])
|
|
}
|
|
}
|
|
|
|
func TestAuthCheckRun_EmptyScopeIsValidationError(t *testing.T) {
|
|
// Scope validation is a real input error, not a predicate negative
|
|
// answer — it must surface as a typed ValidationError with the normal
|
|
// stderr envelope, distinct from the silent ErrBare predicate path.
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
err := authCheckRun(&CheckOptions{Factory: f, Scope: " "})
|
|
if err == nil {
|
|
t.Fatal("expected validation error for empty --scope")
|
|
}
|
|
if got := output.ExitCodeOf(err); got != output.ExitValidation {
|
|
t.Errorf("exit code = %d, want ExitValidation (%d)", got, output.ExitValidation)
|
|
}
|
|
}
|