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,72 @@ 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+ fileErr := eventFileIOError ("write failed: %s" , cause , cause )
80+ requireProblem (t , fileErr , errs .CategoryInternal , errs .SubtypeFileIO , "" )
81+ if ! errors .Is (fileErr , cause ) {
82+ t .Fatal ("file_io error should preserve its cause" )
83+ }
84+
85+ networkErr := eventNetworkError ("websocket failed: %s" , cause , cause )
86+ requireProblem (t , networkErr , errs .CategoryNetwork , errs .SubtypeNetworkTransport , "" )
87+ if ! errors .Is (networkErr , cause ) {
88+ t .Fatal ("network error should preserve its cause" )
89+ }
90+ }
91+
92+ func newSubscribeTestRuntime (t * testing.T ) * common.RuntimeContext {
93+ t .Helper ()
94+
95+ var out , errOut bytes.Buffer
96+ cmd := & cobra.Command {Use : "+subscribe" }
97+ cmd .Flags ().String ("event-types" , "" , "" )
98+ cmd .Flags ().String ("filter" , "" , "" )
99+ cmd .Flags ().Bool ("json" , false , "" )
100+ cmd .Flags ().Bool ("compact" , false , "" )
101+ cmd .Flags ().String ("output-dir" , "" , "" )
102+ cmd .Flags ().Bool ("quiet" , false , "" )
103+ cmd .Flags ().StringArray ("route" , nil , "" )
104+ cmd .Flags ().Bool ("force" , false , "" )
105+
106+ return & common.RuntimeContext {
107+ Cmd : cmd ,
108+ Config : & core.CliConfig {
109+ AppID : "cli_event_test" ,
110+ AppSecret : "secret" ,
111+ Brand : core .BrandFeishu ,
112+ },
113+ Factory : & cmdutil.Factory {
114+ IOStreams : cmdutil .NewIOStreams (strings .NewReader ("" ), & out , & errOut ),
115+ },
116+ }
117+ }
118+
47119// --- Registry ---
48120
49121func TestRegistryLookup (t * testing.T ) {
@@ -63,9 +135,11 @@ func TestRegistryDuplicateReturnsError(t *testing.T) {
63135 if err := r .Register (& ImMessageProcessor {}); err != nil {
64136 t .Fatalf ("first register should succeed: %v" , err )
65137 }
66- if err := r .Register (& ImMessageProcessor {}); err == nil {
138+ err := r .Register (& ImMessageProcessor {})
139+ if err == nil {
67140 t .Error ("expected error on duplicate registration" )
68141 }
142+ requireProblem (t , err , errs .CategoryInternal , errs .SubtypeUnknown , "" )
69143}
70144
71145// --- Filters ---
@@ -106,6 +180,54 @@ func TestRegexFilter_Invalid(t *testing.T) {
106180 }
107181}
108182
183+ func TestEventSubscribeExecuteRejectsUnsafeOutputDir (t * testing.T ) {
184+ rt := newSubscribeTestRuntime (t )
185+ if err := rt .Cmd .Flags ().Set ("output-dir" , "/tmp/events" ); err != nil {
186+ t .Fatal (err )
187+ }
188+ err := EventSubscribe .Execute (context .Background (), rt )
189+ if err == nil {
190+ t .Fatal ("expected unsafe output-dir error" )
191+ }
192+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--output-dir" )
193+ if errors .Unwrap (err ) == nil {
194+ t .Fatal ("unsafe output-dir error should preserve its cause" )
195+ }
196+ }
197+
198+ func TestEventSubscribeExecuteRejectsInvalidFilter (t * testing.T ) {
199+ rt := newSubscribeTestRuntime (t )
200+ if err := rt .Cmd .Flags ().Set ("force" , "true" ); err != nil {
201+ t .Fatal (err )
202+ }
203+ if err := rt .Cmd .Flags ().Set ("filter" , "[invalid" ); err != nil {
204+ t .Fatal (err )
205+ }
206+ err := EventSubscribe .Execute (context .Background (), rt )
207+ if err == nil {
208+ t .Fatal ("expected invalid filter error" )
209+ }
210+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--filter" )
211+ if errors .Unwrap (err ) == nil {
212+ t .Fatal ("invalid filter error should preserve its cause" )
213+ }
214+ }
215+
216+ func TestEventSubscribeExecuteRejectsInvalidRoute (t * testing.T ) {
217+ rt := newSubscribeTestRuntime (t )
218+ if err := rt .Cmd .Flags ().Set ("force" , "true" ); err != nil {
219+ t .Fatal (err )
220+ }
221+ if err := rt .Cmd .Flags ().Set ("route" , "no-equals-sign" ); err != nil {
222+ t .Fatal (err )
223+ }
224+ err := EventSubscribe .Execute (context .Background (), rt )
225+ if err == nil {
226+ t .Fatal ("expected invalid route error" )
227+ }
228+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
229+ }
230+
109231func TestFilterChain (t * testing.T ) {
110232 etf := NewEventTypeFilter ("im.message.receive_v1, drive.file.edit_v1" )
111233 rf , _ := NewRegexFilter ("im\\ ..*" )
@@ -339,6 +461,23 @@ func TestPipeline_OutputDir(t *testing.T) {
339461 }
340462}
341463
464+ func TestPipeline_EnsureDirsFileIOError (t * testing.T ) {
465+ path := filepath .Join (t .TempDir (), "not-a-dir" )
466+ if err := os .WriteFile (path , []byte ("x" ), 0600 ); err != nil {
467+ t .Fatal (err )
468+ }
469+ p := NewEventPipeline (DefaultRegistry (), NewFilterChain (),
470+ PipelineConfig {Mode : TransformCompact , OutputDir : filepath .Join (path , "child" )}, io .Discard , io .Discard )
471+ err := p .EnsureDirs ()
472+ if err == nil {
473+ t .Fatal ("expected file_io error" )
474+ }
475+ requireProblem (t , err , errs .CategoryInternal , errs .SubtypeFileIO , "" )
476+ if errors .Unwrap (err ) == nil {
477+ t .Fatal ("file_io error should preserve its cause" )
478+ }
479+ }
480+
342481// --- Pipeline: JsonFlag ---
343482
344483func TestPipeline_JsonFlag (t * testing.T ) {
@@ -608,20 +747,26 @@ func TestParseRoutes_MissingEquals(t *testing.T) {
608747 if err == nil {
609748 t .Error ("expected error for missing =" )
610749 }
750+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
611751}
612752
613753func TestParseRoutes_InvalidRegex (t * testing.T ) {
614754 _ , err := ParseRoutes ([]string {"[invalid=dir:./foo/" })
615755 if err == nil {
616756 t .Error ("expected error for invalid regex" )
617757 }
758+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
759+ if errors .Unwrap (err ) == nil {
760+ t .Fatal ("invalid regex error should preserve its cause" )
761+ }
618762}
619763
620764func TestParseRoutes_MissingPrefix (t * testing.T ) {
621765 _ , err := ParseRoutes ([]string {`^im\.message=./messages/` })
622766 if err == nil {
623767 t .Error ("expected error for missing dir: prefix" )
624768 }
769+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
625770 if ! strings .Contains (err .Error (), "dir:" ) {
626771 t .Errorf ("error should mention dir: prefix, got: %v" , err )
627772 }
@@ -632,20 +777,23 @@ func TestParseRoutes_EmptyPath(t *testing.T) {
632777 if err == nil {
633778 t .Error ("expected error for empty path" )
634779 }
780+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
635781}
636782
637783func TestParseRoutes_RejectsAbsolutePath (t * testing.T ) {
638784 _ , err := ParseRoutes ([]string {`^test=dir:/tmp/evil` })
639785 if err == nil {
640786 t .Error ("expected error for absolute path in route" )
641787 }
788+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
642789}
643790
644791func TestParseRoutes_RejectsTraversal (t * testing.T ) {
645792 _ , err := ParseRoutes ([]string {`^test=dir:../../etc/evil` })
646793 if err == nil {
647794 t .Error ("expected error for path traversal in route" )
648795 }
796+ requireProblem (t , err , errs .CategoryValidation , errs .SubtypeInvalidArgument , "--route" )
649797}
650798
651799func TestParseRoutes_PathSafety (t * testing.T ) {
0 commit comments