github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/automation/spec/trigger.go (about) 1 package spec 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "testing" 8 "time" 9 10 "github.com/google/go-cmp/cmp" 11 "github.com/qri-io/qri/automation/trigger" 12 "github.com/qri-io/qri/automation/workflow" 13 "github.com/qri-io/qri/event" 14 ) 15 16 // AssertTrigger confirms the expected behavior of a trigger.Trigger Interface 17 // implementation 18 func AssertTrigger(t *testing.T, trig trigger.Trigger, advanced map[string]interface{}) { 19 if trig.Type() == "" { 20 t.Error("Type method must return a non-empty trigger.Type") 21 } 22 if err := trig.Advance(); err != nil { 23 t.Fatalf("trigger.Advance() unexpected error: %s", err) 24 } 25 triggerObj := trig.ToMap() 26 if diff := cmp.Diff(advanced, triggerObj); diff != "" { 27 t.Errorf("advanced trigger mismatch (-want +got):\n%s", diff) 28 } 29 30 if err := trig.SetActive(true); err != nil { 31 t.Fatalf("trigger.SetActive unexpected error: %s", err) 32 } 33 if !trig.Active() { 34 t.Error("expected trigger.Active() to be true after trigger.SetActive(true)") 35 } 36 if err := trig.SetActive(false); err != nil { 37 t.Fatalf("trigger.SetActive unexpected error: %s", err) 38 } 39 if trig.Active() { 40 t.Error("expected trigger.Active() to be false after trigger.SetActive(false)") 41 } 42 triggerBytes, err := json.Marshal(trig) 43 if err != nil { 44 t.Fatalf("json.Marshal unexpected error: %s", err) 45 } 46 triggerObj = map[string]interface{}{} 47 if err := json.Unmarshal(triggerBytes, &triggerObj); err != nil { 48 t.Fatalf("json.Unmarshal unexpected error: %s", err) 49 } 50 triggerType, ok := triggerObj["type"] 51 if !ok { 52 t.Fatal("json.Marshal error, expected Type field to exist") 53 } 54 if triggerType != trig.Type() { 55 t.Fatalf("json.Marshal error, expected marshalled type %q to match trigger.Type() %q", triggerType, trig.Type()) 56 } 57 if err := json.Unmarshal(triggerBytes, &triggerObj); err != nil { 58 t.Fatalf("json.Unmarshal unexpected error: %s", err) 59 } 60 61 triggerObj["type"] = "bad trigger type" 62 triggerBytes, err = json.Marshal(triggerObj) 63 if err != nil { 64 t.Fatalf("json.Marshal unexpected error: %s", err) 65 } 66 if err := json.Unmarshal(triggerBytes, trig); !errors.Is(err, trigger.ErrUnexpectedType) { 67 t.Fatalf("json.Unmarshal should emit a `trigger.ErrUnexpectedType` error if the given type does not match the trigger.Type of the Trigger") 68 } 69 triggerObj = trig.ToMap() 70 triggerType, ok = triggerObj["type"] 71 if !ok { 72 t.Fatal("trigger.ToMap() error, expected 'type' field to exist") 73 } 74 if triggerType != trig.Type() { 75 t.Fatalf("trigger.ToMap() error, expected map type %q to match trigger.Type() %q", triggerType, trig.Type()) 76 } 77 triggerActive, ok := triggerObj["active"] 78 if !ok { 79 t.Fatal("trigger.ToMap() error, expected 'active' field to exist") 80 } 81 if triggerActive != trig.Active() { 82 t.Fatalf("trigger.ToMap() error, expected map field 'active' to match trig.Active() value ") 83 } 84 triggerID, ok := triggerObj["id"] 85 if !ok { 86 t.Fatal("trigger.ToMap() error, expected 'id' field to exist") 87 } 88 if triggerID != trig.ID() { 89 t.Fatal("trigger.ToMap() error, expected map field 'id' to match trig.ID() value") 90 } 91 } 92 93 // ListenerConstructor creates a trigger listener and function that fires the 94 // listener when called, and a function that advances the trigger & updates 95 // the source 96 type ListenerConstructor func(ctx context.Context, bus event.Bus) (listener trigger.Listener, activate func(), advance func()) 97 98 // AssertListener confirms the expected behavior of a trigger.Listener 99 // NOTE: this does not confirm behavior of the `Listen` functionality 100 // beyond the basic usage of adding a trigger using a `trigger.Source` 101 func AssertListener(t *testing.T, listenerConstructor ListenerConstructor) { 102 ctx := context.Background() 103 bus := event.NewBus(ctx) 104 listener, activateTrigger, advanceTrigger := listenerConstructor(ctx, bus) 105 wf := &workflow.Workflow{} 106 if err := listener.Listen(wf); !errors.Is(err, trigger.ErrEmptyWorkflowID) { 107 t.Fatal("listener.Listen should emit a trigger.ErrEmptyWorkflowID if the WorkflowID of the trigger.Source is empty") 108 } 109 wf = &workflow.Workflow{ID: "workflow_id"} 110 if err := listener.Listen(wf); !errors.Is(err, trigger.ErrEmptyOwnerID) { 111 t.Fatal("listener.Listen should emit a trigger.ErrEmptyOwnerID if the OwnerID if the trigger.Source is emtpy") 112 } 113 114 triggered := make(chan string) 115 handler := func(ctx context.Context, e event.Event) error { 116 if e.Type == event.ETAutomationWorkflowTrigger { 117 triggered <- "triggered!" 118 } 119 return nil 120 } 121 bus.SubscribeTypes(handler, event.ETAutomationWorkflowTrigger) 122 done := shouldTimeout(t, triggered, "listener should not emit events until the listener has been started by running `listener.Start()`", time.Millisecond*500) 123 activateTrigger() 124 <-done 125 126 done = errOnTimeout(t, triggered, "listener did not emit an event.ETAutomationWorkflowTrigger event when the trigger was activated", time.Millisecond*500) 127 if err := listener.Start(ctx); err != nil { 128 t.Fatalf("listener.Start unexpected error: %s", err) 129 } 130 activateTrigger() 131 <-done 132 advanceTrigger() 133 134 done = shouldTimeout(t, triggered, "listener should not emit events once the listener has run `listener.Stop()`", time.Millisecond*500) 135 if err := listener.Stop(); err != nil { 136 t.Fatalf("listener.Stop unexpected error: %s", err) 137 } 138 activateTrigger() 139 <-done 140 } 141 142 func errOnTimeout(t *testing.T, c chan string, errMsg string, timeoutDuration time.Duration) <-chan struct{} { 143 done := make(chan struct{}) 144 go func() { 145 select { 146 case msg := <-c: 147 t.Log(msg) 148 case <-time.After(timeoutDuration): 149 t.Errorf(errMsg) 150 } 151 done <- struct{}{} 152 }() 153 return done 154 } 155 156 func shouldTimeout(t *testing.T, c chan string, errMsg string, timeoutDuration time.Duration) <-chan struct{} { 157 done := make(chan struct{}) 158 go func() { 159 select { 160 case badMsg := <-c: 161 t.Errorf(badMsg) 162 case <-time.After(timeoutDuration): 163 t.Log("expected timeout") 164 } 165 done <- struct{}{} 166 }() 167 return done 168 }