From 2b08dea6003dd2f68480d3d0a20672d57b556f8b Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Thu, 19 Mar 2026 12:11:03 +0100 Subject: [PATCH] Fix measure count queries missing category column CountByRiskID and CountByControlID CTEs did not include m.category in their SELECT lists, causing "column category does not exist" errors when MeasureFilter applied category constraints. Added m.category to both CTEs. Also add comprehensive e2e tests for measure filtering by category at organization, risk, and control levels. Signed-off-by: Bryan Frimin --- e2e/console/measure_test.go | 206 ++++++++++++++++++++++++++++++++++++ pkg/coredata/measure.go | 6 +- 2 files changed, 210 insertions(+), 2 deletions(-) diff --git a/e2e/console/measure_test.go b/e2e/console/measure_test.go index f49a18b7a..247349627 100644 --- a/e2e/console/measure_test.go +++ b/e2e/console/measure_test.go @@ -1754,6 +1754,212 @@ func TestMeasure_Filtering(t *testing.T) { }) } +func TestMeasure_FilterByCategory(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + + // Create measures with different categories + policyID := factory.NewMeasure(owner).WithName("Category Policy Measure").WithCategory("POLICY").Create() + factory.NewMeasure(owner).WithName("Category Technical Measure").WithCategory("TECHNICAL").Create() + + t.Run("filter by category on organization", func(t *testing.T) { + t.Parallel() + + const query = ` + query($id: ID!, $filter: MeasureFilter) { + node(id: $id) { + ... on Organization { + measures(first: 100, filter: $filter) { + edges { + node { + id + category + } + } + totalCount + } + } + } + } + ` + + var result struct { + Node struct { + Measures struct { + Edges []struct { + Node struct { + ID string `json:"id"` + Category string `json:"category"` + } `json:"node"` + } `json:"edges"` + TotalCount int `json:"totalCount"` + } `json:"measures"` + } `json:"node"` + } + + err := owner.Execute(query, map[string]any{ + "id": owner.GetOrganizationID().String(), + "filter": map[string]any{"category": "POLICY"}, + }, &result) + require.NoError(t, err) + + assert.GreaterOrEqual(t, result.Node.Measures.TotalCount, 1) + for _, edge := range result.Node.Measures.Edges { + assert.Equal(t, "POLICY", edge.Node.Category) + } + + found := false + for _, edge := range result.Node.Measures.Edges { + if edge.Node.ID == policyID { + found = true + break + } + } + assert.True(t, found, "Expected to find POLICY measure in filtered results") + }) + + t.Run("filter by category on risk", func(t *testing.T) { + t.Parallel() + + riskID := factory.NewRisk(owner).WithName("Category Filter Risk").Create() + + policyMeasureID := factory.NewMeasure(owner).WithName("Risk Policy Measure").WithCategory("POLICY").Create() + techMeasureID := factory.NewMeasure(owner).WithName("Risk Technical Measure").WithCategory("TECHNICAL").Create() + + // Link both measures to the risk + const linkQuery = ` + mutation($input: CreateRiskMeasureMappingInput!) { + createRiskMeasureMapping(input: $input) { + riskEdge { node { id } } + } + } + ` + for _, mID := range []string{policyMeasureID, techMeasureID} { + _, err := owner.Do(linkQuery, map[string]any{ + "input": map[string]any{ + "riskId": riskID, + "measureId": mID, + }, + }) + require.NoError(t, err) + } + + var err error + + const query = ` + query($id: ID!, $filter: MeasureFilter) { + node(id: $id) { + ... on Risk { + measures(first: 100, filter: $filter) { + edges { + node { + id + category + } + } + totalCount + } + } + } + } + ` + + var result struct { + Node struct { + Measures struct { + Edges []struct { + Node struct { + ID string `json:"id"` + Category string `json:"category"` + } `json:"node"` + } `json:"edges"` + TotalCount int `json:"totalCount"` + } `json:"measures"` + } `json:"node"` + } + + err = owner.Execute(query, map[string]any{ + "id": riskID, + "filter": map[string]any{"category": "POLICY"}, + }, &result) + require.NoError(t, err) + + assert.Equal(t, 1, result.Node.Measures.TotalCount) + assert.Equal(t, policyMeasureID, result.Node.Measures.Edges[0].Node.ID) + assert.Equal(t, "POLICY", result.Node.Measures.Edges[0].Node.Category) + }) + + t.Run("filter by category on control", func(t *testing.T) { + t.Parallel() + + frameworkID := factory.NewFramework(owner).WithName("Category Filter Framework").Create() + controlID := factory.NewControl(owner, frameworkID).WithName("Category Filter Control").Create() + + policyMeasureID := factory.NewMeasure(owner).WithName("Control Policy Measure").WithCategory("POLICY").Create() + techMeasureID := factory.NewMeasure(owner).WithName("Control Technical Measure").WithCategory("TECHNICAL").Create() + + // Link both measures to the control + const linkQuery = ` + mutation($input: CreateControlMeasureMappingInput!) { + createControlMeasureMapping(input: $input) { + controlEdge { node { id } } + } + } + ` + for _, mID := range []string{policyMeasureID, techMeasureID} { + _, err := owner.Do(linkQuery, map[string]any{ + "input": map[string]any{ + "controlId": controlID, + "measureId": mID, + }, + }) + require.NoError(t, err) + } + + const query = ` + query($id: ID!, $filter: MeasureFilter) { + node(id: $id) { + ... on Control { + measures(first: 100, filter: $filter) { + edges { + node { + id + category + } + } + totalCount + } + } + } + } + ` + + var result struct { + Node struct { + Measures struct { + Edges []struct { + Node struct { + ID string `json:"id"` + Category string `json:"category"` + } `json:"node"` + } `json:"edges"` + TotalCount int `json:"totalCount"` + } `json:"measures"` + } `json:"node"` + } + + err := owner.Execute(query, map[string]any{ + "id": controlID, + "filter": map[string]any{"category": "POLICY"}, + }, &result) + require.NoError(t, err) + + assert.Equal(t, 1, result.Node.Measures.TotalCount) + assert.Equal(t, policyMeasureID, result.Node.Measures.Edges[0].Node.ID) + assert.Equal(t, "POLICY", result.Node.Measures.Edges[0].Node.Category) + }) +} + func TestMeasure_TenantIsolation(t *testing.T) { t.Parallel() diff --git a/pkg/coredata/measure.go b/pkg/coredata/measure.go index bc7360787..c641d3f1c 100644 --- a/pkg/coredata/measure.go +++ b/pkg/coredata/measure.go @@ -84,7 +84,8 @@ WITH msrs AS ( m.id, m.tenant_id, m.search_vector, - m.state + m.state, + m.category FROM measures m INNER JOIN @@ -195,7 +196,8 @@ WITH mtgtns AS ( m.id, m.tenant_id, m.search_vector, - m.state + m.state, + m.category FROM measures m INNER JOIN