Link measures to third parties

Add a many-to-many relationship between measures and third parties,
surfaced as a measures tab on the third party detail page and a third
parties tab on the measure detail page. Each side gets a paginated
list with a link/unlink dialog.

Also remove the right-hand drawer on the measure detail page and
expose the state as a badge in the page header, mirroring how the
compliance page surfaces its active flag.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2026-05-22 16:07:48 +02:00
parent b6b1e801b1
commit 6dfdd7ca49
35 changed files with 2109 additions and 54 deletions

View File

@@ -2239,3 +2239,240 @@ func TestMeasure_Ordering(t *testing.T) {
testutil.AssertTimesOrderedDescending(t, times, "createdAt")
})
}
func TestMeasure_ThirdPartyMapping(t *testing.T) {
t.Parallel()
owner := testutil.NewClient(t, testutil.RoleOwner)
const createMutation = `
mutation($input: CreateMeasureThirdPartyMappingInput!) {
createMeasureThirdPartyMapping(input: $input) {
measureEdge { node { id } }
thirdPartyEdge { node { id } }
}
}
`
const deleteMutation = `
mutation($input: DeleteMeasureThirdPartyMappingInput!) {
deleteMeasureThirdPartyMapping(input: $input) {
deletedMeasureId
deletedThirdPartyId
}
}
`
t.Run("create mapping links measure to third party on both sides", func(t *testing.T) {
t.Parallel()
measureID := factory.NewMeasure(owner).WithName("Mapping Measure").Create()
thirdPartyID := factory.NewThirdParty(owner).WithName("Mapping Third Party").Create()
_, err := owner.Do(createMutation, map[string]any{
"input": map[string]any{
"measureId": measureID,
"thirdPartyId": thirdPartyID,
},
})
require.NoError(t, err)
const measureQuery = `
query($id: ID!) {
node(id: $id) {
... on Measure {
thirdParties(first: 10) {
edges { node { id } }
totalCount
}
}
}
}
`
var measureResult struct {
Node struct {
ThirdParties struct {
Edges []struct {
Node struct {
ID string `json:"id"`
} `json:"node"`
} `json:"edges"`
TotalCount int `json:"totalCount"`
} `json:"thirdParties"`
} `json:"node"`
}
err = owner.Execute(measureQuery, map[string]any{"id": measureID}, &measureResult)
require.NoError(t, err)
assert.Equal(t, 1, measureResult.Node.ThirdParties.TotalCount)
assert.Equal(t, thirdPartyID, measureResult.Node.ThirdParties.Edges[0].Node.ID)
const thirdPartyQuery = `
query($id: ID!) {
node(id: $id) {
... on ThirdParty {
measures(first: 10) {
edges { node { id } }
totalCount
}
}
}
}
`
var tpResult struct {
Node struct {
Measures struct {
Edges []struct {
Node struct {
ID string `json:"id"`
} `json:"node"`
} `json:"edges"`
TotalCount int `json:"totalCount"`
} `json:"measures"`
} `json:"node"`
}
err = owner.Execute(thirdPartyQuery, map[string]any{"id": thirdPartyID}, &tpResult)
require.NoError(t, err)
assert.Equal(t, 1, tpResult.Node.Measures.TotalCount)
assert.Equal(t, measureID, tpResult.Node.Measures.Edges[0].Node.ID)
})
t.Run("create mapping is idempotent", func(t *testing.T) {
t.Parallel()
measureID := factory.NewMeasure(owner).WithName("Idempotent Measure").Create()
thirdPartyID := factory.NewThirdParty(owner).WithName("Idempotent Third Party").Create()
input := map[string]any{
"measureId": measureID,
"thirdPartyId": thirdPartyID,
}
_, err := owner.Do(createMutation, map[string]any{"input": input})
require.NoError(t, err)
_, err = owner.Do(createMutation, map[string]any{"input": input})
require.NoError(t, err, "second mapping creation should be idempotent")
const countQuery = `
query($id: ID!) {
node(id: $id) {
... on Measure {
thirdParties(first: 10) { totalCount }
}
}
}
`
var result struct {
Node struct {
ThirdParties struct {
TotalCount int `json:"totalCount"`
} `json:"thirdParties"`
} `json:"node"`
}
err = owner.Execute(countQuery, map[string]any{"id": measureID}, &result)
require.NoError(t, err)
assert.Equal(t, 1, result.Node.ThirdParties.TotalCount, "duplicate create must not produce a second row")
})
t.Run("delete mapping removes link from both sides", func(t *testing.T) {
t.Parallel()
measureID := factory.NewMeasure(owner).WithName("Unlink Measure").Create()
thirdPartyID := factory.NewThirdParty(owner).WithName("Unlink Third Party").Create()
input := map[string]any{
"measureId": measureID,
"thirdPartyId": thirdPartyID,
}
_, err := owner.Do(createMutation, map[string]any{"input": input})
require.NoError(t, err)
_, err = owner.Do(deleteMutation, map[string]any{"input": input})
require.NoError(t, err)
const measureCountQuery = `
query($id: ID!) {
node(id: $id) {
... on Measure {
thirdParties(first: 10) { totalCount }
}
}
}
`
var measureResult struct {
Node struct {
ThirdParties struct {
TotalCount int `json:"totalCount"`
} `json:"thirdParties"`
} `json:"node"`
}
err = owner.Execute(measureCountQuery, map[string]any{"id": measureID}, &measureResult)
require.NoError(t, err)
assert.Equal(t, 0, measureResult.Node.ThirdParties.TotalCount, "measure side should have no linked third parties")
const thirdPartyCountQuery = `
query($id: ID!) {
node(id: $id) {
... on ThirdParty {
measures(first: 10) { totalCount }
}
}
}
`
var thirdPartyResult struct {
Node struct {
Measures struct {
TotalCount int `json:"totalCount"`
} `json:"measures"`
} `json:"node"`
}
err = owner.Execute(thirdPartyCountQuery, map[string]any{"id": thirdPartyID}, &thirdPartyResult)
require.NoError(t, err)
assert.Equal(t, 0, thirdPartyResult.Node.Measures.TotalCount, "third-party side should have no linked measures")
})
t.Run("viewer cannot create mapping", func(t *testing.T) {
t.Parallel()
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
measureID := factory.NewMeasure(owner).WithName("RBAC Measure").Create()
thirdPartyID := factory.NewThirdParty(owner).WithName("RBAC Third Party").Create()
_, err := viewer.Do(createMutation, map[string]any{
"input": map[string]any{
"measureId": measureID,
"thirdPartyId": thirdPartyID,
},
})
require.Error(t, err)
})
t.Run("tenant isolation on mapping creation", func(t *testing.T) {
t.Parallel()
otherOwner := testutil.NewClient(t, testutil.RoleOwner)
measureID := factory.NewMeasure(owner).WithName("Tenant Measure").Create()
otherThirdPartyID := factory.NewThirdParty(otherOwner).WithName("Other Tenant Third Party").Create()
_, err := owner.Do(createMutation, map[string]any{
"input": map[string]any{
"measureId": measureID,
"thirdPartyId": otherThirdPartyID,
},
})
require.Error(t, err, "should not link a third party from another tenant")
})
}