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  }