11package manager
22
33import (
4+ "context"
45 "testing"
56
7+ "github.com/docker/cli/cli/config/configfile"
8+ "github.com/spf13/cobra"
69 "gotest.tools/v3/assert"
710 is "gotest.tools/v3/assert/cmp"
811)
912
13+ type fakeConfigProvider struct {
14+ cfg * configfile.ConfigFile
15+ }
16+
17+ func (f * fakeConfigProvider ) ConfigFile () * configfile.ConfigFile {
18+ return f .cfg
19+ }
20+
1021func TestGetNaiveFlags (t * testing.T ) {
1122 testCases := []struct {
1223 args []string
@@ -40,12 +51,15 @@ func TestGetNaiveFlags(t *testing.T) {
4051
4152func TestPluginMatch (t * testing.T ) {
4253 testCases := []struct {
43- commandString string
44- pluginConfig map [string ]string
45- expectedMatch string
46- expectedOk bool
54+ doc string
55+ commandString string
56+ pluginConfig map [string ]string
57+ cmdErrorMessage string
58+ expectedMatch string
59+ expectedOk bool
4760 }{
4861 {
62+ doc : "hooks prefix match" ,
4963 commandString : "image ls" ,
5064 pluginConfig : map [string ]string {
5165 "hooks" : "image" ,
@@ -54,6 +68,7 @@ func TestPluginMatch(t *testing.T) {
5468 expectedOk : true ,
5569 },
5670 {
71+ doc : "hooks no match" ,
5772 commandString : "context ls" ,
5873 pluginConfig : map [string ]string {
5974 "hooks" : "build" ,
@@ -62,6 +77,7 @@ func TestPluginMatch(t *testing.T) {
6277 expectedOk : false ,
6378 },
6479 {
80+ doc : "hooks exact match" ,
6581 commandString : "context ls" ,
6682 pluginConfig : map [string ]string {
6783 "hooks" : "context ls" ,
@@ -70,6 +86,7 @@ func TestPluginMatch(t *testing.T) {
7086 expectedOk : true ,
7187 },
7288 {
89+ doc : "hooks first match wins" ,
7390 commandString : "image ls" ,
7491 pluginConfig : map [string ]string {
7592 "hooks" : "image ls,image" ,
@@ -78,6 +95,7 @@ func TestPluginMatch(t *testing.T) {
7895 expectedOk : true ,
7996 },
8097 {
98+ doc : "hooks empty string" ,
8199 commandString : "image ls" ,
82100 pluginConfig : map [string ]string {
83101 "hooks" : "" ,
@@ -86,6 +104,7 @@ func TestPluginMatch(t *testing.T) {
86104 expectedOk : false ,
87105 },
88106 {
107+ doc : "hooks partial token no match" ,
89108 commandString : "image inspect" ,
90109 pluginConfig : map [string ]string {
91110 "hooks" : "image i" ,
@@ -94,19 +113,148 @@ func TestPluginMatch(t *testing.T) {
94113 expectedOk : false ,
95114 },
96115 {
116+ doc : "hooks prefix token match" ,
97117 commandString : "image inspect" ,
98118 pluginConfig : map [string ]string {
99119 "hooks" : "image" ,
100120 },
101121 expectedMatch : "image" ,
102122 expectedOk : true ,
103123 },
124+ {
125+ doc : "error-hooks match on error" ,
126+ commandString : "build" ,
127+ pluginConfig : map [string ]string {
128+ "error-hooks" : "build" ,
129+ },
130+ cmdErrorMessage : "exit status 1" ,
131+ expectedMatch : "build" ,
132+ expectedOk : true ,
133+ },
134+ {
135+ doc : "error-hooks no match on success" ,
136+ commandString : "build" ,
137+ pluginConfig : map [string ]string {
138+ "error-hooks" : "build" ,
139+ },
140+ cmdErrorMessage : "" ,
141+ expectedMatch : "" ,
142+ expectedOk : false ,
143+ },
144+ {
145+ doc : "error-hooks prefix match on error" ,
146+ commandString : "compose up" ,
147+ pluginConfig : map [string ]string {
148+ "error-hooks" : "compose" ,
149+ },
150+ cmdErrorMessage : "exit status 1" ,
151+ expectedMatch : "compose" ,
152+ expectedOk : true ,
153+ },
154+ {
155+ doc : "error-hooks no match for wrong command" ,
156+ commandString : "pull" ,
157+ pluginConfig : map [string ]string {
158+ "error-hooks" : "build" ,
159+ },
160+ cmdErrorMessage : "exit status 1" ,
161+ expectedMatch : "" ,
162+ expectedOk : false ,
163+ },
164+ {
165+ doc : "hooks takes precedence over error-hooks" ,
166+ commandString : "build" ,
167+ pluginConfig : map [string ]string {
168+ "hooks" : "build" ,
169+ "error-hooks" : "build" ,
170+ },
171+ cmdErrorMessage : "exit status 1" ,
172+ expectedMatch : "build" ,
173+ expectedOk : true ,
174+ },
175+ {
176+ doc : "hooks fires on success even with error-hooks configured" ,
177+ commandString : "build" ,
178+ pluginConfig : map [string ]string {
179+ "hooks" : "build" ,
180+ "error-hooks" : "build" ,
181+ },
182+ cmdErrorMessage : "" ,
183+ expectedMatch : "build" ,
184+ expectedOk : true ,
185+ },
186+ {
187+ doc : "error-hooks with multiple commands" ,
188+ commandString : "compose up" ,
189+ pluginConfig : map [string ]string {
190+ "error-hooks" : "build,compose up,pull" ,
191+ },
192+ cmdErrorMessage : "exit status 1" ,
193+ expectedMatch : "compose up" ,
194+ expectedOk : true ,
195+ },
196+ }
197+
198+ for _ , tc := range testCases {
199+ t .Run (tc .doc , func (t * testing.T ) {
200+ match , ok := pluginMatch (tc .pluginConfig , tc .commandString , tc .cmdErrorMessage )
201+ assert .Equal (t , ok , tc .expectedOk )
202+ assert .Equal (t , match , tc .expectedMatch )
203+ })
204+ }
205+ }
206+
207+ func TestMatchHookConfig (t * testing.T ) {
208+ testCases := []struct {
209+ doc string
210+ configuredHooks string
211+ subCmd string
212+ expectedMatch string
213+ expectedOk bool
214+ }{
215+ {
216+ doc : "empty config" ,
217+ configuredHooks : "" ,
218+ subCmd : "build" ,
219+ expectedMatch : "" ,
220+ expectedOk : false ,
221+ },
222+ {
223+ doc : "exact match" ,
224+ configuredHooks : "build" ,
225+ subCmd : "build" ,
226+ expectedMatch : "build" ,
227+ expectedOk : true ,
228+ },
229+ {
230+ doc : "prefix match" ,
231+ configuredHooks : "image" ,
232+ subCmd : "image ls" ,
233+ expectedMatch : "image" ,
234+ expectedOk : true ,
235+ },
236+ {
237+ doc : "comma-separated match" ,
238+ configuredHooks : "pull,build,push" ,
239+ subCmd : "build" ,
240+ expectedMatch : "build" ,
241+ expectedOk : true ,
242+ },
243+ {
244+ doc : "no match" ,
245+ configuredHooks : "pull,push" ,
246+ subCmd : "build" ,
247+ expectedMatch : "" ,
248+ expectedOk : false ,
249+ },
104250 }
105251
106252 for _ , tc := range testCases {
107- match , ok := pluginMatch (tc .pluginConfig , tc .commandString )
108- assert .Equal (t , ok , tc .expectedOk )
109- assert .Equal (t , match , tc .expectedMatch )
253+ t .Run (tc .doc , func (t * testing.T ) {
254+ match , ok := matchHookConfig (tc .configuredHooks , tc .subCmd )
255+ assert .Equal (t , ok , tc .expectedOk )
256+ assert .Equal (t , match , tc .expectedMatch )
257+ })
110258 }
111259}
112260
@@ -141,3 +289,87 @@ func TestAppendNextSteps(t *testing.T) {
141289 })
142290 }
143291}
292+
293+ func TestRunPluginHooksPassesErrorMessage (t * testing.T ) {
294+ cfg := configfile .New ("" )
295+ cfg .Plugins = map [string ]map [string ]string {
296+ "test-plugin" : {"hooks" : "build" },
297+ }
298+ provider := & fakeConfigProvider {cfg : cfg }
299+ root := & cobra.Command {Use : "docker" }
300+ sub := & cobra.Command {Use : "build" }
301+ root .AddCommand (sub )
302+
303+ // Should not panic with empty error message (success case)
304+ RunPluginHooks (context .Background (), provider , root , sub , []string {"build" }, "" )
305+
306+ // Should not panic with non-empty error message (failure case)
307+ RunPluginHooks (context .Background (), provider , root , sub , []string {"build" }, "exit status 1" )
308+ }
309+
310+ func TestRunPluginHooksErrorHooks (t * testing.T ) {
311+ cfg := configfile .New ("" )
312+ cfg .Plugins = map [string ]map [string ]string {
313+ "test-plugin" : {"error-hooks" : "build" },
314+ }
315+ provider := & fakeConfigProvider {cfg : cfg }
316+ root := & cobra.Command {Use : "docker" }
317+ sub := & cobra.Command {Use : "build" }
318+ root .AddCommand (sub )
319+
320+ // Should not panic — error-hooks with error message
321+ RunPluginHooks (context .Background (), provider , root , sub , []string {"build" }, "exit status 1" )
322+
323+ // Should not panic — error-hooks with no error (should be skipped)
324+ RunPluginHooks (context .Background (), provider , root , sub , []string {"build" }, "" )
325+ }
326+
327+ func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess (t * testing.T ) {
328+ cfg := configfile .New ("" )
329+ cfg .Plugins = map [string ]map [string ]string {
330+ "nonexistent" : {"error-hooks" : "build" },
331+ }
332+ root := & cobra.Command {Use : "docker" }
333+ sub := & cobra.Command {Use : "build" }
334+ root .AddCommand (sub )
335+
336+ // On success, error-hooks should not match, so the plugin
337+ // binary is never looked up and no results are returned.
338+ result := invokeAndCollectHooks (
339+ context .Background (), cfg , root , sub ,
340+ "build" , map [string ]string {}, "" ,
341+ )
342+ assert .Check (t , is .Len (result , 0 ))
343+ }
344+
345+ func TestInvokeAndCollectHooksNoPlugins (t * testing.T ) {
346+ cfg := configfile .New ("" )
347+ root := & cobra.Command {Use : "docker" }
348+ sub := & cobra.Command {Use : "build" }
349+ root .AddCommand (sub )
350+
351+ result := invokeAndCollectHooks (
352+ context .Background (), cfg , root , sub ,
353+ "build" , map [string ]string {}, "some error" ,
354+ )
355+ assert .Check (t , is .Len (result , 0 ))
356+ }
357+
358+ func TestInvokeAndCollectHooksCancelledContext (t * testing.T ) {
359+ cfg := configfile .New ("" )
360+ cfg .Plugins = map [string ]map [string ]string {
361+ "test-plugin" : {"hooks" : "build" },
362+ }
363+ root := & cobra.Command {Use : "docker" }
364+ sub := & cobra.Command {Use : "build" }
365+ root .AddCommand (sub )
366+
367+ ctx , cancel := context .WithCancel (context .Background ())
368+ cancel () // cancel immediately
369+
370+ result := invokeAndCollectHooks (
371+ ctx , cfg , root , sub ,
372+ "build" , map [string ]string {}, "exit status 1" ,
373+ )
374+ assert .Check (t , is .Nil (result ))
375+ }
0 commit comments