From e10dba16f9f3079d040a2a183b6497f7a0921d0e Mon Sep 17 00:00:00 2001 From: Don Browne Date: Thu, 15 Aug 2024 12:46:12 +0100 Subject: [PATCH] Calculate profile status based on evaluation history tables (#4149) Remove the existing triggers on rule_evaluations and rule_details_eval. Replace with triggers on latest_evaluation_statuses. Tweak the trigger logic to work on the new tables. --- .../000093_profile_status_trigger.down.sql | 197 ++++++ .../000093_profile_status_trigger.up.sql | 199 ++++++ internal/db/profiles_test.go | 590 ++++++++++++------ 3 files changed, 789 insertions(+), 197 deletions(-) create mode 100644 database/migrations/000093_profile_status_trigger.down.sql create mode 100644 database/migrations/000093_profile_status_trigger.up.sql diff --git a/database/migrations/000093_profile_status_trigger.down.sql b/database/migrations/000093_profile_status_trigger.down.sql new file mode 100644 index 0000000000..42206b8ac4 --- /dev/null +++ b/database/migrations/000093_profile_status_trigger.down.sql @@ -0,0 +1,197 @@ +-- Copyright 2024 Stacklok, Inc +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +BEGIN; + +-- Drop the triggers for evaluation_statuses +DROP TRIGGER IF EXISTS update_profile_status_after_delete ON latest_evaluation_statuses; +DROP TRIGGER IF EXISTS update_profile_status_after_delete ON latest_evaluation_statuses; + +-- Reinstate the update function as defined in migration script #54 + +CREATE OR REPLACE FUNCTION update_profile_status() RETURNS TRIGGER AS $$ +DECLARE + v_status eval_status_types; + v_profile_id UUID; + v_other_error boolean; + v_other_failed boolean; + v_other_success boolean; + v_other_skipped boolean; + v_pending boolean; +BEGIN + -- Fetch the profile_id for the current rule_eval_id + SELECT profile_id INTO v_profile_id + FROM rule_evaluations + WHERE id = NEW.rule_eval_id; + + -- The next five statements calculate whether there are, for this + -- profile, any rules in evaluations in status 'error', 'failure', + -- 'success', and 'skipped', respectively. This allows to write the + -- subsequent CASE statement in a more compact and readable fashion. + -- + -- The consequence is that this version of the stored procedure adds + -- some load w.r.t. to previous one by unconditionally executing + -- these statements, but this should not be a problem, as all five + -- queries hit the same rows, so they'll likely hit the cache. + + SELECT EXISTS ( + SELECT 1 FROM rule_details_eval rde + INNER JOIN rule_evaluations res ON res.id = rde.rule_eval_id + WHERE res.profile_id = v_profile_id + AND rde.status = 'error' + ) INTO v_other_error; + + SELECT EXISTS ( + SELECT 1 FROM rule_details_eval rde + INNER JOIN rule_evaluations res ON res.id = rde.rule_eval_id + WHERE res.profile_id = v_profile_id + AND rde.status = 'failure' + ) INTO v_other_failed; + + SELECT EXISTS ( + SELECT 1 FROM rule_details_eval rde + INNER JOIN rule_evaluations res ON res.id = rde.rule_eval_id + WHERE res.profile_id = v_profile_id + AND rde.status = 'success' + ) INTO v_other_success; + + SELECT EXISTS ( + SELECT 1 FROM rule_details_eval rde + INNER JOIN rule_evaluations res ON res.id = rde.rule_eval_id + WHERE res.profile_id = v_profile_id + AND rde.status = 'skipped' + ) INTO v_other_skipped; + + SELECT NOT EXISTS ( + SELECT 1 FROM rule_details_eval rde + INNER JOIN rule_evaluations res ON res.id = rde.rule_eval_id + WHERE res.profile_id = v_profile_id + ) INTO v_pending; + + CASE + -- A single rule in error state means policy is in error state + WHEN NEW.status = 'error' THEN + v_status := 'error'; + + -- No rule in error state and at least one rule in failure state + -- means policy is in error state + WHEN NEW.STATUS = 'failure' AND v_other_error THEN + v_status := 'error'; + WHEN NEW.STATUS = 'failure' THEN + v_status := 'failure'; + + -- No rule in error or failure state and at least one rule in + -- success state means policy is in success state + WHEN NEW.STATUS = 'success' AND v_other_error THEN + v_status := 'error'; + WHEN NEW.STATUS = 'success' AND v_other_failed THEN + v_status := 'failure'; + WHEN NEW.STATUS = 'success' THEN + v_status := 'success'; + + -- No rule in error, failure, or success state and at least one + -- rule in skipped state means policy is in skipped state + WHEN NEW.STATUS = 'skipped' AND v_other_error THEN + v_status := 'error'; + WHEN NEW.STATUS = 'skipped' AND v_other_failed THEN + v_status := 'failure'; + WHEN NEW.STATUS = 'skipped' AND v_other_success THEN + v_status := 'success'; + WHEN NEW.STATUS = 'skipped' THEN + v_status := 'skipped'; + + -- No rule evaluations means the policy is pending evaluation + WHEN v_pending THEN + v_status := 'pending'; + + -- This should never happen, if yes, make it visible + ELSE + v_status := 'error'; + RAISE WARNING 'default case should not happen'; + END CASE; + + -- This turned out to be very useful during debugging + -- RAISE LOG '% % % % % % % => %', + -- v_other_error, + -- v_other_failed, + -- v_other_success, + -- v_other_skipped, + -- v_pending, + -- OLD.status, + -- NEW.status, + -- v_status; + + UPDATE profile_status + SET profile_status = v_status, last_updated = NOW() + WHERE profile_id = v_profile_id; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- Reinstate the delete function as defined in migration script #7 +CREATE OR REPLACE FUNCTION update_profile_status_on_delete() RETURNS TRIGGER AS $$ +DECLARE + v_status eval_status_types; +BEGIN + SELECT CASE + WHEN EXISTS ( + SELECT 1 FROM rule_details_eval rde + INNER JOIN rule_evaluations res ON res.id = rde.rule_eval_id + WHERE profile_id = OLD.profile_id AND status = 'error' + ) THEN 'error' + WHEN EXISTS ( + SELECT 1 FROM rule_details_eval rde + INNER JOIN rule_evaluations res ON res.id = rde.rule_eval_id + WHERE profile_id = OLD.profile_id AND status = 'failure' + ) THEN 'failure' + WHEN NOT EXISTS ( + SELECT 1 FROM rule_details_eval rde + INNER JOIN rule_evaluations res ON res.id = rde.rule_eval_id + WHERE profile_id = OLD.profile_id + ) THEN 'pending' + WHEN NOT EXISTS ( + SELECT 1 FROM rule_details_eval rde + INNER JOIN rule_evaluations res ON res.id = rde.rule_eval_id + WHERE profile_id = OLD.profile_id AND status != 'skipped' + ) THEN 'skipped' + WHEN NOT EXISTS ( + SELECT 1 FROM rule_details_eval rde + INNER JOIN rule_evaluations res ON res.id = rde.rule_eval_id + WHERE profile_id = OLD.profile_id AND status NOT IN ('success', 'skipped') + ) THEN 'success' + ELSE ( + 'error' -- This should never happen, if yes, make it visible + ) + END INTO v_status; + + UPDATE profile_status SET profile_status = v_status, last_updated = NOW() + WHERE profile_id = OLD.profile_id; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- recreate triggers for evaluation_statuses +CREATE TRIGGER update_profile_status + AFTER INSERT OR UPDATE ON rule_details_eval + FOR EACH ROW +EXECUTE PROCEDURE update_profile_status(); + +CREATE TRIGGER update_profile_status_after_delete + AFTER DELETE ON rule_evaluations + FOR EACH ROW +EXECUTE FUNCTION update_profile_status_on_delete(); + +COMMIT; diff --git a/database/migrations/000093_profile_status_trigger.up.sql b/database/migrations/000093_profile_status_trigger.up.sql new file mode 100644 index 0000000000..b56eb93872 --- /dev/null +++ b/database/migrations/000093_profile_status_trigger.up.sql @@ -0,0 +1,199 @@ +-- Copyright 2024 Stacklok, Inc +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Start to make sure the function and trigger are either both added or none +BEGIN; + +-- drop the old triggers based on rule_details_eval and rule_evaluations, and replace with latest_evaluation_statuses +-- (See migrations #7 and #54) +DROP TRIGGER IF EXISTS update_profile_status_after_delete ON rule_evaluations; +DROP TRIGGER IF EXISTS update_profile_status ON rule_details_eval; + +-- Trigger function for updates +CREATE OR REPLACE FUNCTION update_profile_status() RETURNS TRIGGER AS $$ +DECLARE + v_status eval_status_types; + v_new_status eval_status_types; + v_other_error boolean; + v_other_failed boolean; + v_other_success boolean; + v_other_skipped boolean; + v_pending boolean; +BEGIN + -- Fetch the status for the latest evaluation + SELECT es.status INTO v_new_status + FROM latest_evaluation_statuses AS les + JOIN evaluation_statuses AS es ON es.id = les.evaluation_history_id + WHERE les.profile_id = NEW.profile_id + AND les.rule_entity_id = NEW.rule_entity_id; + + -- The next five statements calculate whether there are, for this + -- profile, any rules in evaluations in status 'error', 'failure', + -- 'success', and 'skipped', respectively. This allows to write the + -- subsequent CASE statement in a more compact and readable fashion. + -- + -- The consequence is that this version of the stored procedure adds + -- some load w.r.t. to previous one by unconditionally executing + -- these statements, but this should not be a problem, as all five + -- queries hit the same rows, so they'll likely hit the cache. + + -- These queries join on the latest_evaluation_statuses table to ensure that + -- we exclude historical statuses. + + SELECT EXISTS ( + SELECT 1 FROM latest_evaluation_statuses les + INNER JOIN evaluation_statuses es ON es.id = les.evaluation_history_id + WHERE les.profile_id = NEW.profile_id + AND es.status = 'error' + ) INTO v_other_error; + + SELECT EXISTS ( + SELECT 1 FROM latest_evaluation_statuses les + INNER JOIN evaluation_statuses es ON es.id = les.evaluation_history_id + WHERE les.profile_id = NEW.profile_id + AND es.status = 'failure' + ) INTO v_other_failed; + + SELECT EXISTS ( + SELECT 1 FROM latest_evaluation_statuses les + INNER JOIN evaluation_statuses es ON es.id = les.evaluation_history_id + WHERE les.profile_id = NEW.profile_id + AND es.status = 'success' + ) INTO v_other_success; + + SELECT EXISTS ( + SELECT 1 FROM latest_evaluation_statuses les + INNER JOIN evaluation_statuses es ON es.id = les.evaluation_history_id + WHERE les.profile_id = NEW.profile_id + AND es.status = 'skipped' + ) INTO v_other_skipped; + + SELECT NOT EXISTS ( + SELECT 1 FROM latest_evaluation_statuses les + INNER JOIN evaluation_statuses es ON es.id = les.evaluation_history_id + WHERE les.profile_id = NEW.profile_id + ) INTO v_pending; + + CASE + -- A single rule in error state means policy is in error state + WHEN v_new_status = 'error' THEN + v_status := 'error'; + + -- No rule in error state and at least one rule in failure state + -- means policy is in error state + WHEN v_new_status = 'failure' AND v_other_error THEN + v_status := 'error'; + WHEN v_new_status = 'failure' THEN + v_status := 'failure'; + + -- No rule in error or failure state and at least one rule in + -- success state means policy is in success state + WHEN v_new_status = 'success' AND v_other_error THEN + v_status := 'error'; + WHEN v_new_status = 'success' AND v_other_failed THEN + v_status := 'failure'; + WHEN v_new_status = 'success' THEN + v_status := 'success'; + + -- No rule in error, failure, or success state and at least one + -- rule in skipped state means policy is in skipped state + WHEN v_new_status = 'skipped' AND v_other_error THEN + v_status := 'error'; + WHEN v_new_status = 'skipped' AND v_other_failed THEN + v_status := 'failure'; + WHEN v_new_status = 'skipped' AND v_other_success THEN + v_status := 'success'; + WHEN v_new_status = 'skipped' THEN + v_status := 'skipped'; + + -- This should never happen, if yes, make it visible + ELSE + v_status := 'error'; + RAISE WARNING 'default case should not happen'; + END CASE; + + -- This turned out to be very useful during debugging + -- RAISE LOG '% % % % % % % % => %', + -- v_other_error, + -- v_other_failed, + -- v_other_success, + -- v_other_skipped, + -- v_pending, + -- NEW.evaluation_history_id, + -- NEW.profile_id, + -- v_new_status, + -- v_status; + + UPDATE profile_status + SET profile_status = v_status, last_updated = NOW() + WHERE profile_id = NEW.profile_id; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- Trigger function for deletions +CREATE OR REPLACE FUNCTION update_profile_status_on_delete() RETURNS TRIGGER AS $$ +DECLARE + v_status eval_status_types; +BEGIN + SELECT CASE + WHEN EXISTS ( + SELECT 1 FROM latest_evaluation_statuses AS les + INNER JOIN evaluation_statuses AS es ON es.id = les.evaluation_history_id + WHERE les.profile_id = OLD.profile_id AND es.status = 'error' + ) THEN 'error' + WHEN EXISTS ( + SELECT 1 FROM latest_evaluation_statuses AS les + INNER JOIN evaluation_statuses AS es ON es.id = les.evaluation_history_id + WHERE les.profile_id = OLD.profile_id AND es.status = 'failure' + ) THEN 'failure' + WHEN NOT EXISTS ( + SELECT 1 FROM latest_evaluation_statuses + WHERE profile_id = OLD.profile_id + ) THEN 'pending' + WHEN NOT EXISTS ( + SELECT 1 FROM latest_evaluation_statuses AS les + INNER JOIN evaluation_statuses AS es ON es.id = les.evaluation_history_id + WHERE les.profile_id = OLD.profile_id AND es.status != 'skipped' + ) THEN 'skipped' + WHEN NOT EXISTS ( + SELECT 1 FROM latest_evaluation_statuses AS les + INNER JOIN evaluation_statuses AS es ON es.id = les.evaluation_history_id + WHERE les.profile_id = OLD.profile_id AND es.status NOT IN ('success', 'skipped') + ) THEN 'success' + ELSE ( + 'error' -- This should never happen, if yes, make it visible + ) + END INTO v_status; + + UPDATE profile_status SET profile_status = v_status, last_updated = NOW() + WHERE profile_id = OLD.profile_id; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- recreate triggers for evaluation_statuses +CREATE TRIGGER update_profile_status + AFTER INSERT OR UPDATE ON latest_evaluation_statuses + FOR EACH ROW +EXECUTE PROCEDURE update_profile_status(); + +CREATE TRIGGER update_profile_status_after_delete + AFTER DELETE ON latest_evaluation_statuses + FOR EACH ROW +EXECUTE FUNCTION update_profile_status_on_delete(); + +COMMIT; diff --git a/internal/db/profiles_test.go b/internal/db/profiles_test.go index ee512bd3b8..afa8186306 100644 --- a/internal/db/profiles_test.go +++ b/internal/db/profiles_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "fmt" "slices" "testing" "time" @@ -74,7 +75,7 @@ func createRuleInstance(t *testing.T, profileId uuid.UUID, ruleTypeID uuid.UUID, ruleInstance, err := testQueries.UpsertRuleInstance(context.Background(), UpsertRuleInstanceParams{ ProfileID: profileId, RuleTypeID: ruleTypeID, - Name: "rule_instance1", + Name: fmt.Sprintf("rule_instance-%s", ruleTypeID), EntityType: EntitiesRepository, Def: []byte("{}"), Params: []byte("{}"), @@ -180,6 +181,61 @@ func upsertEvalStatus( require.NoError(t, err) } +func createRuleEntity( + t *testing.T, + repoID uuid.UUID, + ruleID uuid.UUID, +) uuid.UUID { + t.Helper() + ctx := context.Background() + + id, err := testQueries.InsertEvaluationRuleEntity(ctx, + InsertEvaluationRuleEntityParams{ + RuleID: ruleID, + RepositoryID: uuid.NullUUID{ + UUID: repoID, + Valid: true, + }, + PullRequestID: uuid.NullUUID{}, + ArtifactID: uuid.NullUUID{}, + EntityType: EntitiesRepository, + }, + ) + require.NoError(t, err) + require.NotNil(t, id) + return id +} + +func upsertEvalHistoryStatus( + t *testing.T, + profileID uuid.UUID, + ruleEntityID uuid.UUID, + evalStatus EvalStatusTypes, + details string, +) { + t.Helper() + ctx := context.Background() + + id, err := testQueries.InsertEvaluationStatus(ctx, + InsertEvaluationStatusParams{ + RuleEntityID: ruleEntityID, + Status: evalStatus, + Details: details, + Checkpoint: []byte("{}"), + }, + ) + require.NoError(t, err) + + err = testQueries.UpsertLatestEvaluationStatus(ctx, + UpsertLatestEvaluationStatusParams{ + RuleEntityID: ruleEntityID, + EvaluationHistoryID: id, + ProfileID: profileID, + }, + ) + require.NoError(t, err) +} + func upsertRemediationStatus( t *testing.T, profileID uuid.UUID, repoID uuid.UUID, ruleTypeID uuid.UUID, remStatus RemediationStatusTypes, details string, metadata json.RawMessage, @@ -639,29 +695,28 @@ func TestCreateProfileStatusSingleRuleTransitions(t *testing.T) { ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) require.NotEmpty(t, profile) - upsertEvalStatus( + ruleEntityID := createRuleEntity(t, randomEntities.repo.ID, ruleID) + + upsertEvalHistoryStatus( t, profile.ID, - randomEntities.repo.ID, - randomEntities.ruleType1.ID, - ruleID, + ruleEntityID, tt.rule1StatusPre, "foo", ) + prfStatusRow := profileIDStatusByIdAndProject(t, profile.ID, randomEntities.proj.ID) require.Equal(t, tt.expectedStatusAfterSetup, prfStatusRow.ProfileStatus, "Status BEFORE transition is %s, expected %s", prfStatusRow.ProfileStatus, tt.expectedStatusAfterSetup, ) - upsertEvalStatus( + upsertEvalHistoryStatus( t, profile.ID, - randomEntities.repo.ID, - randomEntities.ruleType1.ID, - ruleID, + ruleEntityID, tt.rule1StatusPost, - "bar", + "foo", ) prfStatusRow = profileIDStatusByIdAndProject(t, profile.ID, randomEntities.proj.ID) require.Equal(t, tt.expectedStatusAfterModify, prfStatusRow.ProfileStatus, @@ -3009,21 +3064,20 @@ func TestCreateProfileStatusMultiRuleTransitions(t *testing.T) { ruleID2 := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) require.NotEmpty(t, profile) - upsertEvalStatus( + ruleEntityID1 := createRuleEntity(t, randomEntities.repo.ID, ruleID1) + ruleEntityID2 := createRuleEntity(t, randomEntities.repo.ID, ruleID2) + + upsertEvalHistoryStatus( t, profile.ID, - randomEntities.repo.ID, - randomEntities.ruleType1.ID, - ruleID1, + ruleEntityID1, tt.rule1StatusPre, "foo", ) - upsertEvalStatus( + upsertEvalHistoryStatus( t, profile.ID, - randomEntities.repo.ID, - randomEntities.ruleType2.ID, - ruleID2, + ruleEntityID2, tt.rule2StatusPre, "foo", ) @@ -3033,24 +3087,21 @@ func TestCreateProfileStatusMultiRuleTransitions(t *testing.T) { prfStatusRow.ProfileStatus, tt.expectedStatusAfterSetup, ) - upsertEvalStatus( + upsertEvalHistoryStatus( t, profile.ID, - randomEntities.repo.ID, - randomEntities.ruleType1.ID, - ruleID1, + ruleEntityID1, tt.rule1StatusPost, - "bar", + "foo", ) - upsertEvalStatus( + upsertEvalHistoryStatus( t, profile.ID, - randomEntities.repo.ID, - randomEntities.ruleType2.ID, - ruleID2, + ruleEntityID2, tt.rule2StatusPost, - "bar", + "foo", ) + prfStatusRow = profileIDStatusByIdAndProject(t, profile.ID, randomEntities.proj.ID) require.Equal(t, tt.expectedStatusAfterModify, prfStatusRow.ProfileStatus, "Status AFTER transition is %s, expected %s", @@ -3071,203 +3122,265 @@ func TestCreateProfileStatusStoredProcedure(t *testing.T) { tests := []struct { name string - ruleStatusSetupFn func(profile Profile, randomEntities *testRandomEntities) + ruleStatusSetupFn func(profile Profile, ruleEntityID1 uuid.UUID, ruleEntityID2 uuid.UUID) expectedStatusAfterSetup EvalStatusTypes - ruleStatusModifyFn func(profile Profile, randomEntities *testRandomEntities) + ruleStatusModifyFn func(profile Profile, ruleEntityID1 uuid.UUID, ruleEntityID2 uuid.UUID) expectedStatusAfterModify EvalStatusTypes }{ { name: "Profile with no rule evaluations, should be pending", - ruleStatusSetupFn: func(_ Profile, _ *testRandomEntities) { + ruleStatusSetupFn: func(_ Profile, _ uuid.UUID, _ uuid.UUID) { // noop }, expectedStatusAfterSetup: EvalStatusTypesPending, - ruleStatusModifyFn: func(_ Profile, _ *testRandomEntities) { + ruleStatusModifyFn: func(_ Profile, _ uuid.UUID, _ uuid.UUID) { // noop }, expectedStatusAfterModify: EvalStatusTypesPending, }, { name: "Profile with only success rule evaluation, should be success", - ruleStatusSetupFn: func(_ Profile, _ *testRandomEntities) { + ruleStatusSetupFn: func(_ Profile, _ uuid.UUID, _ uuid.UUID) { // noop }, expectedStatusAfterSetup: EvalStatusTypesPending, - ruleStatusModifyFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesSuccess, "") + ruleStatusModifyFn: func(profile Profile, ruleEntityID uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesSuccess, + "", + ) }, expectedStatusAfterModify: EvalStatusTypesSuccess, }, { name: "Profile with all skipped evaluations should be skipped", - ruleStatusSetupFn: func(_ Profile, _ *testRandomEntities) { + ruleStatusSetupFn: func(_ Profile, _ uuid.UUID, _ uuid.UUID) { // noop }, expectedStatusAfterSetup: EvalStatusTypesPending, - ruleStatusModifyFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID1 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - ruleID2 := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID1, - EvalStatusTypesSkipped, "") - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID2, - EvalStatusTypesSkipped, "") + ruleStatusModifyFn: func(profile Profile, ruleEntityID1 uuid.UUID, ruleEntityID2 uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID1, + EvalStatusTypesSkipped, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID2, + EvalStatusTypesSkipped, + "", + ) }, expectedStatusAfterModify: EvalStatusTypesSkipped, }, { name: "Profile with one success and failure rule evaluation, should be failure", - ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesSuccess, "") + ruleStatusSetupFn: func(profile Profile, ruleEntityID uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesSuccess, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesSuccess, - ruleStatusModifyFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID, - EvalStatusTypesFailure, "") + ruleStatusModifyFn: func(profile Profile, ruleEntityID uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesFailure, + "", + ) }, expectedStatusAfterModify: EvalStatusTypesFailure, }, { name: "Profile with one success and one error results in error", - ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesSuccess, "") + ruleStatusSetupFn: func(profile Profile, ruleEntityID uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesSuccess, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesSuccess, - ruleStatusModifyFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID, - EvalStatusTypesError, "") + ruleStatusModifyFn: func(profile Profile, _ uuid.UUID, ruleEntityID uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesError, + "", + ) }, expectedStatusAfterModify: EvalStatusTypesError, }, { name: "Profile with one failure and one error results in error", - ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesFailure, "") + ruleStatusSetupFn: func(profile Profile, ruleEntityID uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesFailure, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesFailure, - ruleStatusModifyFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID, - EvalStatusTypesError, "") + ruleStatusModifyFn: func(profile Profile, _ uuid.UUID, ruleEntityID uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesError, + "", + ) }, expectedStatusAfterModify: EvalStatusTypesError, }, { name: "Inserting success in addition to failure should result in failure", - ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesFailure, "") + ruleStatusSetupFn: func(profile Profile, ruleEntityID uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesFailure, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesFailure, - ruleStatusModifyFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID, - EvalStatusTypesSuccess, "") + ruleStatusModifyFn: func(profile Profile, _ uuid.UUID, ruleEntityID uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesSuccess, + "", + ) }, expectedStatusAfterModify: EvalStatusTypesFailure, }, { name: "Overwriting all to success results in success", - ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID1 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - ruleID2 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID1, - EvalStatusTypesFailure, "") - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID2, - EvalStatusTypesSuccess, "") + ruleStatusSetupFn: func(profile Profile, ruleEntityID1 uuid.UUID, ruleEntityID2 uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID1, + EvalStatusTypesFailure, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID2, + EvalStatusTypesSuccess, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesFailure, - ruleStatusModifyFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesSuccess, "") + ruleStatusModifyFn: func(profile Profile, ruleEntityID1 uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID1, + EvalStatusTypesSuccess, + "", + ) }, expectedStatusAfterModify: EvalStatusTypesSuccess, }, { name: "Overwriting one to failure results in failure", - ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID1 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - ruleID2 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID1, - EvalStatusTypesSuccess, "") - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID2, - EvalStatusTypesSuccess, "") + ruleStatusSetupFn: func(profile Profile, ruleEntityID1 uuid.UUID, ruleEntityID2 uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID1, + EvalStatusTypesSuccess, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID2, + EvalStatusTypesSuccess, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesSuccess, - ruleStatusModifyFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesFailure, "") + ruleStatusModifyFn: func(profile Profile, ruleEntityID1 uuid.UUID, ruleEntityID2 uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID1, + EvalStatusTypesFailure, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID2, + EvalStatusTypesFailure, + "", + ) }, expectedStatusAfterModify: EvalStatusTypesFailure, }, { name: "Skipped then failure results in failure", - ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesSkipped, "") + ruleStatusSetupFn: func(profile Profile, ruleEntityID uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesSkipped, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesSkipped, - ruleStatusModifyFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID, - EvalStatusTypesFailure, "") + ruleStatusModifyFn: func(profile Profile, ruleEntityID uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesFailure, + "", + ) }, expectedStatusAfterModify: EvalStatusTypesFailure, }, { name: "Skipped then success results in success", - ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesSkipped, "") + ruleStatusSetupFn: func(profile Profile, ruleEntityID uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesSkipped, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesSkipped, - ruleStatusModifyFn: func(profile Profile, randomEntities *testRandomEntities) { - ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID, - EvalStatusTypesSuccess, "") + ruleStatusModifyFn: func(profile Profile, ruleEntityID uuid.UUID, _ uuid.UUID) { + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID, + EvalStatusTypesSuccess, + "", + ) }, expectedStatusAfterModify: EvalStatusTypesSuccess, }, @@ -3284,11 +3397,18 @@ func TestCreateProfileStatusStoredProcedure(t *testing.T) { profile := createRandomProfile(t, randomEntities.proj.ID, []string{}) require.NotEmpty(t, profile) - tt.ruleStatusSetupFn(profile, randomEntities) + ruleID1 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) + ruleID2 := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) + require.NotEmpty(t, profile) + + ruleEntityID1 := createRuleEntity(t, randomEntities.repo.ID, ruleID1) + ruleEntityID2 := createRuleEntity(t, randomEntities.repo.ID, ruleID2) + + tt.ruleStatusSetupFn(profile, ruleEntityID1, ruleEntityID2) prfStatusRow := profileIDStatusByIdAndProject(t, profile.ID, randomEntities.proj.ID) require.Equal(t, tt.expectedStatusAfterSetup, prfStatusRow.ProfileStatus) - tt.ruleStatusModifyFn(profile, randomEntities) + tt.ruleStatusModifyFn(profile, ruleEntityID1, ruleEntityID2) prfStatusRow = profileIDStatusByIdAndProject(t, profile.ID, randomEntities.proj.ID) require.Equal(t, tt.expectedStatusAfterModify, prfStatusRow.ProfileStatus) @@ -3316,20 +3436,42 @@ func TestCreateProfileStatusStoredDeleteProcedure(t *testing.T) { ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities, delRepo *Repository) { ruleID1 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) ruleID2 := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID1, - EvalStatusTypesSuccess, "") - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID2, - EvalStatusTypesSuccess, "") - - upsertEvalStatus( - t, profile.ID, delRepo.ID, randomEntities.ruleType1.ID, ruleID1, - EvalStatusTypesFailure, "") - upsertEvalStatus( - t, profile.ID, delRepo.ID, randomEntities.ruleType2.ID, ruleID2, - EvalStatusTypesSuccess, "") + require.NotEmpty(t, profile) + + ruleEntityID1 := createRuleEntity(t, randomEntities.repo.ID, ruleID1) + ruleEntityID2 := createRuleEntity(t, randomEntities.repo.ID, ruleID2) + ruleEntityID3 := createRuleEntity(t, delRepo.ID, ruleID1) + ruleEntityID4 := createRuleEntity(t, delRepo.ID, ruleID2) + + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID1, + EvalStatusTypesSuccess, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID2, + EvalStatusTypesSuccess, + "", + ) + + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID3, + EvalStatusTypesFailure, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID4, + EvalStatusTypesSuccess, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesFailure, ruleStatusDeleteFn: func(delRepo *Repository) { @@ -3342,21 +3484,43 @@ func TestCreateProfileStatusStoredDeleteProcedure(t *testing.T) { name: "Removing last error results in failure", ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities, delRepo *Repository) { ruleID1 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - ruleID2 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID1, - EvalStatusTypesFailure, "") - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID2, - EvalStatusTypesSuccess, "") - - upsertEvalStatus( - t, profile.ID, delRepo.ID, randomEntities.ruleType1.ID, ruleID1, - EvalStatusTypesSuccess, "") - upsertEvalStatus( - t, profile.ID, delRepo.ID, randomEntities.ruleType2.ID, ruleID2, - EvalStatusTypesError, "") + ruleID2 := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) + require.NotEmpty(t, profile) + + ruleEntityID1 := createRuleEntity(t, randomEntities.repo.ID, ruleID1) + ruleEntityID2 := createRuleEntity(t, randomEntities.repo.ID, ruleID2) + ruleEntityID3 := createRuleEntity(t, delRepo.ID, ruleID1) + ruleEntityID4 := createRuleEntity(t, delRepo.ID, ruleID2) + + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID1, + EvalStatusTypesFailure, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID2, + EvalStatusTypesSuccess, + "", + ) + + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID3, + EvalStatusTypesSuccess, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID4, + EvalStatusTypesError, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesError, @@ -3370,21 +3534,43 @@ func TestCreateProfileStatusStoredDeleteProcedure(t *testing.T) { name: "Removing one error retains the other one", ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities, delRepo *Repository) { ruleID1 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - ruleID2 := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID1, - EvalStatusTypesFailure, "") - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType2.ID, ruleID2, - EvalStatusTypesError, "") - - upsertEvalStatus( - t, profile.ID, delRepo.ID, randomEntities.ruleType1.ID, ruleID1, - EvalStatusTypesSuccess, "") - upsertEvalStatus( - t, profile.ID, delRepo.ID, randomEntities.ruleType2.ID, ruleID2, - EvalStatusTypesError, "") + ruleID2 := createRuleInstance(t, profile.ID, randomEntities.ruleType2.ID, profile.ProjectID) + require.NotEmpty(t, profile) + + ruleEntityID1 := createRuleEntity(t, randomEntities.repo.ID, ruleID1) + ruleEntityID2 := createRuleEntity(t, randomEntities.repo.ID, ruleID2) + ruleEntityID3 := createRuleEntity(t, delRepo.ID, ruleID1) + ruleEntityID4 := createRuleEntity(t, delRepo.ID, ruleID2) + + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID1, + EvalStatusTypesFailure, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID2, + EvalStatusTypesError, + "", + ) + + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID3, + EvalStatusTypesSuccess, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID4, + EvalStatusTypesError, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesError, @@ -3399,13 +3585,23 @@ func TestCreateProfileStatusStoredDeleteProcedure(t *testing.T) { ruleStatusSetupFn: func(profile Profile, randomEntities *testRandomEntities, delRepo *Repository) { ruleID := createRuleInstance(t, profile.ID, randomEntities.ruleType1.ID, profile.ProjectID) - upsertEvalStatus( - t, profile.ID, randomEntities.repo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesSkipped, "") - - upsertEvalStatus( - t, profile.ID, delRepo.ID, randomEntities.ruleType1.ID, ruleID, - EvalStatusTypesFailure, "") + ruleEntityID1 := createRuleEntity(t, randomEntities.repo.ID, ruleID) + ruleEntityID2 := createRuleEntity(t, delRepo.ID, ruleID) + + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID1, + EvalStatusTypesSkipped, + "", + ) + upsertEvalHistoryStatus( + t, + profile.ID, + ruleEntityID2, + EvalStatusTypesFailure, + "", + ) }, expectedStatusAfterSetup: EvalStatusTypesFailure,