Files
larksuite-cli/shortcuts/note/note_test.go
max 7c64e63b9d feat(note): clarify note ownership with dedicated detail and transcript flows (#1435)
* feat: split note domain

* fix: address note transcript review comments

* fix: stabilize empty note detail detection
2026-06-12 16:30:41 +08:00

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)
}