mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
* feat: split note domain * fix: address note transcript review comments * fix: stabilize empty note detail detection
281 lines
8.5 KiB
Go
281 lines
8.5 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package note
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/httpmock"
|
|
)
|
|
|
|
// These tests were relocated from shortcuts/vc/vc_notes_test.go together with
|
|
// the note-detail parsing helpers they cover.
|
|
|
|
func TestParseLooseInt(t *testing.T) {
|
|
tests := []struct {
|
|
input any
|
|
want int
|
|
}{
|
|
{float64(1), 1},
|
|
{float64(2), 2},
|
|
{float64(1.9), 0},
|
|
{json.Number("3"), 3},
|
|
{"unknown", 0},
|
|
{nil, 0},
|
|
}
|
|
for _, tt := range tests {
|
|
got := parseLooseInt(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("parseLooseInt(%v) = %d, want %d", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseLooseCursorID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in any
|
|
want string
|
|
ok bool
|
|
}{
|
|
{name: "string", in: "7648924766078847940", want: "7648924766078847940", ok: true},
|
|
{name: "trim string", in: " 123 ", want: "123", ok: true},
|
|
{name: "empty string", in: "", ok: false},
|
|
{name: "zero string", in: "0", ok: false},
|
|
{name: "json number", in: json.Number("123"), want: "123", ok: true},
|
|
{name: "float safe integer", in: float64(123), want: "123", ok: true},
|
|
{name: "float unsafe integer", in: float64(1<<53 + 1), ok: false},
|
|
{name: "float fractional", in: float64(1.5), ok: false},
|
|
{name: "negative", in: -1, ok: false},
|
|
{name: "nil", in: nil, ok: false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, ok := parseLooseCursorID(tt.in)
|
|
if got != tt.want || ok != tt.ok {
|
|
t.Fatalf("parseLooseCursorID(%v) = (%q, %v), want (%q, %v)", tt.in, got, ok, tt.want, tt.ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractArtifactTokens(t *testing.T) {
|
|
artifacts := []any{
|
|
map[string]any{"doc_token": "main_doc", "artifact_type": float64(1)},
|
|
map[string]any{"doc_token": "verbatim_doc", "artifact_type": float64(2)},
|
|
map[string]any{"doc_token": "unknown_doc", "artifact_type": float64(99)},
|
|
nil,
|
|
}
|
|
noteDoc, verbatimDoc := extractArtifactTokens(artifacts)
|
|
if noteDoc != "main_doc" {
|
|
t.Errorf("noteDoc = %q, want %q", noteDoc, "main_doc")
|
|
}
|
|
if verbatimDoc != "verbatim_doc" {
|
|
t.Errorf("verbatimDoc = %q, want %q", verbatimDoc, "verbatim_doc")
|
|
}
|
|
}
|
|
|
|
func TestExtractArtifactTokens_Empty(t *testing.T) {
|
|
noteDoc, verbatimDoc := extractArtifactTokens(nil)
|
|
if noteDoc != "" || verbatimDoc != "" {
|
|
t.Errorf("expected empty tokens for nil input, got %q, %q", noteDoc, verbatimDoc)
|
|
}
|
|
}
|
|
|
|
func TestExtractDocTokens(t *testing.T) {
|
|
refs := []any{
|
|
map[string]any{"doc_token": "shared1"},
|
|
map[string]any{"doc_token": "shared2"},
|
|
map[string]any{"doc_token": ""},
|
|
map[string]any{},
|
|
nil,
|
|
}
|
|
tokens := extractDocTokens(refs)
|
|
if len(tokens) != 2 || tokens[0] != "shared1" || tokens[1] != "shared2" {
|
|
t.Errorf("extractDocTokens = %v, want [shared1 shared2]", tokens)
|
|
}
|
|
}
|
|
|
|
func TestExtractDocTokens_Empty(t *testing.T) {
|
|
tokens := extractDocTokens(nil)
|
|
if tokens != nil {
|
|
t.Errorf("expected nil for nil input, got %v", tokens)
|
|
}
|
|
}
|
|
|
|
func TestDetailToMap(t *testing.T) {
|
|
detail := &Detail{
|
|
NoteID: "note_1",
|
|
CreatorID: "creator_1",
|
|
CreateTime: "2026-06-09 12:00:00",
|
|
DisplayType: "unified",
|
|
NoteDocToken: "note_doc",
|
|
VerbatimDocToken: "verbatim_doc",
|
|
SharedDocTokens: []string{"shared_1", "shared_2"},
|
|
}
|
|
|
|
got := detail.ToMap()
|
|
want := map[string]any{
|
|
"note_id": "note_1",
|
|
"creator_id": "creator_1",
|
|
"create_time": "2026-06-09 12:00:00",
|
|
"note_display_type": "unified",
|
|
"note_doc_token": "note_doc",
|
|
"verbatim_doc_token": "verbatim_doc",
|
|
"shared_doc_tokens": []string{"shared_1", "shared_2"},
|
|
}
|
|
for key, wantValue := range want {
|
|
gotValue, ok := got[key]
|
|
if !ok {
|
|
t.Fatalf("ToMap missing key %q in %#v", key, got)
|
|
}
|
|
if !valuesEqual(gotValue, wantValue) {
|
|
t.Fatalf("ToMap[%q] = %#v, want %#v", key, gotValue, wantValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDetailToMap_OmitsEmptySharedDocTokens(t *testing.T) {
|
|
got := (&Detail{NoteID: "note_1"}).ToMap()
|
|
if _, ok := got["shared_doc_tokens"]; ok {
|
|
t.Fatalf("ToMap should omit empty shared_doc_tokens, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestMapNoteError_NoReadPermission(t *testing.T) {
|
|
err := &errs.PermissionError{
|
|
Problem: errs.Problem{
|
|
Category: errs.CategoryAuthorization,
|
|
Subtype: errs.SubtypePermissionDenied,
|
|
Code: NoNoteReadPermissionCode,
|
|
Message: "upstream permission denied",
|
|
LogID: "log_1",
|
|
},
|
|
MissingScopes: []string{"vc:note:read"},
|
|
Identity: "user",
|
|
}
|
|
|
|
got := mapNoteError(err)
|
|
problem, ok := errs.ProblemOf(got)
|
|
if !ok {
|
|
t.Fatalf("mapNoteError returned %T, want typed problem", got)
|
|
}
|
|
if problem.Code != NoNoteReadPermissionCode {
|
|
t.Fatalf("mapped code = %d, want %d", problem.Code, NoNoteReadPermissionCode)
|
|
}
|
|
if !strings.Contains(problem.Message, "no read permission for this note") || !strings.Contains(problem.Message, "upstream permission denied") {
|
|
t.Fatalf("mapped message = %q, want note permission guidance with upstream message", problem.Message)
|
|
}
|
|
if !errors.Is(got, err) {
|
|
t.Fatal("mapped error should preserve the original typed error as cause")
|
|
}
|
|
originalProblem, _ := errs.ProblemOf(err)
|
|
if originalProblem.Message != "upstream permission denied" {
|
|
t.Fatalf("original message was mutated to %q", originalProblem.Message)
|
|
}
|
|
var gotPerm *errs.PermissionError
|
|
if !errors.As(got, &gotPerm) {
|
|
t.Fatalf("mapped error = %T, want PermissionError", got)
|
|
}
|
|
if gotPerm.LogID != "log_1" {
|
|
t.Fatalf("LogID = %q, want preserved log_1", gotPerm.LogID)
|
|
}
|
|
if len(gotPerm.MissingScopes) != 1 || gotPerm.MissingScopes[0] != "vc:note:read" {
|
|
t.Fatalf("MissingScopes = %#v, want preserved vc:note:read", gotPerm.MissingScopes)
|
|
}
|
|
if gotPerm.Identity != "user" {
|
|
t.Fatalf("Identity = %q, want preserved user", gotPerm.Identity)
|
|
}
|
|
}
|
|
|
|
func TestMapNoteError_NormalizesNonPermissionTypedError(t *testing.T) {
|
|
err := &errs.APIError{
|
|
Problem: errs.Problem{
|
|
Category: errs.CategoryAPI,
|
|
Subtype: errs.SubtypeUnknown,
|
|
Code: NoNoteReadPermissionCode,
|
|
Message: "upstream api error",
|
|
LogID: "log_2",
|
|
},
|
|
}
|
|
|
|
got := mapNoteError(err)
|
|
var gotPerm *errs.PermissionError
|
|
if !errors.As(got, &gotPerm) {
|
|
t.Fatalf("mapped error = %T, want PermissionError", got)
|
|
}
|
|
if gotPerm.Category != errs.CategoryAuthorization || gotPerm.Subtype != errs.SubtypePermissionDenied {
|
|
t.Fatalf("mapped category/subtype = %q/%q, want authorization/permission_denied", gotPerm.Category, gotPerm.Subtype)
|
|
}
|
|
if !strings.Contains(gotPerm.Message, "no read permission for this note") || !strings.Contains(gotPerm.Message, "upstream api error") {
|
|
t.Fatalf("mapped message = %q, want note permission guidance with upstream message", gotPerm.Message)
|
|
}
|
|
if gotPerm.Hint == "" {
|
|
t.Fatal("mapped hint should not be empty")
|
|
}
|
|
if gotPerm.LogID != "log_2" {
|
|
t.Fatalf("LogID = %q, want preserved log_2", gotPerm.LogID)
|
|
}
|
|
if !errors.Is(got, err) {
|
|
t.Fatal("mapped error should preserve the original typed error as cause")
|
|
}
|
|
}
|
|
|
|
func TestMapNoteError_Passthrough(t *testing.T) {
|
|
err := errors.New("boom")
|
|
if got := mapNoteError(err); got != err {
|
|
t.Fatalf("mapNoteError passthrough = %v, want original", got)
|
|
}
|
|
}
|
|
|
|
func TestNoteDetailEmptyDetailPreservesSentinelCause(t *testing.T) {
|
|
factory, stdout, _, reg := noteShortcutTestFactory(t)
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "GET",
|
|
URL: "/open-apis/vc/v1/notes/note_empty_detail",
|
|
Body: map[string]any{
|
|
"code": 0,
|
|
"data": map[string]any{},
|
|
},
|
|
})
|
|
|
|
err := runNoteShortcut(t, NoteDetail, []string{"+detail", "--note-id", "note_empty_detail", "--as", "user"}, factory, stdout)
|
|
if err == nil {
|
|
t.Fatal("expected empty detail to fail")
|
|
}
|
|
if !errors.Is(err, ErrEmptyDetail) {
|
|
t.Fatalf("errors.Is(ErrEmptyDetail) = false for %T: %v", err, err)
|
|
}
|
|
problem, ok := errs.ProblemOf(err)
|
|
if !ok {
|
|
t.Fatalf("expected typed error, got %T", err)
|
|
}
|
|
if problem.Category != errs.CategoryInternal || problem.Subtype != errs.SubtypeInvalidResponse {
|
|
t.Fatalf("category/subtype = %v/%v, want Internal/InvalidResponse", problem.Category, problem.Subtype)
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("stdout = %q, want empty", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestShortcuts(t *testing.T) {
|
|
shortcuts := Shortcuts()
|
|
if len(shortcuts) != 2 {
|
|
t.Fatalf("Shortcuts len = %d, want 2", len(shortcuts))
|
|
}
|
|
if shortcuts[0].Command != "+detail" || shortcuts[1].Command != "+transcript" {
|
|
t.Fatalf("Shortcuts commands = %q, %q", shortcuts[0].Command, shortcuts[1].Command)
|
|
}
|
|
}
|
|
|
|
func valuesEqual(a, b any) bool {
|
|
ab, _ := json.Marshal(a)
|
|
bb, _ := json.Marshal(b)
|
|
return string(ab) == string(bb)
|
|
}
|