github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/automation/spec/workflow_store.go (about) 1 package spec 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/google/go-cmp/cmp" 12 "github.com/qri-io/qri/automation/hook" 13 "github.com/qri-io/qri/automation/trigger" 14 "github.com/qri-io/qri/automation/workflow" 15 "github.com/qri-io/qri/base/params" 16 "github.com/qri-io/qri/profile" 17 ) 18 19 // AssertWorkflowStore confirms the expected behavior of a workflow.Store Interface 20 // implementation 21 func AssertWorkflowStore(t *testing.T, store workflow.Store) { 22 ctx := context.Background() 23 seedStr := "workflow assert store seed string used for testing in the workflow package" 24 workflow.SetIDRand(strings.NewReader(seedStr)) 25 now := time.Now() 26 27 aliceInitID := "alice_dataset_id" 28 aliceProID := profile.ID("alice_pro_id") 29 aliceTrigger := map[string]interface{}{"type": trigger.RuntimeType} 30 aliceHook := map[string]interface{}{"type": hook.RuntimeType} 31 alice := &workflow.Workflow{ 32 InitID: aliceInitID, 33 OwnerID: aliceProID, 34 Created: &now, 35 Triggers: []map[string]interface{}{aliceTrigger}, 36 Hooks: []map[string]interface{}{aliceHook}, 37 } 38 got, err := store.Put(ctx, alice) 39 if err != nil { 40 t.Fatal(err) 41 } 42 if got.ID == "" { 43 t.Errorf("store.Put error: a workflow with no ID is considered a new workflow. The workflow.Store should create an ID and return the workflow with the generated ID") 44 } 45 alice.ID = got.ID 46 aliceID := alice.ID 47 if diff := cmp.Diff(alice, got); diff != "" { 48 t.Errorf("workflow mismatch (-want +got):\n%s", diff) 49 } 50 51 got, err = store.Get(ctx, aliceID) 52 if err != nil { 53 t.Fatal(err) 54 } 55 if diff := cmp.Diff(alice, got); diff != "" { 56 t.Errorf("workflow mismatch (-want +got):\n%s", diff) 57 } 58 59 got, err = store.GetByInitID(ctx, alice.InitID) 60 if err != nil { 61 t.Fatal(err) 62 } 63 if diff := cmp.Diff(alice, got); diff != "" { 64 t.Errorf("workflow mismatch (-want +got):\n%s", diff) 65 } 66 67 // store.Put error checking 68 if _, err := store.Put(ctx, &workflow.Workflow{InitID: aliceInitID}); !errors.Is(err, workflow.ErrWorkflowForDatasetExists) { 69 t.Errorf("Put method must emit `workflow.ErrWorkflowForDatasetExists` error if a workflow for the given InitID already exists") 70 } 71 72 brittInitID := "britt_dataset_id" 73 brittProID := profile.ID("britt_pro_id") 74 brittTrigger := map[string]interface{}{"type": trigger.RuntimeType} 75 brittHook := map[string]interface{}{"type": hook.RuntimeType} 76 britt := &workflow.Workflow{ 77 InitID: brittInitID, 78 OwnerID: brittProID, 79 Created: &now, 80 Triggers: []map[string]interface{}{brittTrigger}, 81 Hooks: []map[string]interface{}{brittHook}, 82 } 83 got, err = store.Put(ctx, britt) 84 if err != nil { 85 t.Fatal(err) 86 } 87 88 britt.ID = got.ID 89 if diff := cmp.Diff(britt, got); diff != "" { 90 t.Errorf("workflow mismatch (-want +got):\n%s", diff) 91 } 92 93 wfs, err := store.List(ctx, "", params.ListAll) 94 if err != nil { 95 t.Fatal(err) 96 } 97 if len(wfs) != 2 { 98 t.Fatalf("store.List count mismatch, expected 2 workflows, got %d", len(wfs)) 99 } 100 101 deployed, err := store.ListDeployed(ctx, "", params.ListAll) 102 if err != nil { 103 t.Fatal(err) 104 } 105 106 if len(deployed) != 0 { 107 t.Fatalf("store.ListDeployed count mismatch, expected 0 workflows, got %d", len(deployed)) 108 } 109 110 aliceUpdated := &workflow.Workflow{ 111 ID: aliceID, 112 InitID: alice.InitID, 113 OwnerID: alice.OwnerID, 114 Active: true, 115 Created: &now, 116 Triggers: []map[string]interface{}{aliceTrigger, brittTrigger}, 117 Hooks: []map[string]interface{}{aliceHook, brittHook}, 118 } 119 _, err = store.Put(ctx, aliceUpdated) 120 if err != nil { 121 t.Fatal(err) 122 } 123 124 got, err = store.Get(ctx, aliceID) 125 if err != nil { 126 t.Fatal(err) 127 } 128 if diff := cmp.Diff(aliceUpdated, got); diff != "" { 129 t.Errorf("workflow mismatch (-want +got):\n%s", diff) 130 } 131 132 deployed, err = store.ListDeployed(ctx, "", params.ListAll) 133 if err != nil { 134 t.Fatal(err) 135 } 136 137 if len(deployed) != 1 { 138 t.Fatalf("store.ListDeployed count mismatch, expected 1 workflow, got %d", len(deployed)) 139 } 140 if diff := cmp.Diff(aliceUpdated, deployed[0]); diff != "" { 141 t.Errorf("workflow mismatch (-want +got):\n%s", diff) 142 } 143 144 err = store.Remove(ctx, aliceID) 145 if err != nil { 146 t.Fatal(err) 147 } 148 149 _, err = store.Get(ctx, aliceID) 150 if !errors.Is(err, workflow.ErrNotFound) { 151 t.Errorf("store.Get error mistmatch, expected %q, got %q", workflow.ErrNotFound, err) 152 } 153 } 154 155 // AssertWorkflowLister confirms the expected behavior of a workflow.Lister Interface 156 // implementation 157 func AssertWorkflowLister(t *testing.T, store workflow.Store) { 158 // set up 159 workflow.SetIDRand(strings.NewReader(strings.Repeat("Lorem ipsum dolor sit amet", 20))) 160 ctx := context.Background() 161 expectedAllWorkflows := [10]*workflow.Workflow{} 162 expectedDeployedWorkflows := [5]*workflow.Workflow{} 163 164 proID := profile.ID("profile_id") 165 for i := 0; i < 10; i++ { 166 now := time.Now() 167 wf, err := store.Put(ctx, &workflow.Workflow{ 168 InitID: fmt.Sprintf("dataset_%d", i), 169 OwnerID: proID, 170 Created: &now, 171 }) 172 if err != nil { 173 t.Fatal(err) 174 } 175 if i%2 == 0 { 176 wf.Active = true 177 expectedDeployedWorkflows[4-(i/2)] = wf 178 } 179 expectedAllWorkflows[9-i] = wf 180 } 181 182 // error cases 183 errCases := []errTestCase{ 184 {"negative limit", "test_owner", -10, 0, "limit of -10 is out of bounds"}, 185 {"negative offset", "test_owner", 0, -1, "offset of -1 is out of bounds"}, 186 } 187 188 runListErrTestCases(ctx, t, "List", store.List, errCases) 189 runListErrTestCases(ctx, t, "ListDeployed", store.ListDeployed, errCases) 190 191 // empty list cases 192 emptyCases := []emptyTestCase{ 193 {"offset out of bounds", 10, 100}, 194 {"zero limit", 0, 0}, 195 } 196 197 runListEmptyTestCases(ctx, t, "List", store.List, emptyCases) 198 runListEmptyTestCases(ctx, t, "ListDeployed", store.ListDeployed, emptyCases) 199 200 // working cases 201 cases := []expectedTestCase{ 202 {"get all", "test_owner", -1, 0, expectedAllWorkflows[:]}, 203 {"get first 4", "test_owner", 4, 0, expectedAllWorkflows[0:4]}, 204 {"get next 4", "test_owner", 4, 4, expectedAllWorkflows[4:8]}, 205 {"get last 2", "test_owner", 4, 8, expectedAllWorkflows[8:]}, 206 } 207 208 runListExpectedTestCases(ctx, t, "List", store.List, cases) 209 210 cases = []expectedTestCase{ 211 {"get all", "test_owner", -1, 0, expectedDeployedWorkflows[:]}, 212 {"get first 2", "test_owner", 2, 0, expectedDeployedWorkflows[0:2]}, 213 {"get next 2", "test_owner", 2, 2, expectedDeployedWorkflows[2:4]}, 214 {"get last 1", "test_owner", 2, 4, expectedDeployedWorkflows[4:]}, 215 } 216 runListExpectedTestCases(ctx, t, "ListDeployed", store.ListDeployed, cases) 217 } 218 219 type expectedTestCase struct { 220 description string 221 ownerID profile.ID 222 limit int 223 offset int 224 expected []*workflow.Workflow 225 } 226 227 func runListExpectedTestCases(ctx context.Context, t *testing.T, fnName string, fn listFunc, cases []expectedTestCase) { 228 for _, c := range cases { 229 got, err := fn(ctx, c.ownerID, params.List{}.WithOffsetLimit(c.offset, c.limit)) 230 if err != nil { 231 t.Errorf("%s case %s: unexpected error %w", fnName, c.description, err) 232 continue 233 } 234 if diff := cmp.Diff(c.expected, got); diff != "" { 235 t.Errorf("%s case %s: workflow list mismatch (-want +got):\n%s", fnName, c.description, diff) 236 } 237 } 238 } 239 240 type listFunc func(ctx context.Context, pid profile.ID, lp params.List) ([]*workflow.Workflow, error) 241 242 type errTestCase struct { 243 description string 244 ownerID profile.ID 245 limit int 246 offset int 247 errMsg string 248 } 249 250 func runListErrTestCases(ctx context.Context, t *testing.T, fnName string, fn listFunc, cases []errTestCase) { 251 for _, c := range cases { 252 _, err := fn(ctx, c.ownerID, params.List{}.WithOffsetLimit(c.offset, c.limit)) 253 if err == nil { 254 t.Errorf("%s case %s: error mismatch, expected %q, got no error", fnName, c.description, c.errMsg) 255 continue 256 } 257 if err.Error() != c.errMsg { 258 t.Errorf("%s case %s: error mismatch, expected %q, got %q", fnName, c.description, c.errMsg, err) 259 } 260 } 261 } 262 263 type emptyTestCase struct { 264 description string 265 limit int 266 offset int 267 } 268 269 func runListEmptyTestCases(ctx context.Context, t *testing.T, fnName string, fn listFunc, cases []emptyTestCase) { 270 expected := []*workflow.Workflow{} 271 for _, c := range cases { 272 got, err := fn(ctx, "", params.List{}.WithOffsetLimit(c.offset, c.limit)) 273 if err != nil { 274 t.Errorf("%s case %s: unexpected error %q", fnName, c.description, err) 275 continue 276 } 277 if diff := cmp.Diff(expected, got); diff != "" { 278 t.Errorf("%s case %s: workflow list mismatch (-want +got):\n%s", fnName, c.description, diff) 279 } 280 } 281 }