77 "bytes"
88 "context"
99 "encoding/json"
10+ "errors"
1011 "io"
1112 "os"
1213 "path/filepath"
@@ -15,7 +16,12 @@ import (
1516 "testing"
1617 "time"
1718
19+ "github.com/larksuite/cli/errs"
20+ "github.com/larksuite/cli/internal/cmdutil"
21+ "github.com/larksuite/cli/internal/core"
22+ "github.com/larksuite/cli/shortcuts/common"
1823 larkevent "github.com/larksuite/oapi-sdk-go/v3/event"
24+ "github.com/spf13/cobra"
1925)
2026
2127// chdirTemp changes cwd to a fresh temp dir for the test duration.
@@ -44,6 +50,87 @@ func makeRawEvent(eventType string, eventJSON string) *RawEvent {
4450 }
4551}
4652
53+ func requireProblem (t * testing.T , err error , category errs.Category , subtype errs.Subtype , param string ) {
54+ t .Helper ()
55+ p , ok := errs .ProblemOf (err )
56+ if ! ok {
57+ t .Fatalf ("ProblemOf(%T) = false, error: %v" , err , err )
58+ }
59+ if p .Category != category || p .Subtype != subtype {
60+ t .Fatalf ("problem = %s/%s, want %s/%s" , p .Category , p .Subtype , category , subtype )
61+ }
62+ if param != "" {
63+ var ve * errs.ValidationError
64+ if ! errors .As (err , & ve ) {
65+ t .Fatalf ("error %T is not *errs.ValidationError" , err )
66+ }
67+ if ve .Param != param {
68+ t .Fatalf ("Param = %q, want %q" , ve .Param , param )
69+ }
70+ }
71+ }
72+
73+ func TestEventTypedErrorHelpers (t * testing.T ) {
74+ cause := errors .New ("cause" )
75+
76+ validation := eventValidationError ("bad input" )
77+ requireProblem (t , validation , errs .CategoryValidation , errs .SubtypeInvalidArgument , "" )
78+
79+ paramErr := eventValidationParamErrorWithCause (cause , "--flag" , "bad %s value" , "flag" )
80+ requireProblem (t , paramErr , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--flag" )
81+ if got := paramErr .Error (); got != "bad flag value: cause" {
82+ t .Fatalf ("message = %q, want %q" , got , "bad flag value: cause" )
83+ }
84+ if ! errors .Is (paramErr , cause ) {
85+ t .Fatal ("validation error should preserve its cause" )
86+ }
87+
88+ fileErr := eventFileIOError (cause , "write failed" )
89+ requireProblem (t , fileErr , errs .CategoryInternal , errs .SubtypeFileIO , "" )
90+ if got := fileErr .Error (); got != "write failed: cause" {
91+ t .Fatalf ("message = %q, want %q" , got , "write failed: cause" )
92+ }
93+ if ! errors .Is (fileErr , cause ) {
94+ t .Fatal ("file_io error should preserve its cause" )
95+ }
96+
97+ networkErr := eventNetworkError (cause , "websocket failed" )
98+ requireProblem (t , networkErr , errs .CategoryNetwork , errs .SubtypeNetworkTransport , "" )
99+ if got := networkErr .Error (); got != "websocket failed: cause" {
100+ t .Fatalf ("message = %q, want %q" , got , "websocket failed: cause" )
101+ }
102+ if ! errors .Is (networkErr , cause ) {
103+ t .Fatal ("network error should preserve its cause" )
104+ }
105+ }
106+
107+ func newSubscribeTestRuntime (t * testing.T ) * common.RuntimeContext {
108+ t .Helper ()
109+
110+ var out , errOut bytes.Buffer
111+ cmd := & cobra.Command {Use : "+subscribe" }
112+ cmd .Flags ().String ("event-types" , "" , "" )
113+ cmd .Flags ().String ("filter" , "" , "" )
114+ cmd .Flags ().Bool ("json" , false , "" )
115+ cmd .Flags ().Bool ("compact" , false , "" )
116+ cmd .Flags ().String ("output-dir" , "" , "" )
117+ cmd .Flags ().Bool ("quiet" , false , "" )
118+ cmd .Flags ().StringArray ("route" , nil , "" )
119+ cmd .Flags ().Bool ("force" , false , "" )
120+
121+ return & common.RuntimeContext {
122+ Cmd : cmd ,
123+ Config : & core.CliConfig {
124+ AppID : "cli_event_test" ,
125+ AppSecret : "secret" ,
126+ Brand : core .BrandFeishu ,
127+ },
128+ Factory : & cmdutil.Factory {
129+ IOStreams : cmdutil .NewIOStreams (strings .NewReader ("" ), & out , & errOut ),
130+ },
131+ }
132+ }
133+
47134// --- Registry ---
48135
49136func TestRegistryLookup (t * testing.T ) {
@@ -63,9 +150,11 @@ func TestRegistryDuplicateReturnsError(t *testing.T) {
63150 if err := r .Register (& ImMessageProcessor {}); err != nil {
64151 t .Fatalf ("first register should succeed: %v" , err )
65152 }
66- if err := r .Register (& ImMessageProcessor {}); err == nil {
153+ err := r .Register (& ImMessageProcessor {})
154+ if err == nil {
67155 t .Error ("expected error on duplicate registration" )
68156 }
157+ requireProblem (t , err , errs .CategoryInternal , errs .SubtypeUnknown , "" )
69158}
70159
71160// --- Filters ---
@@ -106,6 +195,54 @@ func TestRegexFilter_Invalid(t *testing.T) {
106195 }
107196}
108197
198+ func TestEventSubscribeExecuteRejectsUnsafeOutputDir (t * testing.T ) {
199+ rt := newSubscribeTestRuntime (t )
200+ if err := rt .Cmd .Flags ().Set ("output-dir" , "/tmp/events" ); err != nil {
201+ t .Fatal (err )
202+ }
203+ err := EventSubscribe .Execute (context .Background (), rt )
204+ if err == nil {
205+ t .Fatal ("expected unsafe output-dir error" )
206+ }
207+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--output-dir" )
208+ if errors .Unwrap (err ) == nil {
209+ t .Fatal ("unsafe output-dir error should preserve its cause" )
210+ }
211+ }
212+
213+ func TestEventSubscribeExecuteRejectsInvalidFilter (t * testing.T ) {
214+ rt := newSubscribeTestRuntime (t )
215+ if err := rt .Cmd .Flags ().Set ("force" , "true" ); err != nil {
216+ t .Fatal (err )
217+ }
218+ if err := rt .Cmd .Flags ().Set ("filter" , "[invalid" ); err != nil {
219+ t .Fatal (err )
220+ }
221+ err := EventSubscribe .Execute (context .Background (), rt )
222+ if err == nil {
223+ t .Fatal ("expected invalid filter error" )
224+ }
225+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--filter" )
226+ if errors .Unwrap (err ) == nil {
227+ t .Fatal ("invalid filter error should preserve its cause" )
228+ }
229+ }
230+
231+ func TestEventSubscribeExecuteRejectsInvalidRoute (t * testing.T ) {
232+ rt := newSubscribeTestRuntime (t )
233+ if err := rt .Cmd .Flags ().Set ("force" , "true" ); err != nil {
234+ t .Fatal (err )
235+ }
236+ if err := rt .Cmd .Flags ().Set ("route" , "no-equals-sign" ); err != nil {
237+ t .Fatal (err )
238+ }
239+ err := EventSubscribe .Execute (context .Background (), rt )
240+ if err == nil {
241+ t .Fatal ("expected invalid route error" )
242+ }
243+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
244+ }
245+
109246func TestFilterChain (t * testing.T ) {
110247 etf := NewEventTypeFilter ("im.message.receive_v1, drive.file.edit_v1" )
111248 rf , _ := NewRegexFilter ("im\\ ..*" )
@@ -339,6 +476,23 @@ func TestPipeline_OutputDir(t *testing.T) {
339476 }
340477}
341478
479+ func TestPipeline_EnsureDirsFileIOError (t * testing.T ) {
480+ path := filepath .Join (t .TempDir (), "not-a-dir" )
481+ if err := os .WriteFile (path , []byte ("x" ), 0600 ); err != nil {
482+ t .Fatal (err )
483+ }
484+ p := NewEventPipeline (DefaultRegistry (), NewFilterChain (),
485+ PipelineConfig {Mode : TransformCompact , OutputDir : filepath .Join (path , "child" )}, io .Discard , io .Discard )
486+ err := p .EnsureDirs ()
487+ if err == nil {
488+ t .Fatal ("expected file_io error" )
489+ }
490+ requireProblem (t , err , errs .CategoryInternal , errs .SubtypeFileIO , "" )
491+ if errors .Unwrap (err ) == nil {
492+ t .Fatal ("file_io error should preserve its cause" )
493+ }
494+ }
495+
342496// --- Pipeline: JsonFlag ---
343497
344498func TestPipeline_JsonFlag (t * testing.T ) {
@@ -608,20 +762,26 @@ func TestParseRoutes_MissingEquals(t *testing.T) {
608762 if err == nil {
609763 t .Error ("expected error for missing =" )
610764 }
765+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
611766}
612767
613768func TestParseRoutes_InvalidRegex (t * testing.T ) {
614769 _ , err := ParseRoutes ([]string {"[invalid=dir:./foo/" })
615770 if err == nil {
616771 t .Error ("expected error for invalid regex" )
617772 }
773+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
774+ if errors .Unwrap (err ) == nil {
775+ t .Fatal ("invalid regex error should preserve its cause" )
776+ }
618777}
619778
620779func TestParseRoutes_MissingPrefix (t * testing.T ) {
621780 _ , err := ParseRoutes ([]string {`^im\.message=./messages/` })
622781 if err == nil {
623782 t .Error ("expected error for missing dir: prefix" )
624783 }
784+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
625785 if ! strings .Contains (err .Error (), "dir:" ) {
626786 t .Errorf ("error should mention dir: prefix, got: %v" , err )
627787 }
@@ -632,20 +792,23 @@ func TestParseRoutes_EmptyPath(t *testing.T) {
632792 if err == nil {
633793 t .Error ("expected error for empty path" )
634794 }
795+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
635796}
636797
637798func TestParseRoutes_RejectsAbsolutePath (t * testing.T ) {
638799 _ , err := ParseRoutes ([]string {`^test=dir:/tmp/evil` })
639800 if err == nil {
640801 t .Error ("expected error for absolute path in route" )
641802 }
803+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
642804}
643805
644806func TestParseRoutes_RejectsTraversal (t * testing.T ) {
645807 _ , err := ParseRoutes ([]string {`^test=dir:../../etc/evil` })
646808 if err == nil {
647809 t .Error ("expected error for path traversal in route" )
648810 }
811+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
649812}
650813
651814func TestParseRoutes_PathSafety (t * testing.T ) {
0 commit comments