Files
probo/e2e/console/measure_test.go
Bryan Frimin 2b08dea600 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 <bryan@getprobo.com>
2026-03-19 12:25:34 +01:00

2229 lines
54 KiB
Go

// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package console_test
import (
"fmt"
"maps"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/e2e/internal/factory"
"go.probo.inc/probo/e2e/internal/testutil"
)
func TestMeasure_Create(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
tests := []struct {
name string
input map[string]any
wantError bool
assertField string
assertValue string
}{
{
name: "with full details",
input: map[string]any{
"name": "Owner Measure",
"description": "Created by owner",
"category": "POLICY",
},
assertField: "name",
assertValue: "Owner Measure",
},
{
name: "with POLICY category",
input: map[string]any{
"name": "Policy measure",
"category": "POLICY",
},
assertField: "category",
assertValue: "POLICY",
},
{
name: "with PROCEDURE category",
input: map[string]any{
"name": "Procedure measure",
"category": "PROCEDURE",
},
assertField: "category",
assertValue: "PROCEDURE",
},
{
name: "with TECHNICAL category",
input: map[string]any{
"name": "Technical measure",
"category": "TECHNICAL",
},
assertField: "category",
assertValue: "TECHNICAL",
},
{
name: "with EVIDENCE category",
input: map[string]any{
"name": "Evidence measure",
"category": "EVIDENCE",
},
assertField: "category",
assertValue: "EVIDENCE",
},
{
name: "with TRAINING category",
input: map[string]any{
"name": "Training measure",
"category": "TRAINING",
},
assertField: "category",
assertValue: "TRAINING",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := `
mutation CreateMeasure($input: CreateMeasureInput!) {
createMeasure(input: $input) {
measureEdge {
node {
id
name
category
}
}
}
}
`
input := map[string]any{
"organizationId": owner.GetOrganizationID().String(),
}
maps.Copy(input, tt.input)
var result struct {
CreateMeasure struct {
MeasureEdge struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
} `json:"node"`
} `json:"measureEdge"`
} `json:"createMeasure"`
}
err := owner.Execute(query, map[string]any{"input": input}, &result)
require.NoError(t, err)
node := result.CreateMeasure.MeasureEdge.Node
assert.NotEmpty(t, node.ID)
switch tt.assertField {
case "name":
assert.Equal(t, tt.assertValue, node.Name)
case "category":
assert.Equal(t, tt.assertValue, node.Category)
}
})
}
}
func TestMeasure_Create_Validation(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
tests := []struct {
name string
input map[string]any
skipOrganization bool
wantErrorContains string
}{
// Required field validation
{
name: "missing name",
input: map[string]any{
"category": "POLICY",
},
wantErrorContains: "name",
},
{
name: "missing category",
input: map[string]any{
"name": "Test Measure",
},
wantErrorContains: "category",
},
{
name: "empty name",
input: map[string]any{
"name": "",
"category": "POLICY",
},
wantErrorContains: "name",
},
{
name: "empty category",
input: map[string]any{
"name": "Test Measure",
"category": "",
},
wantErrorContains: "category",
},
{
name: "missing organizationId",
input: map[string]any{
"name": "Test Measure",
"category": "POLICY",
},
skipOrganization: true,
wantErrorContains: "organizationId",
},
// HTML injection validation
{
name: "name with HTML tags",
input: map[string]any{
"name": "<script>alert('xss')</script>",
"category": "POLICY",
},
wantErrorContains: "HTML",
},
{
name: "description with HTML tags",
input: map[string]any{
"name": "Test Measure",
"category": "POLICY",
"description": "<div>HTML content</div>",
},
wantErrorContains: "HTML",
},
{
name: "category with HTML tags",
input: map[string]any{
"name": "Test Measure",
"category": "<b>POLICY</b>",
},
wantErrorContains: "HTML",
},
// Newline validation (name should not allow newlines)
{
name: "name with newline",
input: map[string]any{
"name": "Test\nMeasure",
"category": "POLICY",
},
wantErrorContains: "newline",
},
{
name: "name with carriage return",
input: map[string]any{
"name": "Test\rMeasure",
"category": "POLICY",
},
wantErrorContains: "carriage return",
},
// Control character validation
{
name: "name with null byte",
input: map[string]any{
"name": "Test\x00Measure",
"category": "POLICY",
},
wantErrorContains: "control character",
},
{
name: "name with tab character",
input: map[string]any{
"name": "Test\tMeasure",
"category": "POLICY",
},
wantErrorContains: "control character",
},
// Zero-width character validation
{
name: "name with zero-width space",
input: map[string]any{
"name": "Test\u200BMeasure",
"category": "POLICY",
},
wantErrorContains: "zero-width",
},
{
name: "name with zero-width joiner",
input: map[string]any{
"name": "Test\u200DMeasure",
"category": "POLICY",
},
wantErrorContains: "zero-width",
},
// Bidirectional override validation
{
name: "name with right-to-left override",
input: map[string]any{
"name": "Test\u202EMeasure",
"category": "POLICY",
},
wantErrorContains: "bidirectional",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := `
mutation CreateMeasure($input: CreateMeasureInput!) {
createMeasure(input: $input) {
measureEdge {
node {
id
}
}
}
}
`
input := make(map[string]any)
if !tt.skipOrganization {
input["organizationId"] = owner.GetOrganizationID().String()
}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErrorContains)
})
}
}
func TestMeasure_Update(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
tests := []struct {
name string
setup func() string
input func(id string) map[string]any
assertField string
assertValue string
}{
{
name: "update name and description",
setup: func() string {
return factory.NewMeasure(owner).
WithName("Measure to Update").
WithDescription("Original description").
Create()
},
input: func(id string) map[string]any {
return map[string]any{
"id": id,
"name": "Updated by Owner",
"description": "Owner updated this",
}
},
assertField: "name",
assertValue: "Updated by Owner",
},
{
name: "update to NOT_STARTED state",
setup: func() string {
return factory.NewMeasure(owner).WithName("State Test").Create()
},
input: func(id string) map[string]any {
return map[string]any{"id": id, "state": "NOT_STARTED"}
},
assertField: "state",
assertValue: "NOT_STARTED",
},
{
name: "update to IMPLEMENTED state",
setup: func() string {
return factory.NewMeasure(owner).WithName("State Test").Create()
},
input: func(id string) map[string]any {
return map[string]any{"id": id, "state": "IMPLEMENTED"}
},
assertField: "state",
assertValue: "IMPLEMENTED",
},
{
name: "update to NOT_APPLICABLE state",
setup: func() string {
return factory.NewMeasure(owner).WithName("State Test").Create()
},
input: func(id string) map[string]any {
return map[string]any{"id": id, "state": "NOT_APPLICABLE"}
},
assertField: "state",
assertValue: "NOT_APPLICABLE",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
measureID := tt.setup()
query := `
mutation UpdateMeasure($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure {
id
name
state
}
}
}
`
var result struct {
UpdateMeasure struct {
Measure struct {
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
} `json:"measure"`
} `json:"updateMeasure"`
}
err := owner.Execute(query, map[string]any{"input": tt.input(measureID)}, &result)
require.NoError(t, err)
measure := result.UpdateMeasure.Measure
switch tt.assertField {
case "name":
assert.Equal(t, tt.assertValue, measure.Name)
case "state":
assert.Equal(t, tt.assertValue, measure.State)
}
})
}
}
func TestMeasure_Update_Validation(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
// Create a measure to use for most validation tests
baseMeasureID := factory.NewMeasure(owner).WithName("Validation Test Measure").Create()
tests := []struct {
name string
setup func() string
input func(id string) map[string]any
wantErrorContains string
}{
// ID validation
{
name: "invalid ID format",
setup: func() string { return "invalid-id-format" },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test"}
},
wantErrorContains: "base64",
},
// Empty field validation
{
name: "empty name",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": ""}
},
wantErrorContains: "name",
},
{
name: "empty category",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "category": ""}
},
wantErrorContains: "category",
},
// HTML injection validation
{
name: "name with HTML tags",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "<script>alert('xss')</script>"}
},
wantErrorContains: "HTML",
},
{
name: "description with HTML tags",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "description": "<img src=x onerror=alert(1)>"}
},
wantErrorContains: "HTML",
},
{
name: "category with HTML tags",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "category": "<b>POLICY</b>"}
},
wantErrorContains: "HTML",
},
// Newline validation (name should not allow newlines)
{
name: "name with newline",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test\nMeasure"}
},
wantErrorContains: "newline",
},
{
name: "name with carriage return",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test\rMeasure"}
},
wantErrorContains: "carriage return",
},
// Control character validation
{
name: "name with null byte",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test\x00Measure"}
},
wantErrorContains: "control character",
},
{
name: "name with tab character",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test\tMeasure"}
},
wantErrorContains: "control character",
},
{
name: "description with null byte",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "description": "Test\x00Description"}
},
wantErrorContains: "control character",
},
// Zero-width character validation
{
name: "name with zero-width space",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test\u200BMeasure"}
},
wantErrorContains: "zero-width",
},
{
name: "description with zero-width joiner",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "description": "Test\u200DDescription"}
},
wantErrorContains: "zero-width",
},
// Bidirectional override validation
{
name: "name with right-to-left override",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "name": "Test\u202EMeasure"}
},
wantErrorContains: "bidirectional",
},
{
name: "description with left-to-right override",
setup: func() string { return baseMeasureID },
input: func(id string) map[string]any {
return map[string]any{"id": id, "description": "Test\u202DDescription"}
},
wantErrorContains: "bidirectional",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
measureID := tt.setup()
query := `
mutation UpdateMeasure($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure {
id
}
}
}
`
_, err := owner.Do(query, map[string]any{"input": tt.input(measureID)})
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErrorContains)
})
}
}
func TestMeasure_Delete(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
t.Run("delete existing measure", func(t *testing.T) {
measureID := factory.NewMeasure(owner).WithName("Measure to Delete").Create()
query := `
mutation DeleteMeasure($input: DeleteMeasureInput!) {
deleteMeasure(input: $input) {
deletedMeasureId
}
}
`
var result struct {
DeleteMeasure struct {
DeletedMeasureID string `json:"deletedMeasureId"`
} `json:"deleteMeasure"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{"measureId": measureID},
}, &result)
require.NoError(t, err)
assert.Equal(t, measureID, result.DeleteMeasure.DeletedMeasureID)
})
}
func TestMeasure_Delete_Validation(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
tests := []struct {
name string
measureID string
wantErrorContains string
}{
{
name: "invalid ID format",
measureID: "invalid-id-format",
wantErrorContains: "base64",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := `
mutation DeleteMeasure($input: DeleteMeasureInput!) {
deleteMeasure(input: $input) {
deletedMeasureId
}
}
`
_, err := owner.Do(query, map[string]any{
"input": map[string]any{"measureId": tt.measureID},
})
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErrorContains)
})
}
}
func TestMeasure_List(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
measureNames := []string{"Measure A", "Measure B", "Measure C"}
for _, name := range measureNames {
factory.NewMeasure(owner).WithName(name).Create()
}
query := `
query GetMeasures($id: ID!) {
node(id: $id) {
... on Organization {
measures(first: 10) {
edges {
node {
id
name
}
}
totalCount
}
}
}
}
`
var result struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
TotalCount int `json:"totalCount"`
} `json:"measures"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{
"id": owner.GetOrganizationID().String(),
}, &result)
require.NoError(t, err)
assert.GreaterOrEqual(t, result.Node.Measures.TotalCount, 3)
}
func TestMeasure_Query(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
t.Run("query with non-existent ID returns error", func(t *testing.T) {
query := `
query($id: ID!) {
node(id: $id) {
... on Measure {
id
name
}
}
}
`
err := owner.ExecuteShouldFail(query, map[string]any{
"id": "V0wtM0tMNmJBQ1lBQUFBQUFackhLSTJfbXJJRUFZVXo", // Valid format but doesn't exist
})
require.Error(t, err, "Non-existent ID should return error")
})
}
func TestMeasure_Timestamps(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
t.Run("createdAt and updatedAt are set on create", func(t *testing.T) {
beforeCreate := time.Now().Add(-time.Second)
query := `
mutation CreateMeasure($input: CreateMeasureInput!) {
createMeasure(input: $input) {
measureEdge {
node {
id
createdAt
updatedAt
}
}
}
}
`
var result struct {
CreateMeasure struct {
MeasureEdge struct {
Node struct {
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"node"`
} `json:"measureEdge"`
} `json:"createMeasure"`
}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"organizationId": owner.GetOrganizationID().String(),
"name": "Timestamp Test Measure",
"category": "POLICY",
},
}, &result)
require.NoError(t, err)
node := result.CreateMeasure.MeasureEdge.Node
testutil.AssertTimestampsOnCreate(t, node.CreatedAt, node.UpdatedAt, beforeCreate)
})
t.Run("updatedAt changes on update", func(t *testing.T) {
measureID := factory.NewMeasure(owner).WithName("Timestamp Update Test").Create()
// Get initial timestamps
getQuery := `
query($id: ID!) {
node(id: $id) {
... on Measure {
createdAt
updatedAt
}
}
}
`
var getResult struct {
Node struct {
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"node"`
}
err := owner.Execute(getQuery, map[string]any{"id": measureID}, &getResult)
require.NoError(t, err)
initialCreatedAt := getResult.Node.CreatedAt
initialUpdatedAt := getResult.Node.UpdatedAt
// Wait long enough for timestamp to change (database may have second precision)
time.Sleep(1100 * time.Millisecond)
updateQuery := `
mutation UpdateMeasure($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure {
createdAt
updatedAt
}
}
}
`
var updateResult struct {
UpdateMeasure struct {
Measure struct {
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"measure"`
} `json:"updateMeasure"`
}
err = owner.Execute(updateQuery, map[string]any{
"input": map[string]any{
"id": measureID,
"name": "Updated Timestamp Test",
},
}, &updateResult)
require.NoError(t, err)
measure := updateResult.UpdateMeasure.Measure
testutil.AssertTimestampsOnUpdate(t, measure.CreatedAt, measure.UpdatedAt, initialCreatedAt, initialUpdatedAt)
})
}
func TestMeasure_OmittableDescription(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
measureID := factory.NewMeasure(owner).
WithName("Omittable Test Measure").
WithDescription("Initial description").
Create()
tests := []struct {
name string
input map[string]any
wantDescription *string
}{
{
name: "update with new description",
input: map[string]any{"description": "Updated description"},
wantDescription: new("Updated description"),
},
{
name: "update with null description clears it",
input: map[string]any{"description": nil},
wantDescription: nil,
},
{
name: "set description again",
input: map[string]any{"description": "Should persist"},
wantDescription: new("Should persist"),
},
{
name: "update without description preserves it",
input: map[string]any{"name": "Updated Name"},
wantDescription: new("Should persist"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := `
mutation($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure {
id
name
description
}
}
}
`
input := map[string]any{"id": measureID}
maps.Copy(input, tt.input)
var result struct {
UpdateMeasure struct {
Measure struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
} `json:"measure"`
} `json:"updateMeasure"`
}
err := owner.Execute(query, map[string]any{"input": input}, &result)
require.NoError(t, err)
testutil.AssertOptionalStringEqual(t, tt.wantDescription, result.UpdateMeasure.Measure.Description, "description")
})
}
}
func TestMeasure_SubResolvers(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
measureID := factory.NewMeasure(owner).
WithName("SubResolver Test Measure").
Create()
subResolvers := []struct {
name string
field string
}{
{"controls", "controls"},
{"risks", "risks"},
{"tasks", "tasks"},
{"evidences", "evidences"},
}
for _, sr := range subResolvers {
t.Run(sr.name+" sub-resolver returns empty list", func(t *testing.T) {
query := fmt.Sprintf(`
query($id: ID!) {
node(id: $id) {
... on Measure {
id
%s(first: 10) {
edges {
node {
id
}
}
}
}
}
}
`, sr.field)
resp, err := owner.Do(query, map[string]any{"id": measureID})
require.NoError(t, err)
require.NotNil(t, resp)
})
}
}
func TestMeasure_RBAC(t *testing.T) {
t.Parallel()
t.Run("create", func(t *testing.T) {
t.Run("owner can create", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
_, err := owner.Do(`
mutation CreateMeasure($input: CreateMeasureInput!) {
createMeasure(input: $input) {
measureEdge { node { id } }
}
}
`, map[string]any{
"input": map[string]any{
"organizationId": owner.GetOrganizationID().String(),
"name": "RBAC Test Measure",
"category": "POLICY",
},
})
require.NoError(t, err, "owner should be able to create measure")
})
t.Run("admin can create", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner)
_, err := admin.Do(`
mutation CreateMeasure($input: CreateMeasureInput!) {
createMeasure(input: $input) {
measureEdge { node { id } }
}
}
`, map[string]any{
"input": map[string]any{
"organizationId": admin.GetOrganizationID().String(),
"name": "RBAC Test Measure",
"category": "POLICY",
},
})
require.NoError(t, err, "admin should be able to create measure")
})
t.Run("viewer cannot create", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
_, err := viewer.Do(`
mutation CreateMeasure($input: CreateMeasureInput!) {
createMeasure(input: $input) {
measureEdge { node { id } }
}
}
`, map[string]any{
"input": map[string]any{
"organizationId": viewer.GetOrganizationID().String(),
"name": "RBAC Test Measure",
"category": "POLICY",
},
})
testutil.RequireForbiddenError(t, err, "viewer should not be able to create measure")
})
})
t.Run("update", func(t *testing.T) {
t.Run("owner can update", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
measureID := factory.NewMeasure(owner).WithName("RBAC Update Test").Create()
_, err := owner.Do(`
mutation UpdateMeasure($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure { id }
}
}
`, map[string]any{
"input": map[string]any{
"id": measureID,
"name": "Updated by Owner",
},
})
require.NoError(t, err, "owner should be able to update measure")
})
t.Run("admin can update", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner)
measureID := factory.NewMeasure(owner).WithName("RBAC Update Test").Create()
_, err := admin.Do(`
mutation UpdateMeasure($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure { id }
}
}
`, map[string]any{
"input": map[string]any{
"id": measureID,
"name": "Updated by Admin",
},
})
require.NoError(t, err, "admin should be able to update measure")
})
t.Run("viewer cannot update", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
measureID := factory.NewMeasure(owner).WithName("RBAC Update Test").Create()
_, err := viewer.Do(`
mutation UpdateMeasure($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure { id }
}
}
`, map[string]any{
"input": map[string]any{
"id": measureID,
"name": "Updated by Viewer",
},
})
testutil.RequireForbiddenError(t, err, "viewer should not be able to update measure")
})
})
t.Run("delete", func(t *testing.T) {
t.Run("owner can delete", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
measureID := factory.NewMeasure(owner).WithName("RBAC Delete Test").Create()
_, err := owner.Do(`
mutation DeleteMeasure($input: DeleteMeasureInput!) {
deleteMeasure(input: $input) {
deletedMeasureId
}
}
`, map[string]any{
"input": map[string]any{"measureId": measureID},
})
require.NoError(t, err, "owner should be able to delete measure")
})
t.Run("admin can delete", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner)
measureID := factory.NewMeasure(owner).WithName("RBAC Delete Test").Create()
_, err := admin.Do(`
mutation DeleteMeasure($input: DeleteMeasureInput!) {
deleteMeasure(input: $input) {
deletedMeasureId
}
}
`, map[string]any{
"input": map[string]any{"measureId": measureID},
})
require.NoError(t, err, "admin should be able to delete measure")
})
t.Run("viewer cannot delete", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
measureID := factory.NewMeasure(owner).WithName("RBAC Delete Test").Create()
_, err := viewer.Do(`
mutation DeleteMeasure($input: DeleteMeasureInput!) {
deleteMeasure(input: $input) {
deletedMeasureId
}
}
`, map[string]any{
"input": map[string]any{"measureId": measureID},
})
testutil.RequireForbiddenError(t, err, "viewer should not be able to delete measure")
})
})
t.Run("read", func(t *testing.T) {
t.Run("owner can read", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
measureID := factory.NewMeasure(owner).WithName("RBAC Read Test").Create()
var result struct {
Node *struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
}
err := owner.Execute(`
query($id: ID!) {
node(id: $id) {
... on Measure { id name }
}
}
`, map[string]any{"id": measureID}, &result)
require.NoError(t, err, "owner should be able to read measure")
require.NotNil(t, result.Node, "owner should receive measure data")
})
t.Run("admin can read", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner)
measureID := factory.NewMeasure(owner).WithName("RBAC Read Test").Create()
var result struct {
Node *struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
}
err := admin.Execute(`
query($id: ID!) {
node(id: $id) {
... on Measure { id name }
}
}
`, map[string]any{"id": measureID}, &result)
require.NoError(t, err, "admin should be able to read measure")
require.NotNil(t, result.Node, "admin should receive measure data")
})
t.Run("viewer can read", func(t *testing.T) {
owner := testutil.NewClient(t, testutil.RoleOwner)
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
measureID := factory.NewMeasure(owner).WithName("RBAC Read Test").Create()
var result struct {
Node *struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
}
err := viewer.Execute(`
query($id: ID!) {
node(id: $id) {
... on Measure { id name }
}
}
`, map[string]any{"id": measureID}, &result)
require.NoError(t, err, "viewer should be able to read measure")
require.NotNil(t, result.Node, "viewer should receive measure data")
})
})
}
func TestMeasure_MaxLength_Validation(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
// TitleMaxLength = 1000, ContentMaxLength = 5000
longName := strings.Repeat("a", 1001)
longCategory := strings.Repeat("b", 1001)
longDescription := strings.Repeat("c", 5001)
t.Run("create", func(t *testing.T) {
tests := []struct {
name string
input map[string]any
wantErrorContains string
}{
{
name: "name exceeds max length",
input: map[string]any{
"name": longName,
"category": "POLICY",
},
wantErrorContains: "name",
},
{
name: "category exceeds max length",
input: map[string]any{
"name": "Test Measure",
"category": longCategory,
},
wantErrorContains: "category",
},
{
name: "description exceeds max length",
input: map[string]any{
"name": "Test Measure",
"category": "POLICY",
"description": longDescription,
},
wantErrorContains: "description",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := `
mutation CreateMeasure($input: CreateMeasureInput!) {
createMeasure(input: $input) {
measureEdge {
node { id }
}
}
}
`
input := map[string]any{
"organizationId": owner.GetOrganizationID().String(),
}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErrorContains)
})
}
})
t.Run("update", func(t *testing.T) {
measureID := factory.NewMeasure(owner).WithName("Max Length Test").Create()
tests := []struct {
name string
input map[string]any
wantErrorContains string
}{
{
name: "name exceeds max length",
input: map[string]any{"name": longName},
wantErrorContains: "name",
},
{
name: "category exceeds max length",
input: map[string]any{"category": longCategory},
wantErrorContains: "category",
},
{
name: "description exceeds max length",
input: map[string]any{"description": longDescription},
wantErrorContains: "description",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
query := `
mutation UpdateMeasure($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure { id }
}
}
`
input := map[string]any{"id": measureID}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErrorContains)
})
}
})
}
func TestMeasure_SubResolvers_WithData(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
t.Run("tasks sub-resolver with linked tasks", func(t *testing.T) {
measureID := factory.NewMeasure(owner).WithName("Measure with Tasks").Create()
// Create tasks linked to the measure
task1ID := factory.NewTask(owner, measureID).WithName("Task 1").Create()
task2ID := factory.NewTask(owner, measureID).WithName("Task 2").Create()
query := `
query($id: ID!) {
node(id: $id) {
... on Measure {
id
tasks(first: 10) {
edges {
node {
id
name
}
}
totalCount
}
}
}
}
`
var result struct {
Node struct {
ID string `json:"id"`
Tasks struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
TotalCount int `json:"totalCount"`
} `json:"tasks"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{"id": measureID}, &result)
require.NoError(t, err)
assert.Equal(t, 2, result.Node.Tasks.TotalCount)
taskIDs := make([]string, len(result.Node.Tasks.Edges))
for i, edge := range result.Node.Tasks.Edges {
taskIDs[i] = edge.Node.ID
}
assert.Contains(t, taskIDs, task1ID)
assert.Contains(t, taskIDs, task2ID)
})
t.Run("controls sub-resolver with linked controls", func(t *testing.T) {
measureID := factory.NewMeasure(owner).WithName("Measure with Controls").Create()
// Create framework and control
frameworkID := factory.NewFramework(owner).WithName("Test Framework").Create()
controlID := factory.NewControl(owner, frameworkID).WithName("Test Control").Create()
// Link control to measure
linkQuery := `
mutation($input: CreateControlMeasureMappingInput!) {
createControlMeasureMapping(input: $input) {
controlEdge { node { id } }
}
}
`
_, err := owner.Do(linkQuery, map[string]any{
"input": map[string]any{
"controlId": controlID,
"measureId": measureID,
},
})
require.NoError(t, err)
// Query the measure's controls
query := `
query($id: ID!) {
node(id: $id) {
... on Measure {
id
controls(first: 10) {
edges {
node {
id
name
}
}
totalCount
}
}
}
}
`
var result struct {
Node struct {
ID string `json:"id"`
Controls struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
TotalCount int `json:"totalCount"`
} `json:"controls"`
} `json:"node"`
}
err = owner.Execute(query, map[string]any{"id": measureID}, &result)
require.NoError(t, err)
assert.Equal(t, 1, result.Node.Controls.TotalCount)
assert.Equal(t, controlID, result.Node.Controls.Edges[0].Node.ID)
})
t.Run("risks sub-resolver with linked risks", func(t *testing.T) {
measureID := factory.NewMeasure(owner).WithName("Measure with Risks").Create()
// Create risk
riskID := factory.NewRisk(owner).WithName("Test Risk").Create()
// Link risk to measure
linkQuery := `
mutation($input: CreateRiskMeasureMappingInput!) {
createRiskMeasureMapping(input: $input) {
riskEdge { node { id } }
}
}
`
_, err := owner.Do(linkQuery, map[string]any{
"input": map[string]any{
"riskId": riskID,
"measureId": measureID,
},
})
require.NoError(t, err)
// Query the measure's risks
query := `
query($id: ID!) {
node(id: $id) {
... on Measure {
id
risks(first: 10) {
edges {
node {
id
name
}
}
totalCount
}
}
}
}
`
var result struct {
Node struct {
ID string `json:"id"`
Risks struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
TotalCount int `json:"totalCount"`
} `json:"risks"`
} `json:"node"`
}
err = owner.Execute(query, map[string]any{"id": measureID}, &result)
require.NoError(t, err)
assert.Equal(t, 1, result.Node.Risks.TotalCount)
assert.Equal(t, riskID, result.Node.Risks.Edges[0].Node.ID)
})
}
func TestMeasure_Pagination(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
// Create multiple measures for pagination testing
measureIDs := make([]string, 5)
for i := range 5 {
measureIDs[i] = factory.NewMeasure(owner).
WithName(fmt.Sprintf("Pagination Measure %d", i)).
Create()
}
t.Run("first/after pagination", func(t *testing.T) {
// Get first 2 measures
query := `
query($id: ID!) {
node(id: $id) {
... on Organization {
measures(first: 2) {
edges {
node { id name }
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
}
}
`
var result struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
Cursor string `json:"cursor"`
} `json:"edges"`
PageInfo testutil.PageInfo `json:"pageInfo"`
TotalCount int `json:"totalCount"`
} `json:"measures"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{
"id": owner.GetOrganizationID().String(),
}, &result)
require.NoError(t, err)
testutil.AssertFirstPage(t, len(result.Node.Measures.Edges), result.Node.Measures.PageInfo, 2, true)
assert.GreaterOrEqual(t, result.Node.Measures.TotalCount, 5)
// Get next page using cursor
testutil.AssertHasMorePages(t, result.Node.Measures.PageInfo)
queryAfter := `
query($id: ID!, $after: CursorKey) {
node(id: $id) {
... on Organization {
measures(first: 2, after: $after) {
edges {
node { id name }
}
pageInfo {
hasNextPage
hasPreviousPage
}
}
}
}
}
`
var resultAfter struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
PageInfo testutil.PageInfo `json:"pageInfo"`
} `json:"measures"`
} `json:"node"`
}
err = owner.Execute(queryAfter, map[string]any{
"id": owner.GetOrganizationID().String(),
"after": *result.Node.Measures.PageInfo.EndCursor,
}, &resultAfter)
require.NoError(t, err)
testutil.AssertMiddlePage(t, len(resultAfter.Node.Measures.Edges), resultAfter.Node.Measures.PageInfo, 2)
})
t.Run("last/before pagination", func(t *testing.T) {
query := `
query($id: ID!) {
node(id: $id) {
... on Organization {
measures(last: 2) {
edges {
node { id name }
}
pageInfo {
hasNextPage
hasPreviousPage
}
}
}
}
}
`
var result struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
PageInfo testutil.PageInfo `json:"pageInfo"`
} `json:"measures"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{
"id": owner.GetOrganizationID().String(),
}, &result)
require.NoError(t, err)
testutil.AssertLastPage(t, len(result.Node.Measures.Edges), result.Node.Measures.PageInfo, 2, true)
})
}
func TestMeasure_Filtering(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
// Create measures with different states
measure1ID := factory.NewMeasure(owner).WithName("Filter Test Implemented").Create()
measure2ID := factory.NewMeasure(owner).WithName("Filter Test Not Started").Create()
// Update measure1 to IMPLEMENTED state
updateQuery := `
mutation($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure { id state }
}
}
`
_, err := owner.Do(updateQuery, map[string]any{
"input": map[string]any{
"id": measure1ID,
"state": "IMPLEMENTED",
},
})
require.NoError(t, err)
t.Run("filter by state", func(t *testing.T) {
query := `
query($id: ID!, $filter: MeasureFilter) {
node(id: $id) {
... on Organization {
measures(first: 100, filter: $filter) {
edges {
node {
id
name
state
}
}
totalCount
}
}
}
}
`
var result struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
State string `json:"state"`
} `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{"state": "IMPLEMENTED"},
}, &result)
require.NoError(t, err)
// All returned measures should be IMPLEMENTED
for _, edge := range result.Node.Measures.Edges {
assert.Equal(t, "IMPLEMENTED", edge.Node.State)
}
// Should contain our implemented measure
found := false
for _, edge := range result.Node.Measures.Edges {
if edge.Node.ID == measure1ID {
found = true
break
}
}
assert.True(t, found, "Expected to find implemented measure in filtered results")
})
t.Run("filter by query string", func(t *testing.T) {
query := `
query($id: ID!, $filter: MeasureFilter) {
node(id: $id) {
... on Organization {
measures(first: 100, filter: $filter) {
edges {
node {
id
name
}
}
}
}
}
}
`
var result struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
} `json:"measures"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{
"id": owner.GetOrganizationID().String(),
"filter": map[string]any{"query": "Filter Test"},
}, &result)
require.NoError(t, err)
// Should find measures matching the query
foundIDs := make([]string, len(result.Node.Measures.Edges))
for i, edge := range result.Node.Measures.Edges {
foundIDs[i] = edge.Node.ID
}
assert.Contains(t, foundIDs, measure1ID)
assert.Contains(t, foundIDs, measure2ID)
})
}
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()
// Create two separate organizations with their own owners
org1Owner := testutil.NewClient(t, testutil.RoleOwner)
org2Owner := testutil.NewClient(t, testutil.RoleOwner)
// Create a measure in org1
measureID := factory.NewMeasure(org1Owner).WithName("Org1 Measure").Create()
t.Run("cannot read measure from another organization", func(t *testing.T) {
query := `
query($id: ID!) {
node(id: $id) {
... on Measure {
id
name
}
}
}
`
var result struct {
Node *struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
}
err := org2Owner.Execute(query, map[string]any{"id": measureID}, &result)
testutil.AssertNodeNotAccessible(t, err, result.Node == nil, "measure")
})
t.Run("cannot update measure from another organization", func(t *testing.T) {
query := `
mutation UpdateMeasure($input: UpdateMeasureInput!) {
updateMeasure(input: $input) {
measure { id }
}
}
`
_, err := org2Owner.Do(query, map[string]any{
"input": map[string]any{
"id": measureID,
"name": "Hijacked Measure",
},
})
require.Error(t, err, "Should not be able to update measure from another org")
})
t.Run("cannot delete measure from another organization", func(t *testing.T) {
query := `
mutation DeleteMeasure($input: DeleteMeasureInput!) {
deleteMeasure(input: $input) {
deletedMeasureId
}
}
`
_, err := org2Owner.Do(query, map[string]any{
"input": map[string]any{
"measureId": measureID,
},
})
require.Error(t, err, "Should not be able to delete measure from another org")
})
t.Run("cannot list measures from another organization", func(t *testing.T) {
query := `
query($id: ID!) {
node(id: $id) {
... on Organization {
measures(first: 100) {
edges {
node {
id
name
}
}
}
}
}
}
`
var result struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
} `json:"measures"`
} `json:"node"`
}
// Query org1's measures as org2
err := org2Owner.Execute(query, map[string]any{
"id": org1Owner.GetOrganizationID().String(),
}, &result)
// Should either error or return empty list (can't access other org's data)
if err == nil {
// If no error, the measure from org1 should not be in the list
for _, edge := range result.Node.Measures.Edges {
assert.NotEqual(t, measureID, edge.Node.ID, "Should not see measure from another org")
}
}
})
}
func TestMeasure_Ordering(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
// Create measures with distinct names for ordering
factory.NewMeasure(owner).WithName("AAA Order Test").Create()
factory.NewMeasure(owner).WithName("ZZZ Order Test").Create()
t.Run("order by name ascending", func(t *testing.T) {
query := `
query($id: ID!, $orderBy: MeasureOrder) {
node(id: $id) {
... on Organization {
measures(first: 100, orderBy: $orderBy) {
edges {
node {
id
name
}
}
}
}
}
}
`
var result struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
} `json:"measures"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{
"id": owner.GetOrganizationID().String(),
"orderBy": map[string]any{
"field": "NAME",
"direction": "ASC",
},
}, &result)
require.NoError(t, err)
// Verify ordering - names should be in ascending order
names := make([]string, len(result.Node.Measures.Edges))
for i, edge := range result.Node.Measures.Edges {
names[i] = edge.Node.Name
}
testutil.AssertOrderedAscending(t, names, "name")
})
t.Run("order by name descending", func(t *testing.T) {
query := `
query($id: ID!, $orderBy: MeasureOrder) {
node(id: $id) {
... on Organization {
measures(first: 100, orderBy: $orderBy) {
edges {
node {
id
name
}
}
}
}
}
}
`
var result struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"node"`
} `json:"edges"`
} `json:"measures"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{
"id": owner.GetOrganizationID().String(),
"orderBy": map[string]any{
"field": "NAME",
"direction": "DESC",
},
}, &result)
require.NoError(t, err)
// Verify ordering - names should be in descending order
names := make([]string, len(result.Node.Measures.Edges))
for i, edge := range result.Node.Measures.Edges {
names[i] = edge.Node.Name
}
testutil.AssertOrderedDescending(t, names, "name")
})
t.Run("order by created_at", func(t *testing.T) {
query := `
query($id: ID!, $orderBy: MeasureOrder) {
node(id: $id) {
... on Organization {
measures(first: 100, orderBy: $orderBy) {
edges {
node {
id
createdAt
}
}
}
}
}
}
`
var result struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
} `json:"node"`
} `json:"edges"`
} `json:"measures"`
} `json:"node"`
}
err := owner.Execute(query, map[string]any{
"id": owner.GetOrganizationID().String(),
"orderBy": map[string]any{
"field": "CREATED_AT",
"direction": "DESC",
},
}, &result)
require.NoError(t, err)
// Verify ordering - createdAt should be in descending order
times := make([]time.Time, len(result.Node.Measures.Edges))
for i, edge := range result.Node.Measures.Edges {
times[i] = edge.Node.CreatedAt
}
testutil.AssertTimesOrderedDescending(t, times, "createdAt")
})
}