Generated documents (asset list, risk register, SoA, ...) previously
only ever produced a new major version. Every regeneration of an
auto-built register consumed a major number, even when the change was
trivial. They now accept a minor flag and publish as
currentMajor.currentMinor+1 when set, bypassing the approval flow.
To carry the flag through cleanly, the document publish API was
refactored. The three split mutations (publishMajor, publishMinor,
requestDocumentVersionApproval) and the two bulk variants collapse
into a single publishDocument / bulkPublishDocuments, both taking the
new minor: Boolean! and a now-required changelog: String!. The same
shape flows through the CLI ("prb document publish --minor"), the MCP
tool, the n8n operations, and the Relay dialogs, where each
generated-doc dialog gains a "Publish as minor" button. Publishing
minor without an existing major is rejected with
ErrCannotPublishMinorWithoutMajor.
This is a deliberate breaking change for callers of the prior
mutations.
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
1151 lines
29 KiB
Go
1151 lines
29 KiB
Go
// Copyright (c) 2026 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 (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.probo.inc/probo/e2e/internal/testutil"
|
|
)
|
|
|
|
func TestEmployeeDocument_NodeAccess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
employee := testutil.NewClientInOrg(t, testutil.RoleEmployee, owner)
|
|
|
|
docID, _ := createTestDocument(t, owner)
|
|
|
|
t.Run("employee cannot list documents", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := employee.Do(`
|
|
query ListDocuments($orgId: ID!) {
|
|
node(id: $orgId) {
|
|
... on Organization {
|
|
documents(first: 10) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"orgId": employee.GetOrganizationID().String()})
|
|
testutil.RequireForbiddenError(t, err, "employee should not list documents")
|
|
})
|
|
|
|
t.Run("employee cannot access document via node query", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := employee.Do(`
|
|
query GetDocument($id: ID!) {
|
|
node(id: $id) {
|
|
... on Document {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"id": docID})
|
|
testutil.RequireForbiddenError(t, err, "employee should not access document via node")
|
|
})
|
|
|
|
t.Run("owner can access document via node query", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := owner.Do(`
|
|
query GetDocument($id: ID!) {
|
|
node(id: $id) {
|
|
... on Document {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"id": docID})
|
|
require.NoError(t, err, "owner should access document via node")
|
|
})
|
|
|
|
t.Run("employee cannot access signableDocument they are not signer of", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := employee.Do(`
|
|
query($docId: ID!) {
|
|
viewer {
|
|
signableDocument(id: $docId) {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"docId": docID})
|
|
testutil.RequireErrorCode(t, err, "NOT_FOUND", "employee should not access signableDocument for doc they are not signer of")
|
|
})
|
|
|
|
t.Run("employee cannot access approvableDocument they are not approver of", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := employee.Do(`
|
|
query($docId: ID!) {
|
|
viewer {
|
|
approvableDocument(id: $docId) {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"docId": docID})
|
|
testutil.RequireErrorCode(t, err, "NOT_FOUND", "employee should not access approvableDocument for doc they are not approver of")
|
|
})
|
|
}
|
|
|
|
func TestEmployeeDocument_ExportPDF(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
employee := testutil.NewClientInOrg(t, testutil.RoleEmployee, owner)
|
|
|
|
_, docVersionID := createTestDocument(t, owner)
|
|
|
|
t.Run("employee cannot use exportDocumentVersionPDF", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := employee.Do(`
|
|
mutation ExportPDF($input: ExportDocumentVersionPDFInput!) {
|
|
exportDocumentVersionPDF(input: $input) {
|
|
data
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": docVersionID,
|
|
"withWatermark": false,
|
|
"withSignatures": false,
|
|
},
|
|
})
|
|
testutil.RequireForbiddenError(t, err, "employee should not use exportDocumentVersionPDF")
|
|
})
|
|
|
|
t.Run("owner can use exportDocumentVersionPDF", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := owner.Do(`
|
|
mutation ExportPDF($input: ExportDocumentVersionPDFInput!) {
|
|
exportDocumentVersionPDF(input: $input) {
|
|
data
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": docVersionID,
|
|
"withWatermark": false,
|
|
"withSignatures": false,
|
|
},
|
|
})
|
|
require.NoError(t, err, "owner should use exportDocumentVersionPDF")
|
|
})
|
|
|
|
t.Run("employee cannot use exportEmployeeDocumentVersionPDF without being signer", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := employee.Do(`
|
|
mutation ExportEmployeePDF($input: ExportEmployeeDocumentVersionPDFInput!) {
|
|
exportEmployeeDocumentVersionPDF(input: $input) {
|
|
data
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": docVersionID,
|
|
},
|
|
})
|
|
testutil.RequireErrorCode(t, err, "NOT_FOUND", "employee should not export PDF for document they are not signer of")
|
|
})
|
|
}
|
|
|
|
func TestEmployeeDocument_SignableDocuments(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
employee := testutil.NewClientInOrg(t, testutil.RoleEmployee, owner)
|
|
|
|
t.Run("employee can list signable documents", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var result struct {
|
|
Viewer struct {
|
|
SignableDocuments struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"signableDocuments"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err := employee.Execute(`
|
|
query($orgId: ID!) {
|
|
viewer {
|
|
signableDocuments(organizationId: $orgId, first: 10) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"orgId": employee.GetOrganizationID().String()}, &result)
|
|
require.NoError(t, err, "employee should list signable documents")
|
|
})
|
|
|
|
t.Run("employee can list approvable documents", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var result struct {
|
|
Viewer struct {
|
|
ApprovableDocuments struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"approvableDocuments"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err := employee.Execute(`
|
|
query($orgId: ID!) {
|
|
viewer {
|
|
approvableDocuments(organizationId: $orgId, first: 10) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"orgId": employee.GetOrganizationID().String()}, &result)
|
|
require.NoError(t, err, "employee should list approvable documents")
|
|
})
|
|
|
|
t.Run("employee does not see document they are not signer of in signableDocuments", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a document with signature requested from owner only
|
|
docID, _ := createTestDocument(t, owner)
|
|
approveTestDocument(t, owner, docID)
|
|
|
|
var versionResult struct {
|
|
Node struct {
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"node"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($id: ID!) {
|
|
node(id: $id) {
|
|
... on Document {
|
|
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"id": docID}, &versionResult)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, versionResult.Node.Versions.Edges)
|
|
|
|
ownerProfileID := owner.GetProfileID().String()
|
|
_, err = owner.Do(`
|
|
mutation($input: RequestSignatureInput!) {
|
|
requestSignature(input: $input) {
|
|
documentVersionSignatureEdge { node { id } }
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": versionResult.Node.Versions.Edges[0].Node.ID,
|
|
"signatoryId": ownerProfileID,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Employee should not see this document
|
|
var listResult struct {
|
|
Viewer struct {
|
|
SignableDocuments struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"signableDocuments"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err = employee.Execute(`
|
|
query($orgId: ID!) {
|
|
viewer {
|
|
signableDocuments(organizationId: $orgId, first: 100) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"orgId": employee.GetOrganizationID().String()}, &listResult)
|
|
require.NoError(t, err)
|
|
|
|
for _, edge := range listResult.Viewer.SignableDocuments.Edges {
|
|
assert.NotEqual(t, docID, edge.Node.ID, "employee should not see document they are not signer of")
|
|
}
|
|
})
|
|
|
|
t.Run("employee does not see document they are not approver of in approvableDocuments", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create a document with approval requested from owner only
|
|
docID, _ := createTestDocument(t, owner)
|
|
ownerProfileID := owner.GetProfileID().String()
|
|
|
|
_, err := owner.Do(`
|
|
mutation($input: PublishDocumentInput!) {
|
|
publishDocument(input: $input) {
|
|
approvalQuorum { id }
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"minor": false,
|
|
"documentId": docID,
|
|
"approverIds": []string{ownerProfileID},
|
|
"changelog": "Test changelog",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Employee should not see this document
|
|
var listResult struct {
|
|
Viewer struct {
|
|
ApprovableDocuments struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"approvableDocuments"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err = employee.Execute(`
|
|
query($orgId: ID!) {
|
|
viewer {
|
|
approvableDocuments(organizationId: $orgId, first: 100) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"orgId": employee.GetOrganizationID().String()}, &listResult)
|
|
require.NoError(t, err)
|
|
|
|
for _, edge := range listResult.Viewer.ApprovableDocuments.Edges {
|
|
assert.NotEqual(t, docID, edge.Node.ID, "employee should not see document they are not approver of")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestEmployeeDocument_FilterModeIsolation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner)
|
|
|
|
// Create a document and have the admin approve it (so owner is NOT an approver)
|
|
docID, _ := createTestDocument(t, owner)
|
|
|
|
adminProfileID := admin.GetProfileID().String()
|
|
|
|
_, err := owner.Do(`
|
|
mutation($input: PublishDocumentInput!) {
|
|
publishDocument(input: $input) {
|
|
approvalQuorum { id }
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"minor": false,
|
|
"documentId": docID,
|
|
"approverIds": []string{adminProfileID},
|
|
"changelog": "Test changelog",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Get the version ID created by the approval request
|
|
var approveVersionResult struct {
|
|
Node struct {
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"node"`
|
|
}
|
|
|
|
err = owner.Execute(`
|
|
query($id: ID!) {
|
|
node(id: $id) {
|
|
... on Document {
|
|
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"id": docID}, &approveVersionResult)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, approveVersionResult.Node.Versions.Edges)
|
|
|
|
pendingVersionID := approveVersionResult.Node.Versions.Edges[0].Node.ID
|
|
|
|
_, err = admin.Do(`
|
|
mutation($input: ApproveDocumentVersionInput!) {
|
|
approveDocumentVersion(input: $input) {
|
|
approvalDecision { id }
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": pendingVersionID,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Get the published version ID
|
|
var versionResult struct {
|
|
Node struct {
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"node"`
|
|
}
|
|
|
|
err = owner.Execute(`
|
|
query($id: ID!) {
|
|
node(id: $id) {
|
|
... on Document {
|
|
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"id": docID}, &versionResult)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, versionResult.Node.Versions.Edges)
|
|
|
|
publishedVersionID := versionResult.Node.Versions.Edges[0].Node.ID
|
|
ownerProfileID := owner.GetProfileID().String()
|
|
|
|
// Request signature from the owner only (owner is a signer but NOT an approver)
|
|
_, err = owner.Do(`
|
|
mutation($input: RequestSignatureInput!) {
|
|
requestSignature(input: $input) {
|
|
documentVersionSignatureEdge {
|
|
node { id }
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": publishedVersionID,
|
|
"signatoryId": ownerProfileID,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("signer sees document in signableDocuments list", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var result struct {
|
|
Viewer struct {
|
|
SignableDocuments struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"signableDocuments"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($orgId: ID!) {
|
|
viewer {
|
|
signableDocuments(organizationId: $orgId, first: 10) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"orgId": owner.GetOrganizationID().String()}, &result)
|
|
require.NoError(t, err)
|
|
|
|
var found bool
|
|
for _, edge := range result.Viewer.SignableDocuments.Edges {
|
|
if edge.Node.ID == docID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "signer should see document in signableDocuments list")
|
|
})
|
|
|
|
t.Run("signer does not see document in approvableDocuments list", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var result struct {
|
|
Viewer struct {
|
|
ApprovableDocuments struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"approvableDocuments"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($orgId: ID!) {
|
|
viewer {
|
|
approvableDocuments(organizationId: $orgId, first: 10) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"orgId": owner.GetOrganizationID().String()}, &result)
|
|
require.NoError(t, err)
|
|
|
|
for _, edge := range result.Viewer.ApprovableDocuments.Edges {
|
|
assert.NotEqual(t, docID, edge.Node.ID, "signer-only document should not appear in approvableDocuments list")
|
|
}
|
|
})
|
|
|
|
t.Run("signer cannot access document via approvableDocument", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := owner.Do(`
|
|
query($docId: ID!) {
|
|
viewer {
|
|
approvableDocument(id: $docId) {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"docId": docID})
|
|
testutil.RequireErrorCode(t, err, "NOT_FOUND", "signer-only should not access approvableDocument")
|
|
})
|
|
}
|
|
|
|
func TestEmployeeDocument_ApproverFilterModeIsolation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
|
|
// Create a document and request approval (owner is approver but NOT a signer)
|
|
docID, _ := createTestDocument(t, owner)
|
|
ownerProfileID := owner.GetProfileID().String()
|
|
|
|
_, err := owner.Do(`
|
|
mutation($input: PublishDocumentInput!) {
|
|
publishDocument(input: $input) {
|
|
approvalQuorum { id }
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"minor": false,
|
|
"documentId": docID,
|
|
"approverIds": []string{ownerProfileID},
|
|
"changelog": "Test changelog",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("approver sees document in approvableDocuments list", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var result struct {
|
|
Viewer struct {
|
|
ApprovableDocuments struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"approvableDocuments"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($orgId: ID!) {
|
|
viewer {
|
|
approvableDocuments(organizationId: $orgId, first: 10) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"orgId": owner.GetOrganizationID().String()}, &result)
|
|
require.NoError(t, err)
|
|
|
|
var found bool
|
|
for _, edge := range result.Viewer.ApprovableDocuments.Edges {
|
|
if edge.Node.ID == docID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "approver should see document in approvableDocuments list")
|
|
})
|
|
|
|
t.Run("approver does not see document in signableDocuments list", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var result struct {
|
|
Viewer struct {
|
|
SignableDocuments struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"signableDocuments"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($orgId: ID!) {
|
|
viewer {
|
|
signableDocuments(organizationId: $orgId, first: 10) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"orgId": owner.GetOrganizationID().String()}, &result)
|
|
require.NoError(t, err)
|
|
|
|
for _, edge := range result.Viewer.SignableDocuments.Edges {
|
|
assert.NotEqual(t, docID, edge.Node.ID, "approver-only document should not appear in signableDocuments list")
|
|
}
|
|
})
|
|
|
|
t.Run("approver cannot access document via signableDocument", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := owner.Do(`
|
|
query($docId: ID!) {
|
|
viewer {
|
|
signableDocument(id: $docId) {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"docId": docID})
|
|
testutil.RequireErrorCode(t, err, "NOT_FOUND", "approver-only should not access signableDocument")
|
|
})
|
|
}
|
|
|
|
func TestEmployeeDocument_UnsignedDocument(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
|
|
// Create a document, approve and publish it, then request signature from owner
|
|
docID, _ := createTestDocument(t, owner)
|
|
approveTestDocument(t, owner, docID)
|
|
|
|
var versionResult struct {
|
|
Node struct {
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"node"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($id: ID!) {
|
|
node(id: $id) {
|
|
... on Document {
|
|
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"id": docID}, &versionResult)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, versionResult.Node.Versions.Edges)
|
|
|
|
publishedVersionID := versionResult.Node.Versions.Edges[0].Node.ID
|
|
ownerProfileID := owner.GetProfileID().String()
|
|
|
|
_, err = owner.Do(`
|
|
mutation($input: RequestSignatureInput!) {
|
|
requestSignature(input: $input) {
|
|
documentVersionSignatureEdge { node { id } }
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": publishedVersionID,
|
|
"signatoryId": ownerProfileID,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("unsigned document shows signed=false on version", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var result struct {
|
|
Viewer struct {
|
|
SignableDocument struct {
|
|
ID string `json:"id"`
|
|
Signed *bool `json:"signed"`
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
Signed bool `json:"signed"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"signableDocument"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($docId: ID!) {
|
|
viewer {
|
|
signableDocument(id: $docId) {
|
|
id
|
|
signed
|
|
versions(first: 10) {
|
|
edges {
|
|
node {
|
|
id
|
|
signed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"docId": docID}, &result)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result.Viewer.SignableDocument.Signed)
|
|
assert.False(t, *result.Viewer.SignableDocument.Signed, "document should not be signed yet")
|
|
require.NotEmpty(t, result.Viewer.SignableDocument.Versions.Edges)
|
|
assert.False(t, result.Viewer.SignableDocument.Versions.Edges[0].Node.Signed, "version should not be signed yet")
|
|
})
|
|
|
|
t.Run("unsigned document still allows exportEmployeeDocumentVersionPDF", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := owner.Do(`
|
|
mutation($input: ExportEmployeeDocumentVersionPDFInput!) {
|
|
exportEmployeeDocumentVersionPDF(input: $input) {
|
|
data
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": publishedVersionID,
|
|
},
|
|
})
|
|
require.NoError(t, err, "signer should export PDF even before signing")
|
|
})
|
|
}
|
|
|
|
func TestEmployeeDocument_UnapprovedDocument(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
|
|
// Create a document and request approval from owner (don't approve yet)
|
|
docID, _ := createTestDocument(t, owner)
|
|
ownerProfileID := owner.GetProfileID().String()
|
|
|
|
_, err := owner.Do(`
|
|
mutation($input: PublishDocumentInput!) {
|
|
publishDocument(input: $input) {
|
|
approvalQuorum { id }
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"minor": false,
|
|
"documentId": docID,
|
|
"approverIds": []string{ownerProfileID},
|
|
"changelog": "Test changelog",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("unapproved document shows PENDING state", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var result struct {
|
|
Viewer struct {
|
|
ApprovableDocument struct {
|
|
ID string `json:"id"`
|
|
ApprovalState *string `json:"approvalState"`
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
ApprovalDecision *struct {
|
|
ID string `json:"id"`
|
|
State string `json:"state"`
|
|
} `json:"approvalDecision"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"approvableDocument"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($docId: ID!) {
|
|
viewer {
|
|
approvableDocument(id: $docId) {
|
|
id
|
|
approvalState
|
|
versions(first: 10) {
|
|
edges {
|
|
node {
|
|
id
|
|
approvalDecision {
|
|
id
|
|
state
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"docId": docID}, &result)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result.Viewer.ApprovableDocument.ApprovalState)
|
|
assert.Equal(t, "PENDING", *result.Viewer.ApprovableDocument.ApprovalState, "approval state should be PENDING")
|
|
require.NotEmpty(t, result.Viewer.ApprovableDocument.Versions.Edges)
|
|
require.NotNil(t, result.Viewer.ApprovableDocument.Versions.Edges[0].Node.ApprovalDecision)
|
|
assert.Equal(t, "PENDING", result.Viewer.ApprovableDocument.Versions.Edges[0].Node.ApprovalDecision.State, "decision should be PENDING")
|
|
})
|
|
|
|
t.Run("unapproved document still allows exportEmployeeDocumentVersionPDF", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var versionResult struct {
|
|
Node struct {
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"node"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($id: ID!) {
|
|
node(id: $id) {
|
|
... on Document {
|
|
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"id": docID}, &versionResult)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, versionResult.Node.Versions.Edges)
|
|
|
|
_, err = owner.Do(`
|
|
mutation($input: ExportEmployeeDocumentVersionPDFInput!) {
|
|
exportEmployeeDocumentVersionPDF(input: $input) {
|
|
data
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": versionResult.Node.Versions.Edges[0].Node.ID,
|
|
},
|
|
})
|
|
require.NoError(t, err, "approver should export PDF even before approving")
|
|
})
|
|
}
|
|
|
|
func TestEmployeeDocument_SignableDocumentNestedFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
|
|
// Create a document, approve and publish it
|
|
docID, _ := createTestDocument(t, owner)
|
|
approveTestDocument(t, owner, docID)
|
|
|
|
// Get the published version ID
|
|
var versionResult struct {
|
|
Node struct {
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"node"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($id: ID!) {
|
|
node(id: $id) {
|
|
... on Document {
|
|
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"id": docID}, &versionResult)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, versionResult.Node.Versions.Edges)
|
|
|
|
publishedVersionID := versionResult.Node.Versions.Edges[0].Node.ID
|
|
|
|
// Request signature from the owner
|
|
ownerProfileID := owner.GetProfileID().String()
|
|
|
|
_, err = owner.Do(`
|
|
mutation($input: RequestSignatureInput!) {
|
|
requestSignature(input: $input) {
|
|
documentVersionSignatureEdge {
|
|
node { id state }
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": publishedVersionID,
|
|
"signatoryId": ownerProfileID,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("owner can access signableDocument with nested fields", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var result struct {
|
|
Viewer struct {
|
|
SignableDocument struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Signed *bool `json:"signed"`
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
Signed bool `json:"signed"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"signableDocument"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($docId: ID!) {
|
|
viewer {
|
|
signableDocument(id: $docId) {
|
|
id
|
|
title
|
|
signed
|
|
versions(first: 10) {
|
|
edges {
|
|
node {
|
|
id
|
|
signed
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"docId": docID}, &result)
|
|
require.NoError(t, err, "owner should access signableDocument with nested fields")
|
|
assert.NotEmpty(t, result.Viewer.SignableDocument.ID)
|
|
assert.NotNil(t, result.Viewer.SignableDocument.Signed)
|
|
assert.NotEmpty(t, result.Viewer.SignableDocument.Versions.Edges)
|
|
})
|
|
|
|
t.Run("owner can use exportEmployeeDocumentVersionPDF as signer", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := owner.Do(`
|
|
mutation($input: ExportEmployeeDocumentVersionPDFInput!) {
|
|
exportEmployeeDocumentVersionPDF(input: $input) {
|
|
data
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": publishedVersionID,
|
|
},
|
|
})
|
|
require.NoError(t, err, "owner signer should use exportEmployeeDocumentVersionPDF")
|
|
})
|
|
}
|
|
|
|
func TestEmployeeDocument_ApprovableDocumentNestedFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
|
|
|
// Create a document and request approval with the owner as approver
|
|
docID, _ := createTestDocument(t, owner)
|
|
ownerProfileID := owner.GetProfileID().String()
|
|
|
|
_, err := owner.Do(`
|
|
mutation($input: PublishDocumentInput!) {
|
|
publishDocument(input: $input) {
|
|
approvalQuorum { id }
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"minor": false,
|
|
"documentId": docID,
|
|
"approverIds": []string{ownerProfileID},
|
|
"changelog": "Test changelog",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("owner can access approvableDocument with nested fields", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var result struct {
|
|
Viewer struct {
|
|
ApprovableDocument struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
ApprovalState *string `json:"approvalState"`
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
ApprovalDecision *struct {
|
|
ID string `json:"id"`
|
|
State string `json:"state"`
|
|
} `json:"approvalDecision"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"approvableDocument"`
|
|
} `json:"viewer"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($docId: ID!) {
|
|
viewer {
|
|
approvableDocument(id: $docId) {
|
|
id
|
|
title
|
|
approvalState
|
|
versions(first: 10) {
|
|
edges {
|
|
node {
|
|
id
|
|
approvalDecision {
|
|
id
|
|
state
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"docId": docID}, &result)
|
|
require.NoError(t, err, "owner should access approvableDocument with nested fields")
|
|
assert.NotEmpty(t, result.Viewer.ApprovableDocument.ID)
|
|
assert.NotEmpty(t, result.Viewer.ApprovableDocument.Versions.Edges)
|
|
})
|
|
|
|
t.Run("owner can use exportEmployeeDocumentVersionPDF as approver", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Get the version ID
|
|
var versionResult struct {
|
|
Node struct {
|
|
Versions struct {
|
|
Edges []struct {
|
|
Node struct {
|
|
ID string `json:"id"`
|
|
} `json:"node"`
|
|
} `json:"edges"`
|
|
} `json:"versions"`
|
|
} `json:"node"`
|
|
}
|
|
|
|
err := owner.Execute(`
|
|
query($id: ID!) {
|
|
node(id: $id) {
|
|
... on Document {
|
|
versions(first: 1, orderBy: { field: CREATED_AT, direction: DESC }) {
|
|
edges { node { id } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, map[string]any{"id": docID}, &versionResult)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, versionResult.Node.Versions.Edges)
|
|
|
|
versionID := versionResult.Node.Versions.Edges[0].Node.ID
|
|
|
|
_, err = owner.Do(`
|
|
mutation($input: ExportEmployeeDocumentVersionPDFInput!) {
|
|
exportEmployeeDocumentVersionPDF(input: $input) {
|
|
data
|
|
}
|
|
}
|
|
`, map[string]any{
|
|
"input": map[string]any{
|
|
"documentVersionId": versionID,
|
|
},
|
|
})
|
|
require.NoError(t, err, "owner approver should use exportEmployeeDocumentVersionPDF")
|
|
})
|
|
}
|