Add wsl linter and fix

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-19 14:51:08 +04:00
parent eedfdcecc8
commit 9156d6a16a
882 changed files with 6068 additions and 574 deletions

20
.golangci.yml Normal file
View File

@@ -0,0 +1,20 @@
version: "2"
linters:
default: none
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- wsl_v5
settings:
wsl_v5:
allow-first-in-block: true
allow-whole-block: false
branch-max-lines: 2
case-max-lines: 0
cuddle-max-statements: 1
default: default
enable:
- after-block

View File

@@ -68,6 +68,7 @@ func run() error {
}
fmt.Println("done")
return nil
}
@@ -95,6 +96,7 @@ func newPgClientFromDSN(dsn string) (*pg.Client, error) {
if u.Port() == "" {
host = net.JoinHostPort(u.Hostname(), "5432")
}
opts = append(opts, pg.WithAddr(host))
}

View File

@@ -73,6 +73,7 @@ func run() error {
}
var ids []string
err = pgClient.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
rows, err := conn.Query(
ctx,
@@ -94,8 +95,10 @@ ORDER BY id;
if err := rows.Scan(&id); err != nil {
return fmt.Errorf("cannot scan document version id: %w", err)
}
ids = append(ids, id)
}
return rows.Err()
})
if err != nil {
@@ -103,13 +106,16 @@ ORDER BY id;
}
var failures int
for _, idStr := range ids {
if err := migrateOneInTx(ctx, pgClient, idStr, dryRun); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
failures++
if !continueOnError {
return fmt.Errorf("stopped after %d failure(s)", failures)
}
continue
}
}
@@ -119,6 +125,7 @@ ORDER BY id;
}
fmt.Println("done")
return nil
}
@@ -135,6 +142,7 @@ func newPgClientFromDSN(dsn string) (*pg.Client, error) {
if u.Port() == "" {
host = net.JoinHostPort(u.Hostname(), "5432")
}
opts = append(opts, pg.WithAddr(host))
}
@@ -197,6 +205,7 @@ func migrateOne(ctx context.Context, tx pg.Tx, idStr string, dryRun bool) error
}
fmt.Printf("updated %s\n", idStr)
return nil
}
@@ -207,5 +216,6 @@ func isProseMirrorDocJSON(s string) bool {
if err := json.Unmarshal([]byte(s), &probe); err != nil {
return false
}
return probe.Type == "doc"
}

View File

@@ -33,9 +33,11 @@ func main() {
if isNonInteractiveEnv() {
ios.ForceNonInteractive = true
}
if isNoColorEnv() {
ios.ForceNoColor = true
}
ios.ApplyColorProfile()
f := &cmdutil.Factory{
@@ -58,15 +60,19 @@ func isNonInteractiveEnv() bool {
if v := os.Getenv("PROBO_NO_INTERACTIVE"); v == "1" || v == "true" {
return true
}
if v := os.Getenv("CI"); v == "true" || v == "1" {
return true
}
if os.Getenv("DEBIAN_FRONTEND") == "noninteractive" {
return true
}
if os.Getenv("TERM") == "dumb" {
return true
}
return false
}
@@ -74,8 +80,10 @@ func isNoColorEnv() bool {
if _, ok := os.LookupEnv("NO_COLOR"); ok {
return true
}
if os.Getenv("TERM") == "dumb" {
return true
}
return false
}

View File

@@ -30,6 +30,7 @@ var (
func main() {
outputPath := flag.String("output", "/etc/probod/config.yml", "output path for the generated config file")
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *showVersion {

View File

@@ -29,6 +29,7 @@ var (
func main() {
impl := probod.New()
unit := unit.NewUnit(impl, "probod", version, env)
err := unit.Run()
if err != nil && err != context.Canceled {
panic(err)

View File

@@ -540,6 +540,7 @@ func TestAccessReviewCampaign_DeleteRemovesFromListAndNode(t *testing.T) {
err = owner.Execute(listQuery, map[string]any{"id": orgID}, &listResult)
require.NoError(t, err)
for _, edge := range listResult.Node.AccessReviewCampaigns.Edges {
assert.NotEqual(t, campaignID, edge.Node.ID, "deleted campaign must not appear in the connection")
}
@@ -557,6 +558,7 @@ func TestAccessReviewCampaign_DeleteRemovesFromListAndNode(t *testing.T) {
`
_, err = owner.Do(nodeQuery, map[string]any{"id": campaignID})
var gqlErrors testutil.GraphQLErrors
require.ErrorAs(t, err, &gqlErrors)
require.Len(t, gqlErrors, 1)
@@ -1114,11 +1116,13 @@ func TestAccessReviewCampaign_FullLifecycle(t *testing.T) {
}
var campaignResult campaignQueryResult
require.Eventually(t, func() bool {
err := owner.Execute(nodeQuery, map[string]any{"id": campaignID}, &campaignResult)
if err != nil {
return false
}
return campaignResult.Node.Status == "PENDING_ACTIONS"
}, 60*time.Second, 1*time.Second, "campaign should transition to PENDING_ACTIONS")
@@ -1239,6 +1243,7 @@ func TestAccessReviewCampaign_CloseRequiresAllDecisions(t *testing.T) {
}
}
`
err := owner.Execute(startQuery, map[string]any{
"input": map[string]any{
"accessReviewCampaignId": campaignID,
@@ -1264,6 +1269,7 @@ func TestAccessReviewCampaign_CloseRequiresAllDecisions(t *testing.T) {
if err := owner.Execute(nodeQuery, map[string]any{"id": campaignID}, &r); err != nil {
return false
}
return r.Node.Status == "PENDING_ACTIONS"
}, 60*time.Second, 1*time.Second)

View File

@@ -240,6 +240,7 @@ func TestAsset_PublishAssetList(t *testing.T) {
ver1Major := result1.PublishAssetList.DocumentVersionEdge.Node.Major
ver2Major := result2.PublishAssetList.DocumentVersionEdge.Node.Major
assert.Equal(t, 1, ver1Major)
assert.Equal(t, 2, ver2Major)
},

View File

@@ -125,6 +125,7 @@ func TestAsset_Update(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
assetID := createResult.CreateAsset.AssetEdge.Node.ID
const query = `
@@ -205,6 +206,7 @@ func TestAsset_Delete(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
assetID := createResult.CreateAsset.AssetEdge.Node.ID
const query = `

View File

@@ -80,17 +80,21 @@ func TestAuditLog_List(t *testing.T) {
// Find the thirdParty create entry.
found := false
for _, edge := range result.Node.AuditLogEntries.Edges {
if edge.Node.Action == "core:thirdParty:create" {
found = true
assert.Equal(t, "USER", edge.Node.ActorType)
assert.Equal(t, "ThirdParty", edge.Node.ResourceType)
assert.NotEmpty(t, edge.Node.ActorID)
assert.NotEmpty(t, edge.Node.ResourceID)
assert.NotEmpty(t, edge.Node.CreatedAt)
break
}
}
assert.True(t, found, "expected to find core:thirdParty:create audit log entry")
}
@@ -143,6 +147,7 @@ func TestAuditLog_Filter(t *testing.T) {
}, &result)
require.NoError(t, err)
assert.GreaterOrEqual(t, result.Node.AuditLogEntries.TotalCount, 1)
for _, edge := range result.Node.AuditLogEntries.Edges {
assert.Equal(t, "core:thirdParty:create", edge.Node.Action)
}
@@ -171,6 +176,7 @@ func TestAuditLog_Filter(t *testing.T) {
}, &result)
require.NoError(t, err)
assert.GreaterOrEqual(t, result.Node.AuditLogEntries.TotalCount, 1)
for _, edge := range result.Node.AuditLogEntries.Edges {
assert.Equal(t, "ThirdParty", edge.Node.ResourceType)
}

View File

@@ -250,9 +250,11 @@ func TestAudit_Create_Validation(t *testing.T) {
if !tt.skipOrganization {
input["organizationId"] = owner.GetOrganizationID().String()
}
if !tt.skipFramework {
input["frameworkId"] = frameworkID
}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})
@@ -376,6 +378,7 @@ func TestAudit_Update(t *testing.T) {
require.NoError(t, err)
audit := result.UpdateAudit.Audit
switch tt.assertField {
case "name":
assert.Equal(t, tt.assertValue, audit.Name)
@@ -1188,6 +1191,7 @@ func TestAudit_Pagination(t *testing.T) {
assert.GreaterOrEqual(t, result.Node.Audits.TotalCount, 5)
testutil.AssertHasMorePages(t, result.Node.Audits.PageInfo)
queryAfter := `
query($id: ID!, $after: CursorKey) {
node(id: $id) {
@@ -1372,7 +1376,6 @@ func TestAudit_TenantIsolation(t *testing.T) {
err := org2Owner.Execute(query, map[string]any{
"id": org1Owner.GetOrganizationID().String(),
}, &result)
if err == nil {
for _, edge := range result.Node.Audits.Edges {
assert.NotEqual(t, auditID, edge.Node.ID, "Should not see audit from another org")
@@ -1434,6 +1437,7 @@ func TestAudit_Ordering(t *testing.T) {
for i, edge := range result.Node.Audits.Edges {
times[i] = edge.Node.CreatedAt
}
testutil.AssertTimesOrderedDescending(t, times, "createdAt")
})
}
@@ -1539,6 +1543,7 @@ func TestAudit_UploadReport(t *testing.T) {
Content: pdfContent1,
}, &result1)
require.NoError(t, err)
firstReportID := result1.UploadAuditReport.Audit.Report.ID
// Upload second report (should replace)

View File

@@ -75,6 +75,7 @@ func TestConnectorProviderInfos(t *testing.T) {
assert.NotEmpty(t, infos)
providerNames := make(map[string]bool)
for _, info := range infos {
assert.NotEmpty(t, info.Provider)
assert.NotEmpty(t, info.DisplayName)

View File

@@ -118,7 +118,6 @@ func TestControl_Update(t *testing.T) {
assert.Equal(t, controlID, result.UpdateControl.Control.ID)
assert.Equal(t, "Updated Control Name", result.UpdateControl.Control.Name)
})
}
func TestControl_Delete(t *testing.T) {
@@ -243,6 +242,7 @@ func TestControl_RequiredFields(t *testing.T) {
},
}, &frameworkResult)
require.NoError(t, err)
frameworkID := frameworkResult.CreateFramework.FrameworkEdge.Node.ID
createControlQuery := `
@@ -389,6 +389,7 @@ func TestControl_OmittableDescription(t *testing.T) {
},
}, &frameworkResult)
require.NoError(t, err)
frameworkID := frameworkResult.CreateFramework.FrameworkEdge.Node.ID
// Create control with description
@@ -427,6 +428,7 @@ func TestControl_OmittableDescription(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
controlID := createResult.CreateControl.ControlEdge.Node.ID
t.Run("Update with null description should clear it", func(t *testing.T) {
@@ -573,6 +575,7 @@ func TestControl_MaturityLevel(t *testing.T) {
t.Run("create with INITIAL maturityLevel", func(t *testing.T) {
var res createResult
err := owner.Execute(createControlQuery, map[string]any{
"input": map[string]any{
"frameworkId": frameworkID,
@@ -589,6 +592,7 @@ func TestControl_MaturityLevel(t *testing.T) {
t.Run("create with maturityLevel persists value", func(t *testing.T) {
var res createResult
err := owner.Execute(createControlQuery, map[string]any{
"input": map[string]any{
"frameworkId": frameworkID,
@@ -605,6 +609,7 @@ func TestControl_MaturityLevel(t *testing.T) {
t.Run("update lifecycle: set, change, omit", func(t *testing.T) {
var created createResult
err := owner.Execute(createControlQuery, map[string]any{
"input": map[string]any{
"frameworkId": frameworkID,
@@ -616,10 +621,12 @@ func TestControl_MaturityLevel(t *testing.T) {
},
}, &created)
require.NoError(t, err)
controlID := created.CreateControl.ControlEdge.Node.ID
// set
var setRes updateResult
err = owner.Execute(updateControlQuery, map[string]any{
"input": map[string]any{
"id": controlID,
@@ -631,6 +638,7 @@ func TestControl_MaturityLevel(t *testing.T) {
// change
var changeRes updateResult
err = owner.Execute(updateControlQuery, map[string]any{
"input": map[string]any{
"id": controlID,
@@ -642,6 +650,7 @@ func TestControl_MaturityLevel(t *testing.T) {
// omit field on next update -> stays unchanged
var omitRes updateResult
err = owner.Execute(updateControlQuery, map[string]any{
"input": map[string]any{
"id": controlID,
@@ -654,6 +663,7 @@ func TestControl_MaturityLevel(t *testing.T) {
t.Run("invalid maturityLevel is rejected", func(t *testing.T) {
var res createResult
err := owner.Execute(createControlQuery, map[string]any{
"input": map[string]any{
"frameworkId": frameworkID,
@@ -702,6 +712,7 @@ func TestControl_SubResolvers(t *testing.T) {
},
}, &frameworkResult)
require.NoError(t, err)
frameworkID := frameworkResult.CreateFramework.FrameworkEdge.Node.ID
// Create control
@@ -738,6 +749,7 @@ func TestControl_SubResolvers(t *testing.T) {
},
}, &controlResult)
require.NoError(t, err)
controlID := controlResult.CreateControl.ControlEdge.Node.ID
// Create a measure and link it
@@ -771,6 +783,7 @@ func TestControl_SubResolvers(t *testing.T) {
},
}, &measureResult)
require.NoError(t, err)
measureID := measureResult.CreateMeasure.MeasureEdge.Node.ID
// Create mapping

View File

@@ -84,6 +84,7 @@ func TestCookieBanner_Create(t *testing.T) {
}, &result)
require.NoError(t, err)
node := result.CreateCookieBanner.CookieBannerEdge.Node
assert.NotEmpty(t, node.ID)
assert.Equal(t, name, node.Name)
@@ -135,6 +136,7 @@ func TestCookieBanner_Create(t *testing.T) {
}, &result)
require.NoError(t, err)
node := result.CreateCookieBanner.CookieBannerEdge.Node
assert.NotEmpty(t, node.ID)
require.NotNil(t, node.PrivacyPolicyUrl)
@@ -189,6 +191,7 @@ func TestCookieBanner_Create(t *testing.T) {
for _, e := range result.Node.ConsentCategories.Edges {
kinds[e.Node.Kind] = true
}
assert.True(t, kinds["NECESSARY"], "should have a NECESSARY category")
})
@@ -262,6 +265,7 @@ func TestCookieBanner_Update(t *testing.T) {
`
newName := factory.SafeName("Updated")
var result struct {
UpdateCookieBanner struct {
CookieBanner struct {
@@ -410,6 +414,7 @@ func TestCookieBanner_ActivateDeactivate(t *testing.T) {
} `json:"cookieBanner"`
} `json:"deactivateCookieBanner"`
}
err := owner.Execute(`
mutation($input: DeactivateCookieBannerInput!) {
deactivateCookieBanner(input: $input) {
@@ -751,8 +756,10 @@ func TestCookieBanner_UpsertTranslation(t *testing.T) {
} `json:"cookieBannerTranslation"`
} `json:"upsertCookieBannerTranslation"`
}
err := owner.Execute(query, input, &result1)
require.NoError(t, err)
firstID := result1.UpsertCookieBannerTranslation.CookieBannerTranslation.ID
input["input"].(map[string]any)["translations"] = `{"title":"Ajustes de cookies"}`
@@ -765,6 +772,7 @@ func TestCookieBanner_UpsertTranslation(t *testing.T) {
} `json:"cookieBannerTranslation"`
} `json:"upsertCookieBannerTranslation"`
}
err = owner.Execute(query, input, &result2)
require.NoError(t, err)
assert.Equal(t, firstID, result2.UpsertCookieBannerTranslation.CookieBannerTranslation.ID)

View File

@@ -166,6 +166,7 @@ func TestCookieBannerVersioning_NoOpUpdates(t *testing.T) {
`
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"cookieBannerId": bannerID,
@@ -238,6 +239,7 @@ func TestCookieBannerVersioning_NoOpUpdates(t *testing.T) {
`
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"cookieCategoryId": categoryID,
@@ -273,6 +275,7 @@ func TestCookieBannerVersioning_NoOpUpdates(t *testing.T) {
`
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"cookieCategoryId": categoryID,
@@ -308,6 +311,7 @@ func TestCookieBannerVersioning_NoOpUpdates(t *testing.T) {
`
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"cookieCategoryId": categoryID,
@@ -342,6 +346,7 @@ func TestCookieBannerVersioning_NoOpUpdates(t *testing.T) {
`
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"trackerPatternId": patternID,
@@ -424,6 +429,7 @@ func TestCookieBannerVersioning_ExcludedPattern(t *testing.T) {
`
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{"trackerPatternId": patternID},
}, &result)
@@ -456,6 +462,7 @@ func TestCookieBannerVersioning_ExcludedPattern(t *testing.T) {
`
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"trackerPatternId": patternID,
@@ -531,6 +538,7 @@ func reportDetectedCookies(t *testing.T, c *testutil.Client, bannerID string, na
Name string `json:"name"`
Source string `json:"source"`
}
cookies := make([]entry, len(names))
for i, n := range names {
cookies[i] = entry{Name: n, Source: "script"}
@@ -542,7 +550,9 @@ func reportDetectedCookies(t *testing.T, c *testutil.Client, bannerID string, na
endpoint := fmt.Sprintf("%s/api/cookie-banner/v1/%s/report", c.BaseURL(), bannerID)
resp, err := c.HTTPClient().Post(endpoint, "application/json", bytes.NewReader(body))
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
require.Equal(t, http.StatusNoContent, resp.StatusCode,
"report endpoint should return 204")
}
@@ -584,7 +594,9 @@ func TestCookieBannerVersioning_RealChangesStillBumpVersion(t *testing.T) {
updateCookieBanner(input: $input) { cookieBanner { id } }
}
`
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"cookieBannerId": bannerID,
@@ -617,7 +629,9 @@ func TestCookieBannerVersioning_RealChangesStillBumpVersion(t *testing.T) {
updateTrackerPattern(input: $input) { trackerPattern { id } }
}
`
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"trackerPatternId": patternID,

View File

@@ -89,6 +89,7 @@ func TestCookieCategory_Create(t *testing.T) {
}, &result)
require.NoError(t, err)
node := result.CreateCookieCategory.CookieCategoryEdge.Node
assert.NotEmpty(t, node.ID)
assert.Equal(t, "Marketing", node.Name)
@@ -343,12 +344,14 @@ func TestCookieCategory_Delete(t *testing.T) {
require.NoError(t, err)
var necessaryCategoryID string
for _, e := range listResult.Node.ConsentCategories.Edges {
if e.Node.Kind == "NECESSARY" {
necessaryCategoryID = e.Node.ID
break
}
}
require.NotEmpty(t, necessaryCategoryID, "should find a NECESSARY category")
_, err = owner.Do(`

View File

@@ -242,6 +242,7 @@ func TestDatum_PublishDataList(t *testing.T) {
ver1Major := result1.PublishDataList.DocumentVersionEdge.Node.Major
ver2Major := result2.PublishDataList.DocumentVersionEdge.Node.Major
assert.Equal(t, 1, ver1Major)
assert.Equal(t, 2, ver2Major)
},

View File

@@ -249,9 +249,11 @@ func TestDatum_Create_Validation(t *testing.T) {
if !tt.skipOrganization {
input["organizationId"] = owner.GetOrganizationID().String()
}
if !tt.skipOwner {
input["ownerId"] = profileID
}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})
@@ -348,6 +350,7 @@ func TestDatum_Update(t *testing.T) {
require.NoError(t, err)
datum := result.UpdateDatum.Datum
switch tt.assertField {
case "name":
assert.Equal(t, tt.assertValue, datum.Name)
@@ -1159,6 +1162,7 @@ func TestDatum_Pagination(t *testing.T) {
assert.GreaterOrEqual(t, result.Node.Data.TotalCount, 5)
testutil.AssertHasMorePages(t, result.Node.Data.PageInfo)
queryAfter := `
query($id: ID!, $after: CursorKey) {
node(id: $id) {
@@ -1343,7 +1347,6 @@ func TestDatum_TenantIsolation(t *testing.T) {
err := org2Owner.Execute(query, map[string]any{
"id": org1Owner.GetOrganizationID().String(),
}, &result)
if err == nil {
for _, edge := range result.Node.Data.Edges {
assert.NotEqual(t, datumID, edge.Node.ID, "Should not see datum from another org")
@@ -1404,6 +1407,7 @@ func TestDatum_Ordering(t *testing.T) {
for i, edge := range result.Node.Data.Edges {
times[i] = edge.Node.CreatedAt
}
testutil.AssertTimesOrderedDescending(t, times, "createdAt")
})
}

View File

@@ -276,6 +276,7 @@ func TestDocument_Create_Validation(t *testing.T) {
if !tt.skipOrganization {
input["organizationId"] = owner.GetOrganizationID().String()
}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})
@@ -1111,6 +1112,7 @@ func TestDocument_Pagination(t *testing.T) {
assert.GreaterOrEqual(t, result.Node.Documents.TotalCount, 5)
testutil.AssertHasMorePages(t, result.Node.Documents.PageInfo)
queryAfter := `
query($id: ID!, $after: CursorKey) {
node(id: $id) {
@@ -1288,7 +1290,6 @@ func TestDocument_TenantIsolation(t *testing.T) {
err := org2Owner.Execute(query, map[string]any{
"id": org1Owner.GetOrganizationID().String(),
}, &result)
if err == nil {
for _, edge := range result.Node.Documents.Edges {
assert.NotEqual(t, documentID, edge.Node.ID, "Should not see document from another org")
@@ -1348,6 +1349,7 @@ func TestDocument_Ordering(t *testing.T) {
for i, edge := range result.Node.Documents.Edges {
times[i] = edge.Node.CreatedAt
}
testutil.AssertTimesOrderedDescending(t, times, "createdAt")
})
}

View File

@@ -84,6 +84,7 @@ func createTestDocument(t *testing.T, owner *testutil.Client) (docID string, doc
if len(result.CreateDocument.DocumentEdge.Node.Versions.Edges) > 0 {
docVersionID = result.CreateDocument.DocumentEdge.Node.Versions.Edges[0].Node.ID
}
return docID, docVersionID
}
@@ -502,6 +503,7 @@ func TestDocumentVersion_BulkPublish(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 2, len(result.BulkPublishDocuments.DocumentVersions))
for _, dv := range result.BulkPublishDocuments.DocumentVersions {
assert.Equal(t, "PUBLISHED", dv.Status)
}
@@ -720,6 +722,7 @@ func TestDocumentVersion_BulkRequestSignatures(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 2, len(result.BulkRequestSignatures.DocumentVersionSignatureEdges))
for _, edge := range result.BulkRequestSignatures.DocumentVersionSignatureEdges {
assert.Equal(t, "REQUESTED", edge.Node.State)
}
@@ -1028,6 +1031,7 @@ func TestDocumentVersion_VoidApproval(t *testing.T) {
require.NotEmpty(t, quorumResult.Node.Versions.Edges[0].Node.ApprovalQuorums.Edges)
decisions := quorumResult.Node.Versions.Edges[0].Node.ApprovalQuorums.Edges[0].Node.Decisions.Edges
require.NotEmpty(t, decisions)
for _, d := range decisions {
assert.Equal(t, "VOIDED", d.Node.State, "decisions should be VOIDED after voiding")
}
@@ -1382,6 +1386,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) {
}
}
`
var updateResult struct {
UpdateDocument struct {
Document struct {
@@ -1393,6 +1398,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) {
} `json:"documentVersion"`
} `json:"updateDocument"`
}
err := owner.Execute(updateQuery, map[string]any{
"input": map[string]any{
"id": docID,
@@ -1411,6 +1417,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) {
} `json:"document"`
} `json:"deleteDocumentDraft"`
}
err = owner.Execute(query, map[string]any{
"input": map[string]any{"documentId": docID},
}, &result)
@@ -1427,6 +1434,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) {
docID, _ := createTestDocument(t, owner)
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{"documentId": docID},
}, &result)
@@ -1443,6 +1451,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) {
approveTestDocument(t, owner, docID)
var result struct{}
err := owner.Execute(query, map[string]any{
"input": map[string]any{"documentId": docID},
}, &result)
@@ -1469,6 +1478,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) {
}
}
`
var updateResult struct {
UpdateDocument struct {
Document struct {
@@ -1479,6 +1489,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) {
} `json:"documentVersion"`
} `json:"updateDocument"`
}
err := owner.Execute(updateQuery, map[string]any{
"input": map[string]any{
"id": docID,
@@ -1488,6 +1499,7 @@ func TestDocumentVersion_DeleteDraft(t *testing.T) {
require.NoError(t, err)
var result struct{}
err = viewer.Execute(query, map[string]any{
"input": map[string]any{"documentId": docID},
}, &result)

View File

@@ -505,12 +505,14 @@ func TestEmployeeDocument_FilterModeIsolation(t *testing.T) {
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")
})
@@ -613,12 +615,14 @@ func TestEmployeeDocument_ApproverFilterModeIsolation(t *testing.T) {
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")
})

View File

@@ -237,6 +237,7 @@ func TestFinding_PublishFindingList(t *testing.T) {
ver1Major := result1.PublishFindingList.DocumentVersionEdge.Node.Major
ver2Major := result2.PublishFindingList.DocumentVersionEdge.Node.Major
assert.Equal(t, 1, ver1Major)
assert.Equal(t, 2, ver2Major)
},

View File

@@ -186,6 +186,7 @@ func TestFinding_Update(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
findingID := createResult.CreateFinding.FindingEdge.Node.ID
query := `
@@ -274,6 +275,7 @@ func TestFinding_Delete(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
findingID := createResult.CreateFinding.FindingEdge.Node.ID
query := `
@@ -510,6 +512,7 @@ func TestFinding_CreateAuditMapping(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
findingID := createResult.CreateFinding.FindingEdge.Node.ID
// Link finding to audit
@@ -635,6 +638,7 @@ func TestFinding_DeleteAuditMapping(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
findingID := createResult.CreateFinding.FindingEdge.Node.ID
// Link finding to audit
@@ -753,6 +757,7 @@ func TestFinding_StatusAndPriorityValues(t *testing.T) {
"status values",
func(t *testing.T) {
t.Parallel()
riskID := factory.CreateRisk(owner)
statuses := []string{"OPEN", "IN_PROGRESS", "CLOSED", "RISK_ACCEPTED", "MITIGATED", "FALSE_POSITIVE"}
@@ -761,6 +766,7 @@ func TestFinding_StatusAndPriorityValues(t *testing.T) {
status,
func(t *testing.T) {
t.Parallel()
var result struct {
CreateFinding struct {
FindingEdge struct {
@@ -799,6 +805,7 @@ func TestFinding_StatusAndPriorityValues(t *testing.T) {
"priority values",
func(t *testing.T) {
t.Parallel()
priorities := []string{"LOW", "MEDIUM", "HIGH"}
for _, priority := range priorities {
@@ -806,6 +813,7 @@ func TestFinding_StatusAndPriorityValues(t *testing.T) {
priority,
func(t *testing.T) {
t.Parallel()
var result struct {
CreateFinding struct {
FindingEdge struct {

View File

@@ -215,6 +215,7 @@ func TestFramework_Create_Validation(t *testing.T) {
if !tt.skipOrganization {
input["organizationId"] = owner.GetOrganizationID().String()
}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})
@@ -301,6 +302,7 @@ func TestFramework_Update(t *testing.T) {
require.NoError(t, err)
framework := result.UpdateFramework.Framework
switch tt.assertField {
case "name":
assert.Equal(t, tt.assertValue, framework.Name)
@@ -637,6 +639,7 @@ func TestFramework_Timestamps(t *testing.T) {
t.Run("updatedAt changes on update", func(t *testing.T) {
t.Skip("Skipped: server may not update timestamp immediately or has caching")
frameworkID := factory.NewFramework(owner).WithName("Timestamp Update Test").Create()
getQuery := `
@@ -1216,6 +1219,7 @@ func TestFramework_SubResolvers_WithData(t *testing.T) {
for i, edge := range result.Node.Controls.Edges {
controlIDs[i] = edge.Node.ID
}
assert.Contains(t, controlIDs, control1ID)
assert.Contains(t, controlIDs, control2ID)
})
@@ -1281,6 +1285,7 @@ func TestFramework_Pagination(t *testing.T) {
assert.GreaterOrEqual(t, result.Node.Frameworks.TotalCount, 5)
testutil.AssertHasMorePages(t, result.Node.Frameworks.PageInfo)
queryAfter := `
query($id: ID!, $after: CursorKey) {
node(id: $id) {
@@ -1464,7 +1469,6 @@ func TestFramework_TenantIsolation(t *testing.T) {
err := org2Owner.Execute(query, map[string]any{
"id": org1Owner.GetOrganizationID().String(),
}, &result)
if err == nil {
for _, edge := range result.Node.Frameworks.Edges {
assert.NotEqual(t, frameworkID, edge.Node.ID, "Should not see framework from another org")
@@ -1524,6 +1528,7 @@ func TestFramework_Ordering(t *testing.T) {
for i, edge := range result.Node.Frameworks.Edges {
times[i] = edge.Node.CreatedAt
}
testutil.AssertTimesOrderedDescending(t, times, "createdAt")
})
}

View File

@@ -23,7 +23,9 @@ import (
func TestMain(m *testing.M) {
testutil.Setup()
code := m.Run()
testutil.Teardown()
os.Exit(code)
}

View File

@@ -37,6 +37,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) {
} `json:"frameworkEdge"`
} `json:"createFramework"`
}
err := owner.Execute(`
mutation($input: CreateFrameworkInput!) {
createFramework(input: $input) {
@@ -54,6 +55,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) {
},
}, &createFrameworkResult)
require.NoError(t, err)
frameworkID := createFrameworkResult.CreateFramework.FrameworkEdge.Node.ID
// Create a control
@@ -66,6 +68,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) {
} `json:"controlEdge"`
} `json:"createControl"`
}
err = owner.Execute(`
mutation($input: CreateControlInput!) {
createControl(input: $input) {
@@ -87,6 +90,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) {
},
}, &createControlResult)
require.NoError(t, err)
controlID := createControlResult.CreateControl.ControlEdge.Node.ID
// Create a measure
@@ -99,6 +103,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) {
} `json:"measureEdge"`
} `json:"createMeasure"`
}
err = owner.Execute(`
mutation($input: CreateMeasureInput!) {
createMeasure(input: $input) {
@@ -117,6 +122,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) {
},
}, &createMeasureResult)
require.NoError(t, err)
measureID := createMeasureResult.CreateMeasure.MeasureEdge.Node.ID
t.Run("create mapping", func(t *testing.T) {
@@ -134,6 +140,7 @@ func TestControlMeasureMapping_CreateDelete(t *testing.T) {
} `json:"measureEdge"`
} `json:"createControlMeasureMapping"`
}
err := owner.Execute(`
mutation($input: CreateControlMeasureMappingInput!) {
createControlMeasureMapping(input: $input) {
@@ -192,6 +199,7 @@ func TestRiskMeasureMapping_CreateDelete(t *testing.T) {
} `json:"riskEdge"`
} `json:"createRisk"`
}
err := owner.Execute(`
mutation($input: CreateRiskInput!) {
createRisk(input: $input) {
@@ -213,6 +221,7 @@ func TestRiskMeasureMapping_CreateDelete(t *testing.T) {
},
}, &createRiskResult)
require.NoError(t, err)
riskID := createRiskResult.CreateRisk.RiskEdge.Node.ID
// Create a measure
@@ -225,6 +234,7 @@ func TestRiskMeasureMapping_CreateDelete(t *testing.T) {
} `json:"measureEdge"`
} `json:"createMeasure"`
}
err = owner.Execute(`
mutation($input: CreateMeasureInput!) {
createMeasure(input: $input) {
@@ -243,6 +253,7 @@ func TestRiskMeasureMapping_CreateDelete(t *testing.T) {
},
}, &createMeasureResult)
require.NoError(t, err)
measureID := createMeasureResult.CreateMeasure.MeasureEdge.Node.ID
t.Run("create mapping", func(t *testing.T) {
@@ -260,6 +271,7 @@ func TestRiskMeasureMapping_CreateDelete(t *testing.T) {
} `json:"measureEdge"`
} `json:"createRiskMeasureMapping"`
}
err := owner.Execute(`
mutation($input: CreateRiskMeasureMappingInput!) {
createRiskMeasureMapping(input: $input) {
@@ -318,6 +330,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) {
} `json:"frameworkEdge"`
} `json:"createFramework"`
}
err := owner.Execute(`
mutation($input: CreateFrameworkInput!) {
createFramework(input: $input) {
@@ -335,6 +348,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) {
},
}, &createFrameworkResult)
require.NoError(t, err)
frameworkID := createFrameworkResult.CreateFramework.FrameworkEdge.Node.ID
var createControlResult struct {
@@ -346,6 +360,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) {
} `json:"controlEdge"`
} `json:"createControl"`
}
err = owner.Execute(`
mutation($input: CreateControlInput!) {
createControl(input: $input) {
@@ -367,6 +382,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) {
},
}, &createControlResult)
require.NoError(t, err)
controlID := createControlResult.CreateControl.ControlEdge.Node.ID
// Create a document
@@ -379,6 +395,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) {
} `json:"documentEdge"`
} `json:"createDocument"`
}
err = owner.Execute(`
mutation($input: CreateDocumentInput!) {
createDocument(input: $input) {
@@ -399,6 +416,7 @@ func TestControlDocumentMapping_CreateDelete(t *testing.T) {
},
}, &createDocumentResult)
require.NoError(t, err)
documentID := createDocumentResult.CreateDocument.DocumentEdge.Node.ID
t.Run("create mapping", func(t *testing.T) {
@@ -458,6 +476,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) {
} `json:"frameworkEdge"`
} `json:"createFramework"`
}
err := owner.Execute(`
mutation($input: CreateFrameworkInput!) {
createFramework(input: $input) {
@@ -475,6 +494,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) {
},
}, &createFrameworkResult)
require.NoError(t, err)
frameworkID := createFrameworkResult.CreateFramework.FrameworkEdge.Node.ID
var createControlResult struct {
@@ -486,6 +506,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) {
} `json:"controlEdge"`
} `json:"createControl"`
}
err = owner.Execute(`
mutation($input: CreateControlInput!) {
createControl(input: $input) {
@@ -507,6 +528,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) {
},
}, &createControlResult)
require.NoError(t, err)
controlID := createControlResult.CreateControl.ControlEdge.Node.ID
// Create an audit
@@ -519,6 +541,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) {
} `json:"auditEdge"`
} `json:"createAudit"`
}
err = owner.Execute(`
mutation($input: CreateAuditInput!) {
createAudit(input: $input) {
@@ -537,6 +560,7 @@ func TestControlAuditMapping_CreateDelete(t *testing.T) {
},
}, &createAuditResult)
require.NoError(t, err)
auditID := createAuditResult.CreateAudit.AuditEdge.Node.ID
t.Run("create mapping", func(t *testing.T) {
@@ -596,6 +620,7 @@ func TestRiskDocumentMapping_CreateDelete(t *testing.T) {
} `json:"riskEdge"`
} `json:"createRisk"`
}
err := owner.Execute(`
mutation($input: CreateRiskInput!) {
createRisk(input: $input) {
@@ -617,6 +642,7 @@ func TestRiskDocumentMapping_CreateDelete(t *testing.T) {
},
}, &createRiskResult)
require.NoError(t, err)
riskID := createRiskResult.CreateRisk.RiskEdge.Node.ID
// Create a document
@@ -629,6 +655,7 @@ func TestRiskDocumentMapping_CreateDelete(t *testing.T) {
} `json:"documentEdge"`
} `json:"createDocument"`
}
err = owner.Execute(`
mutation($input: CreateDocumentInput!) {
createDocument(input: $input) {
@@ -649,6 +676,7 @@ func TestRiskDocumentMapping_CreateDelete(t *testing.T) {
},
}, &createDocumentResult)
require.NoError(t, err)
documentID := createDocumentResult.CreateDocument.DocumentEdge.Node.ID
t.Run("create mapping", func(t *testing.T) {
@@ -708,6 +736,7 @@ func TestRiskObligationMapping_CreateDelete(t *testing.T) {
} `json:"riskEdge"`
} `json:"createRisk"`
}
err := owner.Execute(`
mutation($input: CreateRiskInput!) {
createRisk(input: $input) {
@@ -729,10 +758,12 @@ func TestRiskObligationMapping_CreateDelete(t *testing.T) {
},
}, &createRiskResult)
require.NoError(t, err)
riskID := createRiskResult.CreateRisk.RiskEdge.Node.ID
// Create an obligation
profileID := factory.CreateUser(owner)
var createObligationResult struct {
CreateObligation struct {
ObligationEdge struct {
@@ -742,6 +773,7 @@ func TestRiskObligationMapping_CreateDelete(t *testing.T) {
} `json:"obligationEdge"`
} `json:"createObligation"`
}
err = owner.Execute(`
mutation($input: CreateObligationInput!) {
createObligation(input: $input) {
@@ -763,6 +795,7 @@ func TestRiskObligationMapping_CreateDelete(t *testing.T) {
},
}, &createObligationResult)
require.NoError(t, err)
obligationID := createObligationResult.CreateObligation.ObligationEdge.Node.ID
t.Run("create mapping", func(t *testing.T) {
@@ -815,6 +848,7 @@ func TestMeasureDocumentMapping_CreateDelete(t *testing.T) {
t.Run("create mapping", func(t *testing.T) {
t.Parallel()
documentID := factory.NewDocument(owner).Create()
var result struct {
@@ -831,6 +865,7 @@ func TestMeasureDocumentMapping_CreateDelete(t *testing.T) {
} `json:"documentEdge"`
} `json:"createMeasureDocumentMapping"`
}
err := owner.Execute(`
mutation($input: CreateMeasureDocumentMappingInput!) {
createMeasureDocumentMapping(input: $input) {
@@ -859,6 +894,7 @@ func TestMeasureDocumentMapping_CreateDelete(t *testing.T) {
t.Run("delete mapping", func(t *testing.T) {
t.Parallel()
documentID := factory.NewDocument(owner).Create()
// Create the mapping first

View File

@@ -300,6 +300,7 @@ func TestMeasure_Create_Validation(t *testing.T) {
if !tt.skipOrganization {
input["organizationId"] = owner.GetOrganizationID().String()
}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})
@@ -403,6 +404,7 @@ func TestMeasure_Update(t *testing.T) {
require.NoError(t, err)
measure := result.UpdateMeasure.Measure
switch tt.assertField {
case "name":
assert.Equal(t, tt.assertValue, measure.Name)
@@ -1349,6 +1351,7 @@ func TestMeasure_SubResolvers_WithData(t *testing.T) {
for i, edge := range result.Node.Tasks.Edges {
taskIDs[i] = edge.Node.ID
}
assert.Contains(t, taskIDs, task1ID)
assert.Contains(t, taskIDs, task2ID)
})
@@ -1543,6 +1546,7 @@ func TestMeasure_Pagination(t *testing.T) {
// Get next page using cursor
testutil.AssertHasMorePages(t, result.Node.Measures.PageInfo)
queryAfter := `
query($id: ID!, $after: CursorKey) {
node(id: $id) {
@@ -1698,12 +1702,14 @@ func TestMeasure_Filtering(t *testing.T) {
// 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")
})
@@ -1749,6 +1755,7 @@ func TestMeasure_Filtering(t *testing.T) {
for i, edge := range result.Node.Measures.Edges {
foundIDs[i] = edge.Node.ID
}
assert.Contains(t, foundIDs, measure1ID)
assert.Contains(t, foundIDs, measure2ID)
})
@@ -1804,17 +1811,20 @@ func TestMeasure_FilterByCategory(t *testing.T) {
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")
})
@@ -2127,6 +2137,7 @@ func TestMeasure_Ordering(t *testing.T) {
for i, edge := range result.Node.Measures.Edges {
names[i] = edge.Node.Name
}
testutil.AssertOrderedAscending(t, names, "name")
})
@@ -2175,6 +2186,7 @@ func TestMeasure_Ordering(t *testing.T) {
for i, edge := range result.Node.Measures.Edges {
names[i] = edge.Node.Name
}
testutil.AssertOrderedDescending(t, names, "name")
})
@@ -2223,6 +2235,7 @@ func TestMeasure_Ordering(t *testing.T) {
for i, edge := range result.Node.Measures.Edges {
times[i] = edge.Node.CreatedAt
}
testutil.AssertTimesOrderedDescending(t, times, "createdAt")
})
}

View File

@@ -314,6 +314,7 @@ func TestOAuth2_AuthorizationCodeFlow(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -365,6 +366,7 @@ func TestOAuth2_AuthorizationCodeFlow(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -1564,6 +1566,7 @@ func TestOAuth2_Security(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -1617,6 +1620,7 @@ func TestOAuth2_Security(t *testing.T) {
require.NoError(t, err)
var redirectLoc string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -1696,6 +1700,7 @@ func TestOAuth2_Security(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -1822,6 +1827,7 @@ func TestOAuth2_Security(t *testing.T) {
require.NoError(t, err)
_, challenge2 := testutil.GeneratePKCE()
params.Set("state", "consent-second")
params.Set("code_challenge", challenge2)
@@ -1862,6 +1868,7 @@ func TestOAuth2_Security(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -2003,12 +2010,14 @@ func TestOAuth2_Security(t *testing.T) {
require.NotNil(t, jwks)
var matchingKey map[string]any
for _, k := range jwks.Keys {
if kid, ok := k["kid"].(string); ok && kid == header.Kid {
matchingKey = k
break
}
}
require.NotNil(t, matchingKey, "JWKS must contain key matching kid=%s", header.Kid)
// Reconstruct the RSA public key from JWK.
@@ -2157,6 +2166,7 @@ func TestOAuth2_Security(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -2222,6 +2232,7 @@ func TestOAuth2_ClientSecretPost(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -2309,6 +2320,7 @@ func TestOAuth2_PublicClientAuthCodeFlow(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -2486,6 +2498,7 @@ func TestOAuth2_IDTokenClaims(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -2585,6 +2598,7 @@ func TestOAuth2_CacheHeaders(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -2825,6 +2839,7 @@ func TestOAuth2_Expiry(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -3010,6 +3025,7 @@ func TestOAuth2_OfflineAccessScope(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -3065,6 +3081,7 @@ func TestOAuth2_OfflineAccessScope(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)
@@ -3470,6 +3487,7 @@ func TestOAuth2_AuthorizationCodeReplayRevokesTokens(t *testing.T) {
require.NoError(t, err)
var code string
if testutil.IsConsentRedirect(authResp) {
consentID, err := testutil.ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)

View File

@@ -237,6 +237,7 @@ func TestObligation_PublishObligationList(t *testing.T) {
ver1Major := result1.PublishObligationList.DocumentVersionEdge.Node.Major
ver2Major := result2.PublishObligationList.DocumentVersionEdge.Node.Major
assert.Equal(t, 1, ver1Major)
assert.Equal(t, 2, ver2Major)
},

View File

@@ -192,6 +192,7 @@ func TestProcessingActivity_Create_Validation(t *testing.T) {
if !tt.skipOrganization {
input["organizationId"] = owner.GetOrganizationID().String()
}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})
@@ -290,6 +291,7 @@ func TestProcessingActivity_Update(t *testing.T) {
require.NoError(t, err)
pa := result.UpdateProcessingActivity.ProcessingActivity
switch tt.assertField {
case "name":
assert.Equal(t, tt.assertValue, pa.Name)
@@ -954,6 +956,7 @@ func TestProcessingActivity_Pagination(t *testing.T) {
assert.GreaterOrEqual(t, result.Node.ProcessingActivities.TotalCount, 5)
testutil.AssertHasMorePages(t, result.Node.ProcessingActivities.PageInfo)
queryAfter := `
query($id: ID!, $after: CursorKey) {
node(id: $id) {
@@ -1137,7 +1140,6 @@ func TestProcessingActivity_TenantIsolation(t *testing.T) {
err := org2Owner.Execute(query, map[string]any{
"id": org1Owner.GetOrganizationID().String(),
}, &result)
if err == nil {
for _, edge := range result.Node.ProcessingActivities.Edges {
assert.NotEqual(t, paID, edge.Node.ID, "Should not see processing activity from another org")
@@ -1197,6 +1199,7 @@ func TestProcessingActivity_Ordering(t *testing.T) {
for i, edge := range result.Node.ProcessingActivities.Edges {
times[i] = edge.Node.CreatedAt
}
testutil.AssertTimesOrderedDescending(t, times, "createdAt")
})
}
@@ -1533,6 +1536,7 @@ func TestProcessingActivity_DPIA(t *testing.T) {
}
}
`
var createResult struct {
CreateDataProtectionImpactAssessment struct {
DataProtectionImpactAssessment struct {
@@ -1540,6 +1544,7 @@ func TestProcessingActivity_DPIA(t *testing.T) {
} `json:"dataProtectionImpactAssessment"`
} `json:"createDataProtectionImpactAssessment"`
}
err := owner.Execute(createQuery, map[string]any{
"input": map[string]any{
"processingActivityId": paID,
@@ -1548,6 +1553,7 @@ func TestProcessingActivity_DPIA(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
dpiaID := createResult.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessment.ID
updateQuery := `
@@ -1596,6 +1602,7 @@ func TestProcessingActivity_DPIA(t *testing.T) {
}
}
`
var createResult struct {
CreateDataProtectionImpactAssessment struct {
DataProtectionImpactAssessment struct {
@@ -1603,6 +1610,7 @@ func TestProcessingActivity_DPIA(t *testing.T) {
} `json:"dataProtectionImpactAssessment"`
} `json:"createDataProtectionImpactAssessment"`
}
err := owner.Execute(createQuery, map[string]any{
"input": map[string]any{
"processingActivityId": paID,
@@ -1610,6 +1618,7 @@ func TestProcessingActivity_DPIA(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
dpiaID := createResult.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessment.ID
deleteQuery := `
@@ -1643,6 +1652,7 @@ func TestProcessingActivity_DPIA(t *testing.T) {
}
}
`
var readResult struct {
Node struct {
DataProtectionImpactAssessment *struct {
@@ -1650,6 +1660,7 @@ func TestProcessingActivity_DPIA(t *testing.T) {
} `json:"dataProtectionImpactAssessment"`
} `json:"node"`
}
err = owner.Execute(readQuery, map[string]any{"id": paID}, &readResult)
require.NoError(t, err)
assert.Nil(t, readResult.Node.DataProtectionImpactAssessment)
@@ -1816,6 +1827,7 @@ func TestProcessingActivity_TIA(t *testing.T) {
}
}
`
var createResult struct {
CreateTransferImpactAssessment struct {
TransferImpactAssessment struct {
@@ -1823,6 +1835,7 @@ func TestProcessingActivity_TIA(t *testing.T) {
} `json:"transferImpactAssessment"`
} `json:"createTransferImpactAssessment"`
}
err := owner.Execute(createQuery, map[string]any{
"input": map[string]any{
"processingActivityId": paID,
@@ -1831,6 +1844,7 @@ func TestProcessingActivity_TIA(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
tiaID := createResult.CreateTransferImpactAssessment.TransferImpactAssessment.ID
updateQuery := `
@@ -1883,6 +1897,7 @@ func TestProcessingActivity_TIA(t *testing.T) {
}
}
`
var createResult struct {
CreateTransferImpactAssessment struct {
TransferImpactAssessment struct {
@@ -1890,6 +1905,7 @@ func TestProcessingActivity_TIA(t *testing.T) {
} `json:"transferImpactAssessment"`
} `json:"createTransferImpactAssessment"`
}
err := owner.Execute(createQuery, map[string]any{
"input": map[string]any{
"processingActivityId": paID,
@@ -1897,6 +1913,7 @@ func TestProcessingActivity_TIA(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
tiaID := createResult.CreateTransferImpactAssessment.TransferImpactAssessment.ID
deleteQuery := `
@@ -1930,6 +1947,7 @@ func TestProcessingActivity_TIA(t *testing.T) {
}
}
`
var readResult struct {
Node struct {
TransferImpactAssessment *struct {
@@ -1937,6 +1955,7 @@ func TestProcessingActivity_TIA(t *testing.T) {
} `json:"transferImpactAssessment"`
} `json:"node"`
}
err = owner.Execute(readQuery, map[string]any{"id": paID}, &readResult)
require.NoError(t, err)
assert.Nil(t, readResult.Node.TransferImpactAssessment)
@@ -1957,6 +1976,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) {
} `json:"dataProtectionImpactAssessment"`
} `json:"createDataProtectionImpactAssessment"`
}
err := owner.Execute(`
mutation($input: CreateDataProtectionImpactAssessmentInput!) {
createDataProtectionImpactAssessment(input: $input) {
@@ -1970,6 +1990,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) {
},
}, &createResult)
require.NoError(t, err, "owner should be able to create DPIA")
dpiaID := createResult.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessment.ID
_, err = owner.Do(`
@@ -2012,6 +2033,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) {
} `json:"dataProtectionImpactAssessment"`
} `json:"createDataProtectionImpactAssessment"`
}
err := admin.Execute(`
mutation($input: CreateDataProtectionImpactAssessmentInput!) {
createDataProtectionImpactAssessment(input: $input) {
@@ -2025,6 +2047,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) {
},
}, &createResult)
require.NoError(t, err, "admin should be able to create DPIA")
dpiaID := createResult.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessment.ID
_, err = admin.Do(`
@@ -2081,6 +2104,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) {
} `json:"dataProtectionImpactAssessment"`
} `json:"createDataProtectionImpactAssessment"`
}
err = owner.Execute(`
mutation($input: CreateDataProtectionImpactAssessmentInput!) {
createDataProtectionImpactAssessment(input: $input) {
@@ -2094,6 +2118,7 @@ func TestProcessingActivity_DPIA_RBAC(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
dpiaID := createResult.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessment.ID
_, err = viewer.Do(`
@@ -2139,6 +2164,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) {
} `json:"transferImpactAssessment"`
} `json:"createTransferImpactAssessment"`
}
err := owner.Execute(`
mutation($input: CreateTransferImpactAssessmentInput!) {
createTransferImpactAssessment(input: $input) {
@@ -2152,6 +2178,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) {
},
}, &createResult)
require.NoError(t, err, "owner should be able to create TIA")
tiaID := createResult.CreateTransferImpactAssessment.TransferImpactAssessment.ID
_, err = owner.Do(`
@@ -2194,6 +2221,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) {
} `json:"transferImpactAssessment"`
} `json:"createTransferImpactAssessment"`
}
err := admin.Execute(`
mutation($input: CreateTransferImpactAssessmentInput!) {
createTransferImpactAssessment(input: $input) {
@@ -2207,6 +2235,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) {
},
}, &createResult)
require.NoError(t, err, "admin should be able to create TIA")
tiaID := createResult.CreateTransferImpactAssessment.TransferImpactAssessment.ID
_, err = admin.Do(`
@@ -2263,6 +2292,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) {
} `json:"transferImpactAssessment"`
} `json:"createTransferImpactAssessment"`
}
err = owner.Execute(`
mutation($input: CreateTransferImpactAssessmentInput!) {
createTransferImpactAssessment(input: $input) {
@@ -2276,6 +2306,7 @@ func TestProcessingActivity_TIA_RBAC(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
tiaID := createResult.CreateTransferImpactAssessment.TransferImpactAssessment.ID
_, err = viewer.Do(`

View File

@@ -120,6 +120,7 @@ func TestRightsRequest_Update(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
rrID := createResult.CreateRightsRequest.RightsRequestEdge.Node.ID
query := `
@@ -208,6 +209,7 @@ func TestRightsRequest_Delete(t *testing.T) {
},
}, &createResult)
require.NoError(t, err)
rrID := createResult.CreateRightsRequest.RightsRequestEdge.Node.ID
query := `

View File

@@ -402,6 +402,7 @@ func TestRisk_RequiredFields(t *testing.T) {
if !tt.skipOrganization {
input["organizationId"] = owner.GetOrganizationID().String()
}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})

View File

@@ -112,9 +112,11 @@ func (sc *scimClient) doRequest(method, path string, payload any) (string, int)
sc.t.Helper()
var body io.Reader
if payload != nil {
data, err := json.Marshal(payload)
require.NoError(sc.t, err)
body = bytes.NewReader(data)
}
@@ -122,12 +124,14 @@ func (sc *scimClient) doRequest(method, path string, payload any) (string, int)
require.NoError(sc.t, err)
req.Header.Set("Authorization", "Bearer "+sc.token)
if payload != nil {
req.Header.Set("Content-Type", "application/scim+json")
}
resp, err := sc.client.Do(req)
require.NoError(sc.t, err)
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)
@@ -269,6 +273,7 @@ func TestSCIM_Unauthorized(t *testing.T) {
resp, err := client.Do(req)
require.NoError(t, err)
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)

View File

@@ -75,12 +75,12 @@ func TestStatementOfApplicability_Create(t *testing.T) {
)
require.NoError(t, err)
node := result.CreateStatementOfApplicability.StatementOfApplicabilityEdge.Node
assert.NotEmpty(t, node.ID)
assert.Equal(t, name, node.Name)
},
)
}
func TestStatementOfApplicability_CreateDocument(t *testing.T) {
@@ -310,6 +310,7 @@ func TestStatementOfApplicability_CreateDocument(t *testing.T) {
ver1Major := result1.PublishStatementOfApplicability.DocumentVersionEdge.Node.Major
ver2Major := result2.PublishStatementOfApplicability.DocumentVersionEdge.Node.Major
assert.Equal(t, 1, ver1Major)
assert.Equal(t, 2, ver2Major)
},
@@ -533,6 +534,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) {
documentID, publishedVersionID := publishSOADocument(t)
var result updateResult
err := owner.Execute(
updateQuery,
map[string]any{
@@ -620,6 +622,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) {
}
var firstPublish publishSOAResult
err := owner.Execute(
publishSOAQuery,
map[string]any{
@@ -631,11 +634,13 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) {
&firstPublish,
)
require.NoError(t, err)
documentID := firstPublish.PublishStatementOfApplicability.DocumentEdge.Node.ID
require.Equal(t, "STATEMENT_OF_APPLICABILITY", firstPublish.PublishStatementOfApplicability.DocumentVersionEdge.Node.DocumentType)
require.Equal(t, "CONFIDENTIAL", firstPublish.PublishStatementOfApplicability.DocumentVersionEdge.Node.Classification)
var editResult updateResult
err = owner.Execute(
updateQuery,
map[string]any{
@@ -659,6 +664,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) {
}
}
`
var publishDraft struct {
PublishDocument struct {
DocumentVersion struct {
@@ -672,6 +678,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) {
} `json:"documentVersion"`
} `json:"publishDocument"`
}
err = owner.Execute(
publishDraftQuery,
map[string]any{
@@ -690,6 +697,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) {
require.Equal(t, "INTERNAL", publishDraft.PublishDocument.DocumentVersion.Classification)
var rePublish publishSOAResult
err = owner.Execute(
publishSOAQuery,
map[string]any{
@@ -701,6 +709,7 @@ func TestStatementOfApplicability_UpdateDocumentMetadata(t *testing.T) {
&rePublish,
)
require.NoError(t, err)
node := rePublish.PublishStatementOfApplicability.DocumentVersionEdge.Node
assert.Equal(t, "Custom SOA Name", node.Title, "re-publish should preserve edited title")
assert.Equal(t, "POLICY", node.DocumentType, "re-publish should preserve edited type")

View File

@@ -296,10 +296,12 @@ func TestTask_RequiredFields(t *testing.T) {
if !tt.skipOrganization {
input["organizationId"] = owner.GetOrganizationID().String()
}
for k, v := range tt.input {
if v == "placeholder" {
continue // Skip placeholder values
}
input[k] = v
}

View File

@@ -341,6 +341,7 @@ func TestThirdParty_RequiredFields(t *testing.T) {
if !tt.skipOrganization {
input["organizationId"] = owner.GetOrganizationID().String()
}
maps.Copy(input, tt.input)
_, err := owner.Do(query, map[string]any{"input": input})
@@ -1020,6 +1021,7 @@ func TestThirdParty_Assess(t *testing.T) {
thirdPartyID := factory.NewThirdParty(owner).WithName("Unconfigured assess").Create()
var result resultShape
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"id": thirdPartyID,
@@ -1037,6 +1039,7 @@ func TestThirdParty_Assess(t *testing.T) {
thirdPartyID := factory.NewThirdParty(owner).WithName("Admin-assessed thirdParty").Create()
var result resultShape
err := admin.Execute(query, map[string]any{
"input": map[string]any{
"id": thirdPartyID,
@@ -1054,6 +1057,7 @@ func TestThirdParty_Assess(t *testing.T) {
thirdPartyID := factory.NewThirdParty(owner).WithName("Viewer attempt").Create()
var result resultShape
err := viewer.Execute(query, map[string]any{
"input": map[string]any{
"id": thirdPartyID,
@@ -1071,6 +1075,7 @@ func TestThirdParty_Assess(t *testing.T) {
thirdPartyID := factory.NewThirdParty(org1Owner).WithName("Org1 thirdParty").Create()
var result resultShape
err := org2Owner.Execute(query, map[string]any{
"input": map[string]any{
"id": thirdPartyID,
@@ -1087,6 +1092,7 @@ func TestThirdParty_Assess(t *testing.T) {
thirdPartyID := factory.NewThirdParty(owner).WithName("Procedure test").Create()
var result resultShape
err := owner.Execute(query, map[string]any{
"input": map[string]any{
"id": thirdPartyID,

View File

@@ -91,6 +91,7 @@ func TestTrackerPattern_Create(t *testing.T) {
}, &result)
require.NoError(t, err)
node := result.CreateTrackerPattern.TrackerPatternEdge.Node
assert.NotEmpty(t, node.ID)
assert.Equal(t, "_ga", node.Pattern)
@@ -151,6 +152,7 @@ func TestTrackerPattern_Create(t *testing.T) {
}, &result)
require.NoError(t, err)
node := result.CreateTrackerPattern.TrackerPatternEdge.Node
assert.Equal(t, "_gat_*", node.Pattern)
assert.Equal(t, "GLOB", node.MatchType)

View File

@@ -85,6 +85,7 @@ func TestTrackerResource_Create(t *testing.T) {
}, &result)
require.NoError(t, err)
node := result.CreateTrackerResource.TrackerResourceEdge.Node
assert.NotEmpty(t, node.ID)
assert.Equal(t, "SCRIPT", node.Type)

View File

@@ -71,12 +71,14 @@ func TestUser_UpdateMembership(t *testing.T) {
// Find the admin
var adminMembershipID string
for _, edge := range result.Node.Profiles.Edges {
if edge.Node.Membership.Role == "ADMIN" {
adminMembershipID = edge.Node.Membership.ID
break
}
}
require.NotEmpty(t, adminMembershipID, "Should find admin member")
// Update the member role to VIEWER
@@ -162,12 +164,14 @@ func TestUser_RemoveUser(t *testing.T) {
// Find a viewer user to remove
var userID string
for _, edge := range result.Node.Profiles.Edges {
if edge.Node.Membership.Role == "VIEWER" {
userID = edge.Node.ID
break
}
}
assert.NotEmpty(t, userID, "Should find viewer member")
// Remove the member
@@ -251,12 +255,14 @@ func TestUser_RemoveOwner(t *testing.T) {
// Find the other owner (not the calling owner)
var targetProfileID string
for _, edge := range result.Node.Profiles.Edges {
if edge.Node.Membership.Role == "OWNER" && edge.Node.Identity.ID != owner.GetUserID().String() {
targetProfileID = edge.Node.ID
break
}
}
require.NotEmpty(t, targetProfileID, "Should find another owner to remove")
mutation := `

View File

@@ -42,9 +42,11 @@ func (a Attrs) get(key string, defaultVal any) any {
if a == nil {
return defaultVal
}
if v, ok := a[key]; ok {
return v
}
return defaultVal
}
@@ -52,6 +54,7 @@ func (a Attrs) getString(key string, defaultVal string) string {
if v, ok := a.get(key, defaultVal).(string); ok {
return v
}
return defaultVal
}
@@ -59,11 +62,13 @@ func (a Attrs) getStringPtr(key string) *string {
if a == nil {
return nil
}
if v, ok := a[key]; ok {
if s, ok := v.(string); ok {
return &s
}
}
return nil
}
@@ -71,6 +76,7 @@ func (a Attrs) getInt(key string, defaultVal int) int {
if a == nil {
return defaultVal
}
if v, ok := a[key]; ok {
switch val := v.(type) {
case int:
@@ -81,6 +87,7 @@ func (a Attrs) getInt(key string, defaultVal int) int {
return int(val)
}
}
return defaultVal
}
@@ -88,11 +95,13 @@ func (a Attrs) getBool(key string, defaultVal bool) bool {
if a == nil {
return defaultVal
}
if v, ok := a[key]; ok {
if b, ok := v.(bool); ok {
return b
}
}
return defaultVal
}
@@ -167,9 +176,11 @@ func CreateThirdParty(c *testutil.Client, attrs ...Attrs) string {
if desc := a.getStringPtr("description"); desc != nil {
input["description"] = *desc
}
if url := a.getStringPtr("websiteUrl"); url != nil {
input["websiteUrl"] = *url
}
if cat := a.getStringPtr("category"); cat != nil {
input["category"] = *cat
}
@@ -348,6 +359,7 @@ func CreateTask(c *testutil.Client, measureID *string, attrs ...Attrs) string {
if measureID != nil {
input["measureId"] = *measureID
}
if desc := a.getStringPtr("description"); desc != nil {
input["description"] = *desc
}
@@ -951,6 +963,7 @@ func CreateAccessSource(c *testutil.Client, organizationID string, attrs ...Attr
if csvData := a.getStringPtr("csvData"); csvData != nil {
input["csvData"] = *csvData
}
if connectorID := a.getStringPtr("connectorId"); connectorID != nil {
input["connectorId"] = *connectorID
}
@@ -1446,7 +1459,9 @@ func ReportDetectedResources(c *testutil.Client, bannerID string, count int) {
url := fmt.Sprintf("%s/api/cookie-banner/v1/%s/report", c.BaseURL(), bannerID)
resp, err := http.Post(url, "application/json", bytes.NewReader(body))
require.NoError(c.T, err, "report detected resources request failed")
defer func() { _ = resp.Body.Close() }()
require.Equal(c.T, http.StatusNoContent, resp.StatusCode, "report detected resources unexpected status")
}

View File

@@ -80,6 +80,7 @@ func AssertTimestampsOnUpdate(t *testing.T, createdAt, updatedAt, originalCreate
func AssertOptionalStringEqual(t *testing.T, expected, actual *string, fieldName string) {
t.Helper()
if expected == nil {
assert.Nil(t, actual, "%s should be nil", fieldName)
} else {
@@ -95,6 +96,7 @@ func AssertOrderedAscending[T cmp.Ordered](t *testing.T, values []T, fieldName s
func AssertOrderedDescending[T cmp.Ordered](t *testing.T, values []T, fieldName string) {
t.Helper()
reversed := slices.Clone(values)
slices.Reverse(reversed)
assert.True(t, slices.IsSorted(reversed), "%s should be in descending order, got: %v", fieldName, values)
@@ -102,6 +104,7 @@ func AssertOrderedDescending[T cmp.Ordered](t *testing.T, values []T, fieldName
func AssertTimesOrderedAscending(t *testing.T, times []time.Time, fieldName string) {
t.Helper()
isSorted := slices.IsSortedFunc(times, func(a, b time.Time) int {
return a.Compare(b)
})
@@ -110,6 +113,7 @@ func AssertTimesOrderedAscending(t *testing.T, times []time.Time, fieldName stri
func AssertTimesOrderedDescending(t *testing.T, times []time.Time, fieldName string) {
t.Helper()
isSorted := slices.IsSortedFunc(times, func(a, b time.Time) int {
return b.Compare(a)
})
@@ -118,9 +122,11 @@ func AssertTimesOrderedDescending(t *testing.T, times []time.Time, fieldName str
func AssertNodeNotAccessible(t *testing.T, err error, nodeIsNil bool, resourceType string) {
t.Helper()
if err == nil {
assert.True(t, nodeIsNil, "should not be able to access %s from another org", resourceType)
}
// If there's an error, that's also acceptable (access denied)
}

View File

@@ -32,6 +32,7 @@ import (
func generateUniqueID() string {
randomBytes := make([]byte, 4)
_, _ = rand.Read(randomBytes)
return fmt.Sprintf("%d-%s", time.Now().UnixNano(), hex.EncodeToString(randomBytes))
}
@@ -139,6 +140,7 @@ func (c *Client) SetupTestUserInOrg(ownerClient *Client) {
c.userID = identityID
c.profileID = profileID
ownerClient.inviteUser(profileID)
token := c.getActivationToken(email)
passwordToken := c.activateUser(token)
c.resetPassword(password, passwordToken)
@@ -285,12 +287,14 @@ func (c *Client) updateOwnMembershipRole(role coredata.MembershipRole) {
require.NoError(c.T, err, "cannot query organization members")
var membershipID string
for _, edge := range qResult.Node.Members.Edges {
if edge.Node.Identity.ID == c.userID.String() {
membershipID = edge.Node.ID
break
}
}
require.NotEmpty(c.T, membershipID, "membership not found for user")
// Update the role
@@ -422,6 +426,7 @@ func (c *Client) getActivationToken(email string) string {
c.T.Logf("activation token not found")
c.T.FailNow()
return ""
}

View File

@@ -50,9 +50,11 @@ func (e GraphQLError) Code() string {
if e.Extensions == nil {
return ""
}
if code, ok := e.Extensions["code"].(string); ok {
return code
}
return ""
}
@@ -62,9 +64,11 @@ func (e GraphQLErrors) Error() string {
if len(e) == 0 {
return ""
}
if len(e) == 1 {
return e[0].Message
}
return fmt.Sprintf("%s (and %d more errors)", e[0].Message, len(e)-1)
}
@@ -83,12 +87,14 @@ func (c *Client) doWithEndpoint(endpoint string, query string, variables map[str
if err != nil {
return nil, fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)
@@ -160,6 +166,7 @@ func (c *Client) ExecuteShouldFail(query string, variables map[string]any) error
c.T.Helper()
_, err := c.Do(query, variables)
require.Error(c.T, err, "expected GraphQL request to fail but it succeeded")
return err
}
@@ -188,6 +195,7 @@ func (c *Client) ExecuteWithFiles(query string, variables map[string]any, files
func (c *Client) executeMultipart(query string, variables map[string]any, files map[string]UploadFile, result any) error {
// Create multipart writer using standard library
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// Build the operations JSON
@@ -195,6 +203,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
"query": query,
"variables": variables,
}
operationsJSON, err := json.Marshal(operations)
if err != nil {
return fmt.Errorf("cannot marshal operations: %w", err)
@@ -207,14 +216,17 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
// Build the map for file variables (sorted for deterministic order)
fileMap := make(map[string][]string)
fileOrder := make([]string, 0, len(files))
for path := range files {
fileOrder = append(fileOrder, path)
}
// Sort for deterministic ordering
for i, path := range fileOrder {
fileMap[fmt.Sprintf("%d", i)] = []string{"variables." + path}
}
mapJSON, err := json.Marshal(fileMap)
if err != nil {
return fmt.Errorf("cannot marshal map: %w", err)
@@ -239,6 +251,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
if err != nil {
return fmt.Errorf("cannot create file part %s: %w", path, err)
}
if _, err := part.Write(file.Content); err != nil {
return fmt.Errorf("cannot write file content %s: %w", path, err)
}
@@ -253,6 +266,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
if err != nil {
return fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
// Execute request
@@ -260,6 +274,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)

View File

@@ -45,12 +45,14 @@ func (c *Client) SearchMails(query string) (*MailpitSearchResponse, error) {
if err != nil {
return nil, fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)
@@ -75,12 +77,14 @@ func (c *Client) CheckMessageLinks(messageID string) (*MailpitLinkCheckResponse,
if err != nil {
return nil, fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)

View File

@@ -128,6 +128,7 @@ func (mc *MCPClient) doRequest(method string, params any) (json.RawMessage, erro
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("Authorization", "Bearer "+mc.apiToken)
if mc.sessionID != "" {
req.Header.Set("Mcp-Session-Id", mc.sessionID)
}
@@ -136,6 +137,7 @@ func (mc *MCPClient) doRequest(method string, params any) (json.RawMessage, erro
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)
@@ -198,6 +200,7 @@ func (mc *MCPClient) CallTool(toolName string, args map[string]any) *MCPToolResu
require.NoError(mc.t, err, "MCP tools/call %s failed", toolName)
var toolResult MCPToolResult
err = json.Unmarshal(result, &toolResult)
require.NoError(mc.t, err, "cannot unmarshal tool result for %s", toolName)
@@ -212,6 +215,7 @@ func (mc *MCPClient) CallToolExpectToolError(toolName string, args map[string]an
require.NotEmpty(mc.t, tr.Content, "tool %s returned no content", toolName)
var text string
err := json.Unmarshal(tr.Content[0].Text, &text)
require.NoError(mc.t, err, "cannot unmarshal error text for %s", toolName)
@@ -227,6 +231,7 @@ func (mc *MCPClient) CallToolInto(toolName string, args map[string]any, dest any
// The text field in MCP content is a JSON-encoded string of the output.
// First unmarshal the raw JSON to get the string.
var textStr string
err := json.Unmarshal(tr.Content[0].Text, &textStr)
require.NoError(mc.t, err, "cannot unmarshal text content for %s", toolName)

View File

@@ -129,6 +129,7 @@ func postForm(
if err != nil {
return nil, fmt.Errorf("cannot post form: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
@@ -153,6 +154,7 @@ func postJSON(
if err != nil {
return nil, fmt.Errorf("cannot post json: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
@@ -181,6 +183,7 @@ func getJSON(
if err != nil {
return nil, fmt.Errorf("cannot execute request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
@@ -213,6 +216,7 @@ func postFormWithBasicAuth(
if err != nil {
return nil, fmt.Errorf("cannot execute request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
@@ -298,10 +302,12 @@ func OAuth2Authorize(
}
reqURL := oauth2BaseURL(c) + "/authorize?" + params.Encode()
resp, err := noRedirectClient.Get(reqURL)
if err != nil {
return nil, fmt.Errorf("cannot get authorize: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
@@ -521,6 +527,7 @@ func OAuth2TokenWithDeviceCode(
if err := json.Unmarshal(raw.Body, &result); err != nil {
return nil, nil, raw, fmt.Errorf("cannot decode token response: %w", err)
}
return &result, nil, raw, nil
}
@@ -799,6 +806,7 @@ func GeneratePKCE() (verifier, challenge string) {
for i := range b {
b[i] = charset[rand.IntN(len(charset))]
}
verifier = string(b)
h := sha256.Sum256([]byte(verifier))
@@ -814,11 +822,14 @@ func IsConsentRedirect(resp *OAuth2HTTPResponse) bool {
if resp.StatusCode != http.StatusFound {
return false
}
loc := resp.Header.Get("Location")
u, err := url.Parse(loc)
if err != nil {
return false
}
return u.Query().Get("consent_id") != ""
}
@@ -831,14 +842,17 @@ func ExtractConsentIDFromResponse(resp *OAuth2HTTPResponse) (string, error) {
if loc == "" {
return "", fmt.Errorf("no Location header in redirect response")
}
u, err := url.Parse(loc)
if err != nil {
return "", fmt.Errorf("cannot parse redirect url: %w", err)
}
consentID := u.Query().Get("consent_id")
if consentID == "" {
return "", fmt.Errorf("no consent_id in redirect url: %s", loc)
}
return consentID, nil
}
@@ -850,12 +864,14 @@ func ExtractConsentID(body []byte) (string, error) {
s := string(body)
needle := `name="consent_id" value="`
idx := strings.Index(s, needle)
if idx == -1 {
return "", fmt.Errorf("consent_id not found in page")
}
start := idx + len(needle)
end := strings.Index(s[start:], `"`)
if end == -1 {
return "", fmt.Errorf("malformed consent_id value")
@@ -890,6 +906,7 @@ func OAuth2PerformAuthorizationCodeFlow(
require.NoError(t, err)
var code string
if IsConsentRedirect(authResp) {
consentID, err := ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)

View File

@@ -32,9 +32,11 @@ func ProseMirrorTextDoc(text string) string {
},
},
}
b, err := json.Marshal(doc)
if err != nil {
panic(err)
}
return string(b)
}

View File

@@ -54,6 +54,7 @@ func (s *switchableWriter) Write(p []byte) (int, error) {
s.mu.Lock()
w := s.w
s.mu.Unlock()
return w.Write(p)
}
@@ -104,6 +105,7 @@ func Setup() {
cmd.Stderr = os.Stderr
} else {
var buf bytes.Buffer
testEnv.outputBuf = &buf
sw := &switchableWriter{w: &buf}
testEnv.outputWriter = sw
@@ -128,14 +130,18 @@ func Setup() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := waitForServer(ctx, testEnv.BaseURL+"/api/console/v1/graphql", 30*time.Second); err != nil {
testEnv.dumpOutputOnFailure("API server failed to start", err)
_ = testEnv.cmd.Process.Kill()
os.Exit(1)
}
if err := waitForServer(ctx, testEnv.MailpitBaseURL+"/api/v1/messages", 30*time.Second); err != nil {
testEnv.dumpOutputOnFailure("MailPit server failed to start", err)
_ = testEnv.cmd.Process.Kill()
os.Exit(1)
}
@@ -161,11 +167,13 @@ func (e *TestEnv) dumpOutputOnFailure(context string, err error) {
if e.outputBuf != nil && e.outputBuf.Len() > 0 {
output := e.outputBuf.Bytes()
const maxTail = 10_000
if len(output) > maxTail {
fmt.Fprintf(os.Stderr, "e2etest: (showing last %d bytes of output)\n", maxTail)
output = output[len(output)-maxTail:]
}
fmt.Fprintf(os.Stderr, "--- probod output start ---\n%s\n--- probod output end ---\n", output)
} else {
fmt.Fprintf(os.Stderr, "e2etest: no captured output available\n")
@@ -224,6 +232,7 @@ func GetBaseURL() string {
if testEnv == nil {
return "http://localhost:8080"
}
return testEnv.BaseURL
}
@@ -231,6 +240,7 @@ func GetMailpitBaseURL() string {
if testEnv == nil {
return "http://localhost:8025"
}
return testEnv.MailpitBaseURL
}
@@ -305,6 +315,7 @@ func generateConfig() (string, error) {
if v, ok := env[key]; ok {
return v
}
return os.Getenv(key)
})
@@ -317,6 +328,7 @@ func generateConfig() (string, error) {
if err != nil {
return "", fmt.Errorf("create temp dir: %w", err)
}
path := filepath.Join(tmpDir, "probod.yml")
if err := bootstrap.WriteConfig(cfg, path); err != nil {

View File

@@ -23,7 +23,9 @@ import (
func TestMain(m *testing.M) {
testutil.Setup()
code := m.Run()
testutil.Teardown()
os.Exit(code)
}

View File

@@ -127,6 +127,7 @@ func TestMCP_ListThirdPartyContacts(t *testing.T) {
"email": factory.SafeEmail(),
}, &result)
require.NotEmpty(t, result.ThirdPartyContact.ID)
_ = i
}

View File

@@ -120,6 +120,7 @@ func TestMCP_ListThirdPartyServices(t *testing.T) {
"name": factory.SafeName("Service"),
}, &result)
require.NotEmpty(t, result.ThirdPartyService.ID)
_ = i
}

View File

@@ -36,6 +36,7 @@ func TestMCP_ThirdParty_CRUD(t *testing.T) {
Name string `json:"name"`
} `json:"third_party"`
}
name := factory.SafeName("ThirdParty")
mc.CallToolInto("addThirdParty", map[string]any{
"organizationId": orgID,

View File

@@ -220,6 +220,7 @@ func TestMCP_ListTrustCenterReferences(t *testing.T) {
"url": "https://example.com/" + factory.SafeName("path"),
}, &result)
require.NotEmpty(t, result.TrustCenterReference.ID)
_ = i
}
@@ -395,6 +396,7 @@ func TestMCP_ListComplianceExternalURLs(t *testing.T) {
"url": "https://example.com/" + factory.SafeName("path"),
}, &result)
require.NotEmpty(t, result.ComplianceExternalURL.ID)
_ = i
}

View File

@@ -96,6 +96,7 @@ func main() {
fmt.Fprintf(os.Stderr, "cannot fetch models: %v\n", err)
os.Exit(1)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
@@ -126,6 +127,7 @@ var generatedModels = map[string]ModelDefinition{
)
var count int
for _, m := range result.Data {
provider := providerFromID(m.ID)
if _, ok := includedProviders[provider]; !ok {
@@ -148,6 +150,7 @@ var generatedModels = map[string]ModelDefinition{
m.TopProvider.MaxCompletionTokens,
buildSupports(m.SupportedParams),
)
count++
}
@@ -172,11 +175,13 @@ func providerFromID(id string) string {
if ok {
return provider
}
return ""
}
func buildSupports(params []string) string {
fields := make(map[string]bool)
for _, p := range params {
if field, ok := paramFieldMap[p]; ok {
fields[field] = true
@@ -184,6 +189,7 @@ func buildSupports(params []string) string {
}
var buf strings.Builder
fieldNames := []string{
"Temperature", "TopP", "TopK", "FrequencyPenalty", "PresencePenalty",
"Stop", "Seed", "MaxTokens", "ToolChoice", "ParallelToolCalls",
@@ -192,5 +198,6 @@ func buildSupports(params []string) string {
for _, f := range fieldNames {
fmt.Fprintf(&buf, " %s: %t,\n", f, fields[f])
}
return buf.String()
}

View File

@@ -179,6 +179,7 @@ func UploadStaticAssets(ctx context.Context, s3Client *s3.Client, staticAssetsBu
if err != nil {
return err
}
defer func() { _ = file.Close() }()
_, err = s3Client.PutObject(
@@ -200,7 +201,6 @@ func UploadStaticAssets(ctx context.Context, s3Client *s3.Client, staticAssetsBu
return nil
})
if err != nil {
return fmt.Errorf("cannot generate asset URLs: %w", err)
}
@@ -261,6 +261,7 @@ func (p *Presenter) getCommonVariables(ctx context.Context) (*CommonVariables, e
if err != nil {
return nil, fmt.Errorf("cannot generate probo logo URL: %w", err)
}
senderCompanyLogoURL, err := p.fm.GenerateFileUrl(ctx, &p.config.SenderCompanyLogo, staticAssetsDuration)
if err != nil {
return nil, fmt.Errorf("cannot generate sender logo URL: %w", err)
@@ -298,6 +299,7 @@ func (p *Presenter) RenderConfirmEmail(ctx context.Context, confirmationURLPath
}
textBody, htmlBody, err = renderEmail(confirmEmailTextTemplate, confirmEmailHTMLTemplate, data)
return subjectConfirmEmail, textBody, htmlBody, err
}
@@ -322,6 +324,7 @@ func (p *Presenter) RenderPasswordReset(ctx context.Context, resetPasswordURLPat
}
textBody, htmlBody, err = renderEmail(passwordResetTextTemplate, passwordResetHTMLTemplate, data)
return subjectPasswordReset, textBody, htmlBody, err
}
@@ -348,6 +351,7 @@ func (p *Presenter) RenderInvitation(ctx context.Context, invitationURLPath stri
}
textBody, htmlBody, err = renderEmail(invitationTextTemplate, invitationHTMLTemplate, data)
return fmt.Sprintf(subjectInvitation, organizationName), textBody, htmlBody, err
}
@@ -381,6 +385,7 @@ func (p *Presenter) RenderDocumentApproval(
}
textBody, htmlBody, err = renderEmail(documentApprovalTextTemplate, documentApprovalHTMLTemplate, data)
return fmt.Sprintf(subjectDocumentApproval, documentName), textBody, htmlBody, err
}
@@ -411,6 +416,7 @@ func (p *Presenter) RenderDocumentSigning(
}
textBody, htmlBody, err = renderEmail(documentSigningTextTemplate, documentSigningHTMLTemplate, data)
return fmt.Sprintf(subjectDocumentSigning, organizationName), textBody, htmlBody, err
}
@@ -429,6 +435,7 @@ func (p *Presenter) RenderDocumentExport(ctx context.Context, downloadUrl string
}
textBody, htmlBody, err = renderEmail(documentExportTextTemplate, documentExportHTMLTemplate, data)
return subjectDocumentExport, textBody, htmlBody, err
}
@@ -447,6 +454,7 @@ func (p *Presenter) RenderFrameworkExport(ctx context.Context, downloadUrl strin
}
textBody, htmlBody, err = renderEmail(frameworkExportTextTemplate, frameworkExportHTMLTemplate, data)
return subjectFrameworkExport, textBody, htmlBody, err
}
@@ -465,6 +473,7 @@ func (p *Presenter) RenderTrustCenterAccess(ctx context.Context, organizationNam
}
textBody, htmlBody, err = renderEmail(trustCenterAccessTextTemplate, trustCenterAccessHTMLTemplate, data)
return fmt.Sprintf(subjectTrustCenterAccess, organizationName), textBody, htmlBody, err
}
@@ -489,6 +498,7 @@ func (p *Presenter) RenderTrustCenterDocumentAccessRejected(
}
textBody, htmlBody, err = renderEmail(trustCenterDocumentAccessRejectedTextTemplate, trustCenterDocumentAccessRejectedHTMLTemplate, data)
return fmt.Sprintf(subjectTrustCenterDocumentAccessRejected, organizationName), textBody, htmlBody, err
}
@@ -511,6 +521,7 @@ func (p *Presenter) RenderMagicLink(ctx context.Context, magicLinkUrlPath string
}
textBody, htmlBody, err = renderEmail(magicLinkTextTemplate, magicLinkHTMLTemplate, data)
return fmt.Sprintf(subjectMagicLink, organizationName), textBody, htmlBody, err
}
@@ -531,6 +542,7 @@ func (p *Presenter) RenderElectronicSignatureCertificate(ctx context.Context, si
}
textBody, htmlBody, err = renderEmail(electronicSignatureCertificateTextTemplate, electronicSignatureCertificateHTMLTemplate, data)
return fmt.Sprintf(subjectElectronicSignatureCertificate, documentName), textBody, htmlBody, err
}
@@ -617,12 +629,14 @@ func renderEmail(textTemplate *texttemplate.Template, htmlTemplate *htmltemplate
if err := textTemplate.Execute(&textBuf, data); err != nil {
return "", nil, fmt.Errorf("cannot execute text template: %w", err)
}
textBody = textBuf.String()
var htmlBuf bytes.Buffer
if err := htmlTemplate.Execute(&htmlBuf, data); err != nil {
return "", nil, fmt.Errorf("cannot execute html template: %w", err)
}
htmlBodyStr := htmlBuf.String()
htmlBody = &htmlBodyStr

View File

@@ -102,13 +102,16 @@ func (s AccessEntryService) RecordDecision(
entry.DecisionNote = req.DecisionNote
entry.DecidedBy = req.DecidedByID
entry.DecidedAt = &now
entry.UpdatedAt = now
if entry.Flags == nil {
entry.Flags = []coredata.AccessEntryFlag{}
}
if entry.FlagReasons == nil {
entry.FlagReasons = []string{}
}
if req.Decision == coredata.AccessEntryDecisionRevoke || req.Decision == coredata.AccessEntryDecisionEscalate {
if len(entry.Flags) == 0 {
entry.Flags = []coredata.AccessEntryFlag{coredata.AccessEntryFlagExcessive}
@@ -156,6 +159,7 @@ func (s AccessEntryService) RecordDecisions(
if d.Decision == coredata.AccessEntryDecisionPending {
return nil, fmt.Errorf("cannot bulk decide access entries: invalid decision %q", d.Decision)
}
if d.Decision != coredata.AccessEntryDecisionApproved {
if d.DecisionNote == nil || strings.TrimSpace(*d.DecisionNote) == "" {
return nil, fmt.Errorf(
@@ -189,9 +193,11 @@ func (s AccessEntryService) RecordDecisions(
if err := campaign.LoadByID(ctx, conn, s.scope, entry.AccessReviewCampaignID); err != nil {
return fmt.Errorf("cannot load campaign: %w", err)
}
if campaign.Status != coredata.AccessReviewCampaignStatusPendingActions {
return fmt.Errorf("cannot decide access entry: campaign status is %s, expected PENDING_ACTIONS", campaign.Status)
}
verifiedCampaigns[entry.AccessReviewCampaignID] = true
}
@@ -200,13 +206,16 @@ func (s AccessEntryService) RecordDecisions(
entry.DecisionNote = d.DecisionNote
entry.DecidedBy = d.DecidedByID
entry.DecidedAt = &now
entry.UpdatedAt = now
if entry.Flags == nil {
entry.Flags = []coredata.AccessEntryFlag{}
}
if entry.FlagReasons == nil {
entry.FlagReasons = []string{}
}
if d.Decision == coredata.AccessEntryDecisionRevoke || d.Decision == coredata.AccessEntryDecisionEscalate {
if len(entry.Flags) == 0 {
entry.Flags = []coredata.AccessEntryFlag{coredata.AccessEntryFlagExcessive}
@@ -245,6 +254,7 @@ func (s AccessEntryService) RecordDecisions(
if err != nil {
return nil, fmt.Errorf("cannot reload access entry %s: %w", id, err)
}
entries[i] = entry
}
@@ -274,14 +284,17 @@ func (s AccessEntryService) FlagEntry(
}
now := time.Now()
entry.Flags = req.Flags
if entry.Flags == nil {
entry.Flags = []coredata.AccessEntryFlag{}
}
entry.FlagReasons = req.FlagReasons
if entry.FlagReasons == nil {
entry.FlagReasons = []string{}
}
entry.UpdatedAt = now
return entry.UpdateFlags(ctx, conn, s.scope)
@@ -348,10 +361,12 @@ func (s AccessEntryService) CountForCampaignID(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
entries := coredata.AccessEntries{}
count, err = entries.CountByCampaignID(ctx, conn, s.scope, campaignID, filter)
if err != nil {
return fmt.Errorf("cannot count access entries by campaign: %w", err)
}
return nil
},
)
@@ -374,10 +389,12 @@ func (s AccessEntryService) CountForCampaignIDAndSourceID(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
entries := coredata.AccessEntries{}
count, err = entries.CountByCampaignIDAndSourceID(ctx, conn, s.scope, campaignID, sourceID, filter)
if err != nil {
return fmt.Errorf("cannot count access entries by campaign and source: %w", err)
}
return nil
},
)
@@ -398,10 +415,12 @@ func (s AccessEntryService) CountPendingForCampaignID(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
entries := coredata.AccessEntries{}
count, err = entries.CountPendingByCampaignID(ctx, conn, s.scope, campaignID)
if err != nil {
return fmt.Errorf("cannot count pending access entries: %w", err)
}
return nil
},
)

View File

@@ -188,6 +188,7 @@ func (s AccessSourceService) Update(
return fmt.Errorf("cannot load connector: %w", err)
}
}
source.ConnectorID = *req.ConnectorID
}
@@ -256,6 +257,7 @@ func (s AccessSourceService) CountForOrganizationID(
func(ctx context.Context, conn pg.Querier) (err error) {
sources := coredata.AccessSources{}
count, err = sources.CountByOrganizationID(ctx, conn, s.scope, organizationID)
return err
},
)
@@ -300,6 +302,7 @@ func (s AccessSourceService) ConnectorHTTPClient(
if err := dbConnector.LoadByID(ctx, conn, s.scope, connectorID, s.encryptionKey); err != nil {
return fmt.Errorf("cannot load connector: %w", err)
}
return nil
},
)
@@ -308,16 +311,19 @@ func (s AccessSourceService) ConnectorHTTPClient(
}
var tokenBefore string
oauth2Conn, isOAuth2 := dbConnector.Connection.(*connector.OAuth2Connection)
if isOAuth2 {
tokenBefore = oauth2Conn.AccessToken
}
var httpClient *http.Client
if isOAuth2 && s.connectorRegistry != nil {
refreshCfg := s.connectorRegistry.GetOAuth2RefreshConfig(string(dbConnector.Provider))
if refreshCfg != nil {
var err error
httpClient, err = oauth2Conn.RefreshableClient(ctx, *refreshCfg)
if err != nil {
return nil, nil, fmt.Errorf("cannot create refreshable HTTP client: %w", err)
@@ -327,6 +333,7 @@ func (s AccessSourceService) ConnectorHTTPClient(
if httpClient == nil {
var err error
httpClient, err = dbConnector.Connection.Client(ctx)
if err != nil {
return nil, nil, fmt.Errorf("cannot create HTTP client: %w", err)
@@ -336,6 +343,7 @@ func (s AccessSourceService) ConnectorHTTPClient(
// Persist refreshed token if it changed.
if isOAuth2 && oauth2Conn.AccessToken != tokenBefore {
dbConnector.UpdatedAt = time.Now()
if err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {

View File

@@ -105,6 +105,7 @@ func (s *CampaignService) Get(
if err := campaign.LoadByID(ctx, conn, s.scope, campaignID); err != nil {
return fmt.Errorf("cannot load campaign: %w", err)
}
return nil
},
)
@@ -192,6 +193,7 @@ func (s *CampaignService) Delete(
if err := campaign.Delete(ctx, conn, s.scope); err != nil {
return fmt.Errorf("cannot delete campaign: %w", err)
}
return nil
},
)
@@ -359,6 +361,7 @@ func (s *CampaignService) Close(
}
entries := coredata.AccessEntries{}
pendingCount, err := entries.CountPendingByCampaignID(ctx, conn, s.scope, campaignID)
if err != nil {
return fmt.Errorf("cannot count pending entries: %w", err)
@@ -392,6 +395,7 @@ func lockCampaignForUpdate(ctx context.Context, tx pg.Tx, scope coredata.Scoper,
if err := c.LockForUpdate(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot lock campaign for update: %w", err)
}
return nil
}
@@ -402,6 +406,7 @@ func (s *CampaignService) enqueueSourceFetches(
sources coredata.AccessSources,
) error {
now := time.Now()
for _, source := range sources {
fetch := &coredata.AccessReviewCampaignSourceFetch{
AccessReviewCampaignID: campaignID,
@@ -469,6 +474,7 @@ func (s *CampaignService) ListForOrganizationID(
if err := campaigns.LoadByOrganizationID(ctx, conn, s.scope, organizationID, cursor); err != nil {
return fmt.Errorf("cannot load campaigns by organization: %w", err)
}
return nil
},
)
@@ -491,6 +497,7 @@ func (s *CampaignService) ListSourceFetches(
if err := fetches.LoadByCampaignID(ctx, conn, s.scope, campaignID); err != nil {
return fmt.Errorf("cannot load source fetches by campaign: %w", err)
}
return nil
},
)
@@ -511,6 +518,7 @@ func (s *CampaignService) CountForOrganizationID(
ctx,
func(ctx context.Context, conn pg.Querier) (err error) {
campaigns := coredata.AccessReviewCampaigns{}
count, err = campaigns.CountByOrganizationID(ctx, conn, s.scope, organizationID)
if err != nil {
return fmt.Errorf("cannot count campaigns by organization: %w", err)

View File

@@ -98,6 +98,7 @@ func (d *AsanaDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
if page.NextPage == nil || page.NextPage.URI == "" {
return records, nil
}
next = page.NextPage.URI
}
@@ -109,12 +110,14 @@ func (d *AsanaDriver) queryUsers(ctx context.Context, endpoint string) (*asanaUs
if err != nil {
return nil, fmt.Errorf("cannot create asana users request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot execute asana users request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {

View File

@@ -110,12 +110,14 @@ func (d *BitbucketDriver) queryMembers(ctx context.Context, endpoint string) (*b
if err != nil {
return nil, fmt.Errorf("cannot create bitbucket members request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot execute bitbucket members request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {

View File

@@ -84,6 +84,7 @@ func (d *BrexDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
if resp.NextCursor == "" {
return records, nil
}
nextCursor := resp.NextCursor
cursor = &nextCursor
}
@@ -110,6 +111,7 @@ func (d *BrexDriver) queryUsers(ctx context.Context, cursor *string) (*brexUsers
if err != nil {
return nil, fmt.Errorf("cannot execute brex users request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -82,6 +82,7 @@ func TestCassettesUseSyntheticEmails(t *testing.T) {
if seen[email] {
continue
}
seen[email] = true
domain := email[strings.IndexByte(email, '@')+1:]
@@ -90,6 +91,7 @@ func TestCassettesUseSyntheticEmails(t *testing.T) {
}
ok := false
for _, suffix := range allowedDomainSuffixes {
if strings.HasSuffix("."+domain, suffix) || domain == strings.TrimPrefix(suffix, ".") {
ok = true

View File

@@ -77,12 +77,14 @@ func (d *ClickUpDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro
if err != nil {
return nil, fmt.Errorf("cannot create clickup team request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot execute clickup team request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -159,5 +161,6 @@ func parseClickUpTime(raw string) (time.Time, error) {
if err != nil {
return time.Time{}, fmt.Errorf("cannot parse clickup time %q: %w", raw, err)
}
return time.UnixMilli(ms).UTC(), nil
}

View File

@@ -131,6 +131,7 @@ func (d *CloudflareDriver) queryAccounts(ctx context.Context, page int) (*cloudf
if err != nil {
return nil, fmt.Errorf("cannot execute cloudflare accounts request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -168,6 +169,7 @@ func (d *CloudflareDriver) queryAllMembers(ctx context.Context, accountID string
}
isAdmin := false
for _, r := range m.Roles {
if r.Name == "Super Administrator - All Privileges" || r.Name == "Administrator" {
isAdmin = true
@@ -224,6 +226,7 @@ func (d *CloudflareDriver) queryMembers(ctx context.Context, accountID string, p
if err != nil {
return nil, fmt.Errorf("cannot execute cloudflare members request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -51,6 +51,7 @@ func (d *CSVDriver) ListAccounts(_ context.Context) ([]AccountRecord, error) {
for i, col := range header {
colIndex[strings.TrimSpace(strings.ToLower(col))] = i
}
if _, ok := colIndex["email"]; !ok {
return nil, fmt.Errorf("cannot parse CSV: missing required column email")
}
@@ -62,6 +63,7 @@ func (d *CSVDriver) ListAccounts(_ context.Context) ([]AccountRecord, error) {
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("cannot read CSV row: %w", err)
}
@@ -75,24 +77,31 @@ func (d *CSVDriver) ListAccounts(_ context.Context) ([]AccountRecord, error) {
if idx, ok := colIndex["email"]; ok && idx < len(row) {
record.Email = strings.TrimSpace(row[idx])
}
if idx, ok := colIndex["full_name"]; ok && idx < len(row) {
record.FullName = strings.TrimSpace(row[idx])
}
if idx, ok := colIndex["role"]; ok && idx < len(row) {
record.Role = strings.TrimSpace(row[idx])
}
if idx, ok := colIndex["job_title"]; ok && idx < len(row) {
record.JobTitle = strings.TrimSpace(row[idx])
}
if idx, ok := colIndex["is_admin"]; ok && idx < len(row) {
record.IsAdmin = strings.TrimSpace(strings.ToLower(row[idx])) == "true"
}
if idx, ok := colIndex["active"]; ok && idx < len(row) {
record.Active = new(strings.TrimSpace(strings.ToLower(row[idx])) == "true")
}
if idx, ok := colIndex["external_id"]; ok && idx < len(row) {
record.ExternalID = strings.TrimSpace(row[idx])
}
if idx, ok := colIndex["account_type"]; ok && idx < len(row) {
if strings.TrimSpace(strings.ToUpper(row[idx])) == "SERVICE_ACCOUNT" {
record.AccountType = coredata.AccessEntryAccountTypeServiceAccount

View File

@@ -24,6 +24,7 @@ func TestCSVDriverRequiresEmailHeader(t *testing.T) {
t.Parallel()
driver := NewCSVDriver(strings.NewReader("full_name,role\nJane Doe,Admin\n"))
_, err := driver.ListAccounts(context.Background())
if err == nil {
t.Fatalf("expected error when email header is missing")
@@ -36,16 +37,20 @@ func TestCSVDriverParsesRequiredAndOptionalColumns(t *testing.T) {
driver := NewCSVDriver(strings.NewReader(
"email,full_name,role,external_id\njane@example.com,Jane Doe,Admin,42\n",
))
records, err := driver.ListAccounts(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(records) != 1 {
t.Fatalf("expected 1 record, got %d", len(records))
}
if records[0].Email != "jane@example.com" {
t.Fatalf("unexpected email: %s", records[0].Email)
}
if records[0].ExternalID != "42" {
t.Fatalf("unexpected external id: %s", records[0].ExternalID)
}

View File

@@ -79,6 +79,7 @@ func (d *DocuSignDriver) ListAccounts(ctx context.Context) ([]AccountRecord, err
}
var records []AccountRecord
startPosition := 0
for range maxPaginationPages {
@@ -143,12 +144,14 @@ func (d *DocuSignDriver) discoverAccount(ctx context.Context) (accountID string,
if err != nil {
return "", "", fmt.Errorf("cannot create docusign userinfo request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := d.httpClient.Do(req)
if err != nil {
return "", "", fmt.Errorf("cannot execute docusign userinfo request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -183,12 +186,14 @@ func (d *DocuSignDriver) queryUsers(ctx context.Context, baseURI string, account
if err != nil {
return nil, fmt.Errorf("cannot create docusign users request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot execute docusign users request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -85,6 +85,7 @@ func (d *GitHubDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
d.logger.WarnCtx(ctx, "cannot fetch github membership, skipping member",
log.Error(err),
)
continue
}
@@ -93,6 +94,7 @@ func (d *GitHubDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
d.logger.WarnCtx(ctx, "cannot fetch github user profile, skipping member",
log.Error(err),
)
continue
}
@@ -107,6 +109,7 @@ func (d *GitHubDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
}
mfaStatus := coredata.MFAStatusUnknown
if no2FASet != nil {
if no2FASet[m.Login] {
mfaStatus = coredata.MFAStatusDisabled
@@ -158,6 +161,7 @@ func (d *GitHubDriver) fetchAllMembers(ctx context.Context) ([]githubMember, err
if nextURL == "" {
return members, nil
}
url = nextURL
}
@@ -176,6 +180,7 @@ func (d *GitHubDriver) fetchMembersPage(ctx context.Context, url string) ([]gith
if err != nil {
return nil, "", fmt.Errorf("cannot execute github members request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -215,6 +220,7 @@ func (d *GitHubDriver) fetchAll2FADisabledLogins(ctx context.Context) (map[strin
if nextURL == "" {
return set, nil
}
url = nextURL
}
@@ -239,6 +245,7 @@ func (d *GitHubDriver) fetchMembership(ctx context.Context, login string) (*gith
if err != nil {
return nil, fmt.Errorf("cannot execute github membership request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -269,6 +276,7 @@ func (d *GitHubDriver) fetchUserProfile(ctx context.Context, login string) (*git
if err != nil {
return nil, fmt.Errorf("cannot execute github user profile request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -117,12 +117,14 @@ func (d *GitLabDriver) queryMembers(ctx context.Context, endpoint string) ([]git
if err != nil {
return nil, "", fmt.Errorf("cannot create gitlab members request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, "", fmt.Errorf("cannot execute gitlab members request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {

View File

@@ -59,6 +59,7 @@ func (rt *retryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
}
var lastResp *http.Response
for attempt := range rt.maxRetries {
resp, err := transport.RoundTrip(req)
if err != nil {
@@ -94,6 +95,7 @@ func (d *GoogleWorkspaceDriver) ListAccounts(ctx context.Context) ([]AccountReco
}
var records []AccountRecord
pageToken := ""
for range maxPaginationPages {

View File

@@ -124,6 +124,7 @@ func (d *HerokuDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
if nextRange == "" {
return records, nil
}
rangeHeader = nextRange
}
@@ -135,7 +136,9 @@ func (d *HerokuDriver) queryMembers(ctx context.Context, endpoint, rangeHeader s
if err != nil {
return nil, "", fmt.Errorf("cannot create heroku members request: %w", err)
}
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
if rangeHeader != "" {
req.Header.Set("Range", rangeHeader)
}
@@ -144,6 +147,7 @@ func (d *HerokuDriver) queryMembers(ctx context.Context, endpoint, rangeHeader s
if err != nil {
return nil, "", fmt.Errorf("cannot execute heroku members request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
// Heroku returns 206 Partial Content for ranged responses with more

View File

@@ -83,6 +83,7 @@ func (d *HubSpotDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro
for _, u := range resp.Results {
role := "User"
if roleMap != nil && u.RoleID != "" {
if name, ok := roleMap[u.RoleID]; ok {
role = name
@@ -114,6 +115,7 @@ func (d *HubSpotDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro
if resp.Paging == nil || resp.Paging.Next == nil || resp.Paging.Next.After == "" {
return records, nil
}
after = resp.Paging.Next.After
}
@@ -128,9 +130,11 @@ func (d *HubSpotDriver) fetchUsers(ctx context.Context, after string) (*hubspotU
q := req.URL.Query()
q.Set("limit", "100")
if after != "" {
q.Set("after", after)
}
req.URL.RawQuery = q.Encode()
req.Header.Set("Accept", "application/json")
@@ -139,6 +143,7 @@ func (d *HubSpotDriver) fetchUsers(ctx context.Context, after string) (*hubspotU
if err != nil {
return nil, fmt.Errorf("cannot execute hubspot users request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -167,6 +172,7 @@ func (d *HubSpotDriver) fetchRoles(ctx context.Context) (map[string]string, erro
if err != nil {
return nil, fmt.Errorf("cannot execute hubspot roles request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -61,6 +61,7 @@ func (d *IntercomDriver) ListAccounts(ctx context.Context) ([]AccountRecord, err
}
var records []AccountRecord
for _, a := range resp.Admins {
record := AccountRecord{
Email: a.Email,
@@ -95,6 +96,7 @@ func (d *IntercomDriver) fetchAdmins(ctx context.Context) (*intercomAdminsRespon
if err != nil {
return nil, fmt.Errorf("cannot execute intercom admins request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -119,5 +121,6 @@ func intercomRole(hasInboxSeat bool) string {
if hasInboxSeat {
return "Agent"
}
return "Viewer"
}

View File

@@ -125,6 +125,7 @@ func (d *LinearDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
if !resp.Data.Users.PageInfo.HasNextPage || resp.Data.Users.PageInfo.EndCursor == "" {
return records, nil
}
nextCursor := resp.Data.Users.PageInfo.EndCursor
after = &nextCursor
}
@@ -170,6 +171,7 @@ query AccessReviewLinearUsers($after: String) {
if err != nil {
return nil, fmt.Errorf("cannot create linear users request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
@@ -177,6 +179,7 @@ query AccessReviewLinearUsers($after: String) {
if err != nil {
return nil, fmt.Errorf("cannot execute linear users request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -189,6 +192,7 @@ query AccessReviewLinearUsers($after: String) {
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
return nil, fmt.Errorf("cannot decode linear users response: %w", err)
}
if len(resp.Errors) > 0 {
return nil, fmt.Errorf("linear graphql error: %s", resp.Errors[0].Message)
}

View File

@@ -120,15 +120,18 @@ func (d *Microsoft365Driver) ListAccounts(ctx context.Context) ([]AccountRecord,
}
rolesByUser := make(map[string][]string)
for _, role := range roles {
members, err := d.listRoleMembers(ctx, role.ID)
if err != nil {
return nil, fmt.Errorf("cannot list members of role %q: %w", role.DisplayName, err)
}
for _, m := range members {
if m.ODataType != "" && m.ODataType != "#microsoft.graph.user" {
continue
}
rolesByUser[m.ID] = append(rolesByUser[m.ID], role.DisplayName)
}
}
@@ -147,6 +150,7 @@ func (d *Microsoft365Driver) ListAccounts(ctx context.Context) ([]AccountRecord,
userRoles := rolesByUser[u.ID]
isAdmin := false
for _, r := range userRoles {
if adminRoleDisplayNames[r] {
isAdmin = true
@@ -214,6 +218,7 @@ func pickHighestRole(roles []string) string {
if len(roles) > 0 {
return roles[0]
}
return ""
}
@@ -224,15 +229,18 @@ func (d *Microsoft365Driver) listUsers(ctx context.Context) ([]microsoft365User,
}
var all []microsoft365User
for range microsoft365MaxPaginationOK {
var page microsoft365UsersPage
if err := d.fetchJSON(ctx, pageURL, &page); err != nil {
return nil, err
}
all = append(all, page.Value...)
if page.NextLink == "" {
return all, nil
}
pageURL = page.NextLink
}
@@ -258,15 +266,18 @@ func (d *Microsoft365Driver) listDirectoryRoles(ctx context.Context) ([]microsof
url := fmt.Sprintf("%s/directoryRoles", microsoft365GraphBaseURL)
var all []microsoft365DirectoryRole
for range microsoft365MaxPaginationOK {
var page microsoft365RolesPage
if err := d.fetchJSON(ctx, url, &page); err != nil {
return nil, err
}
all = append(all, page.Value...)
if page.NextLink == "" {
return all, nil
}
url = page.NextLink
}
@@ -277,15 +288,18 @@ func (d *Microsoft365Driver) listRoleMembers(ctx context.Context, roleID string)
url := fmt.Sprintf("%s/directoryRoles/%s/members", microsoft365GraphBaseURL, roleID)
var all []microsoft365RoleMember
for range microsoft365MaxPaginationOK {
var page microsoft365MembersPage
if err := d.fetchJSON(ctx, url, &page); err != nil {
return nil, err
}
all = append(all, page.Value...)
if page.NextLink == "" {
return all, nil
}
url = page.NextLink
}
@@ -297,12 +311,14 @@ func (d *Microsoft365Driver) fetchJSON(ctx context.Context, url string, dst any)
if err != nil {
return fmt.Errorf("cannot create graph request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := d.httpClient.Do(req)
if err != nil {
return fmt.Errorf("cannot execute graph request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {

View File

@@ -144,6 +144,7 @@ func (d *MondayDriver) queryUsers(ctx context.Context, page int) ([]mondayUser,
if err != nil {
return nil, fmt.Errorf("cannot create monday users request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
@@ -151,6 +152,7 @@ func (d *MondayDriver) queryUsers(ctx context.Context, page int) ([]mondayUser,
if err != nil {
return nil, fmt.Errorf("cannot execute monday users request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {

View File

@@ -69,6 +69,7 @@ func ProviderDisplayName(provider coredata.ConnectorProvider) string {
if name, ok := providerDisplayNames[provider]; ok {
return name
}
return string(provider)
}
@@ -91,6 +92,7 @@ func (r *slackNameResolver) ResolveInstanceName(ctx context.Context) (string, er
if err != nil {
return "", fmt.Errorf("cannot execute slack auth.test request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
var resp struct {
@@ -156,6 +158,7 @@ func (r *linearNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot create linear organization request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
@@ -163,6 +166,7 @@ func (r *linearNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot execute linear organization request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -182,6 +186,7 @@ func (r *linearNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
return "", fmt.Errorf("cannot decode linear organization response: %w", err)
}
if len(resp.Errors) > 0 {
return "", fmt.Errorf("linear graphql error: %s", resp.Errors[0].Message)
}
@@ -208,12 +213,14 @@ func (r *cloudflareNameResolver) ResolveInstanceName(ctx context.Context) (strin
if err != nil {
return "", fmt.Errorf("cannot create cloudflare accounts request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute cloudflare accounts request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -255,12 +262,14 @@ func (r *brexNameResolver) ResolveInstanceName(ctx context.Context) (string, err
if err != nil {
return "", fmt.Errorf("cannot create brex company request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute brex company request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -297,12 +306,14 @@ func (r *tallyNameResolver) ResolveInstanceName(ctx context.Context) (string, er
if err != nil {
return "", fmt.Errorf("cannot create tally organization request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute tally organization request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -338,12 +349,14 @@ func (r *hubspotNameResolver) ResolveInstanceName(ctx context.Context) (string,
if err != nil {
return "", fmt.Errorf("cannot create hubspot account-info request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute hubspot account-info request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -375,12 +388,14 @@ func (r *docusignNameResolver) ResolveInstanceName(ctx context.Context) (string,
if err != nil {
return "", fmt.Errorf("cannot create docusign userinfo request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute docusign userinfo request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -429,12 +444,14 @@ func (r *openaiNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot create openai organization request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute openai organization request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -473,12 +490,14 @@ func (r *sentryNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot create sentry organization request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute sentry organization request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -512,12 +531,14 @@ func (r *githubNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot create github organization request: %w", err)
}
req.Header.Set("Accept", "application/vnd.github+json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute github organization request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -565,6 +586,7 @@ func (r *intercomNameResolver) ResolveInstanceName(ctx context.Context) (string,
if err != nil {
return "", fmt.Errorf("cannot create intercom me request: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Intercom-Version", "2.11")
@@ -572,6 +594,7 @@ func (r *intercomNameResolver) ResolveInstanceName(ctx context.Context) (string,
if err != nil {
return "", fmt.Errorf("cannot execute intercom me request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -622,12 +645,14 @@ func (r *gitlabNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot create gitlab group request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute gitlab group request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -645,6 +670,7 @@ func (r *gitlabNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if resp.Name != "" {
return resp.Name, nil
}
return resp.FullPath, nil
}
@@ -669,12 +695,14 @@ func (r *bitbucketNameResolver) ResolveInstanceName(ctx context.Context) (string
if err != nil {
return "", fmt.Errorf("cannot create bitbucket workspace request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute bitbucket workspace request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -692,6 +720,7 @@ func (r *bitbucketNameResolver) ResolveInstanceName(ctx context.Context) (string
if resp.Name != "" {
return resp.Name, nil
}
return resp.Slug, nil
}
@@ -716,12 +745,14 @@ func (r *herokuNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot create heroku team request: %w", err)
}
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute heroku team request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -774,12 +805,14 @@ func (r *asanaNameResolver) ResolveInstanceName(ctx context.Context) (string, er
if err != nil {
return "", fmt.Errorf("cannot create asana workspace request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute asana workspace request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -819,12 +852,14 @@ func (r *netlifyNameResolver) ResolveInstanceName(ctx context.Context) (string,
if err != nil {
return "", fmt.Errorf("cannot create netlify account request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute netlify account request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -862,12 +897,14 @@ func (r *clickupNameResolver) ResolveInstanceName(ctx context.Context) (string,
if err != nil {
return "", fmt.Errorf("cannot create clickup team request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute clickup team request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -905,16 +942,19 @@ func (r *vercelNameResolver) ResolveInstanceName(ctx context.Context) (string, e
}
teamURL := fmt.Sprintf("https://api.vercel.com/v2/teams/%s", url.PathEscape(r.teamID))
teamReq, err := http.NewRequestWithContext(ctx, http.MethodGet, teamURL, nil)
if err != nil {
return "", fmt.Errorf("cannot create vercel team request: %w", err)
}
teamReq.Header.Set("Accept", "application/json")
teamResp, err := r.httpClient.Do(teamReq)
if err != nil {
return "", fmt.Errorf("cannot execute vercel team request: %w", err)
}
defer func() { _ = teamResp.Body.Close() }()
if teamResp.StatusCode == http.StatusOK {
@@ -925,9 +965,11 @@ func (r *vercelNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err := json.NewDecoder(teamResp.Body).Decode(&body); err != nil {
return "", fmt.Errorf("cannot decode vercel team response: %w", err)
}
if body.Name != "" {
return body.Name, nil
}
return body.Slug, nil
}
@@ -941,9 +983,11 @@ func (r *vercelNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", err
}
if user.Username != "" {
return user.Username, nil
}
return user.Name, nil
}
@@ -972,6 +1016,7 @@ func (r *mondayNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot create monday account request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
@@ -979,6 +1024,7 @@ func (r *mondayNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot execute monday account request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -1023,6 +1069,7 @@ func (r *notionNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot create notion users/me request: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Notion-Version", notionAPIVersion)
@@ -1030,6 +1077,7 @@ func (r *notionNameResolver) ResolveInstanceName(ctx context.Context) (string, e
if err != nil {
return "", fmt.Errorf("cannot execute notion users/me request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -1068,12 +1116,14 @@ func (r *microsoft365NameResolver) ResolveInstanceName(ctx context.Context) (str
if err != nil {
return "", fmt.Errorf("cannot create microsoft 365 organization request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := r.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot execute microsoft 365 organization request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
@@ -1101,13 +1151,16 @@ func (r *microsoft365NameResolver) ResolveInstanceName(ctx context.Context) (str
if org.DisplayName != "" {
return org.DisplayName, nil
}
for _, d := range org.VerifiedDomains {
if d.IsDefault {
return d.Name, nil
}
}
if len(org.VerifiedDomains) > 0 {
return org.VerifiedDomains[0].Name, nil
}
return "", nil
}

View File

@@ -37,9 +37,11 @@ func (h *hostRewriter) RoundTrip(r *http.Request) (*http.Response, error) {
if err != nil {
return nil, err
}
r2 := r.Clone(r.Context())
r2.URL.Scheme = u.Scheme
r2.URL.Host = u.Host
return http.DefaultTransport.RoundTrip(r2)
}
@@ -88,11 +90,13 @@ func TestNotionNameResolver(t *testing.T) {
defer srv.Close()
client := &http.Client{Transport: &hostRewriter{target: srv.URL}}
got, err := NewNotionNameResolver(client).ResolveInstanceName(context.Background())
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tc.want, got)
})

View File

@@ -94,12 +94,14 @@ func (d *NetlifyDriver) queryMembers(ctx context.Context, endpoint string) ([]ne
if err != nil {
return nil, "", fmt.Errorf("cannot create netlify members request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, "", fmt.Errorf("cannot execute netlify members request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {

View File

@@ -96,6 +96,7 @@ func (d *NotionDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
if !resp.HasMore || resp.NextCursor == "" {
return records, nil
}
nextCursor := resp.NextCursor
startCursor = &nextCursor
}
@@ -114,15 +115,18 @@ func (d *NotionDriver) queryUsers(ctx context.Context, startCursor *string) (*no
q := req.URL.Query()
q.Set("page_size", "100")
if startCursor != nil {
q.Set("start_cursor", *startCursor)
}
req.URL.RawQuery = q.Encode()
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot execute notion users request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -71,6 +71,7 @@ func NewOnePasswordDriver(httpClient *http.Client, baseURL string) *OnePasswordD
func (d *OnePasswordDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
var records []AccountRecord
startIndex := 1
for range maxPaginationPages {
@@ -103,6 +104,7 @@ func (d *OnePasswordDriver) ListAccounts(ctx context.Context) ([]AccountRecord,
if record.FullName == "" && u.Name.Formatted != "" {
record.FullName = u.Name.Formatted
}
if record.FullName == "" && (u.Name.GivenName != "" || u.Name.FamilyName != "") {
record.FullName = u.Name.GivenName + " " + u.Name.FamilyName
}
@@ -128,6 +130,7 @@ func (d *OnePasswordDriver) ListAccounts(ctx context.Context) ([]AccountRecord,
if len(resp.Resources) == 0 || resp.ItemsPerPage <= 0 || startIndex+resp.ItemsPerPage > resp.TotalResults {
return records, nil
}
startIndex += resp.ItemsPerPage
}
@@ -139,6 +142,7 @@ func (d *OnePasswordDriver) queryUsers(ctx context.Context, startIndex int) (*on
if err != nil {
return nil, fmt.Errorf("cannot parse 1password base url: %w", err)
}
u = u.JoinPath("scim", "v2", "Users")
q := u.Query()
q.Set("startIndex", strconv.Itoa(startIndex))
@@ -149,12 +153,14 @@ func (d *OnePasswordDriver) queryUsers(ctx context.Context, startIndex int) (*on
if err != nil {
return nil, fmt.Errorf("cannot create 1password users request: %w", err)
}
req.Header.Set("Accept", "application/scim+json")
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot execute 1password users request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -107,6 +107,7 @@ func (d *OnePasswordUsersAPIDriver) ListAccounts(ctx context.Context) ([]Account
if resp.NextPageToken == "" {
return records, nil
}
pageToken = resp.NextPageToken
}
@@ -118,6 +119,7 @@ func (d *OnePasswordUsersAPIDriver) queryUsers(ctx context.Context, pageToken st
if err != nil {
return nil, fmt.Errorf("cannot parse 1password users api base url: %w", err)
}
u = u.JoinPath("v1beta1", "accounts", d.accountID, "users")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
@@ -127,9 +129,11 @@ func (d *OnePasswordUsersAPIDriver) queryUsers(ctx context.Context, pageToken st
q := req.URL.Query()
q.Set("max_page_size", "100")
if pageToken != "" {
q.Set("page_token", pageToken)
}
req.URL.RawQuery = q.Encode()
req.Header.Set("Accept", "application/json")
@@ -138,6 +142,7 @@ func (d *OnePasswordUsersAPIDriver) queryUsers(ctx context.Context, pageToken st
if err != nil {
return nil, fmt.Errorf("cannot execute 1password users api request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -89,6 +89,7 @@ func (d *OpenAIDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
if !resp.HasMore || resp.LastID == "" {
return records, nil
}
after = resp.LastID
}
@@ -103,9 +104,11 @@ func (d *OpenAIDriver) fetchUsers(ctx context.Context, after string) (*openaiUse
q := req.URL.Query()
q.Set("limit", "100")
if after != "" {
q.Set("after", after)
}
req.URL.RawQuery = q.Encode()
req.Header.Set("Accept", "application/json")
@@ -114,6 +117,7 @@ func (d *OpenAIDriver) fetchUsers(ctx context.Context, after string) (*openaiUse
if err != nil {
return nil, fmt.Errorf("cannot execute openai users request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -38,12 +38,14 @@ func ListGitHubOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
if err != nil {
return nil, fmt.Errorf("cannot create github organizations request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot fetch github organizations: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
@@ -64,8 +66,10 @@ func ListGitHubOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
if displayName == "" {
displayName = org.Login
}
result[i] = Organization{Slug: org.Login, DisplayName: displayName}
}
return result, nil
}
@@ -81,12 +85,14 @@ func ListSentryOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
if err != nil {
return nil, fmt.Errorf("cannot create sentry organizations request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot fetch sentry organizations: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
@@ -107,8 +113,10 @@ func ListSentryOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
if displayName == "" {
displayName = org.Slug
}
result[i] = Organization{Slug: org.Slug, DisplayName: displayName}
}
return result, nil
}
@@ -125,12 +133,14 @@ func ListGitLabOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
if err != nil {
return nil, fmt.Errorf("cannot create gitlab organizations request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot fetch gitlab organizations: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
@@ -152,11 +162,13 @@ func ListGitLabOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
if displayName == "" {
displayName = g.FullPath
}
result[i] = Organization{
Slug: strconv.FormatInt(g.ID, 10),
DisplayName: displayName,
}
}
return result, nil
}
@@ -174,6 +186,7 @@ func ListBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([
if err != nil {
return nil, fmt.Errorf("cannot create bitbucket organizations request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
@@ -203,6 +216,7 @@ func ListBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([
_ = resp.Body.Close()
return nil, fmt.Errorf("cannot decode bitbucket organizations response: %w", err)
}
_ = resp.Body.Close()
for _, v := range body.Values {
@@ -211,18 +225,22 @@ func ListBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([
slug = v.Workspace.Slug
name = v.Workspace.Name
}
displayName := name
if displayName == "" {
displayName = slug
}
result = append(result, Organization{Slug: slug, DisplayName: displayName})
}
if body.Next == "" {
return result, nil
}
pageURL = body.Next
}
return nil, fmt.Errorf("cannot list all bitbucket organizations: %w", ErrPaginationLimitReached)
}
@@ -233,12 +251,14 @@ func ListHerokuOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
if err != nil {
return nil, fmt.Errorf("cannot create heroku organizations request: %w", err)
}
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot fetch heroku organizations: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
@@ -259,8 +279,10 @@ func ListHerokuOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
if displayName == "" {
displayName = t.ID
}
result[i] = Organization{Slug: t.ID, DisplayName: displayName}
}
return result, nil
}
@@ -276,12 +298,14 @@ func ListAsanaOrganizations(ctx context.Context, httpClient *http.Client) ([]Org
if err != nil {
return nil, fmt.Errorf("cannot create asana organizations request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot fetch asana organizations: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
@@ -304,8 +328,10 @@ func ListAsanaOrganizations(ctx context.Context, httpClient *http.Client) ([]Org
if displayName == "" {
displayName = w.GID
}
result[i] = Organization{Slug: w.GID, DisplayName: displayName}
}
return result, nil
}
@@ -321,12 +347,14 @@ func ListNetlifyOrganizations(ctx context.Context, httpClient *http.Client) ([]O
if err != nil {
return nil, fmt.Errorf("cannot create netlify organizations request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot fetch netlify organizations: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
@@ -348,8 +376,10 @@ func ListNetlifyOrganizations(ctx context.Context, httpClient *http.Client) ([]O
if displayName == "" {
displayName = a.Slug
}
result[i] = Organization{Slug: a.Slug, DisplayName: displayName}
}
return result, nil
}
@@ -365,12 +395,14 @@ func ListClickUpOrganizations(ctx context.Context, httpClient *http.Client) ([]O
if err != nil {
return nil, fmt.Errorf("cannot create clickup organizations request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot fetch clickup organizations: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
@@ -393,7 +425,9 @@ func ListClickUpOrganizations(ctx context.Context, httpClient *http.Client) ([]O
if displayName == "" {
displayName = t.ID
}
result[i] = Organization{Slug: t.ID, DisplayName: displayName}
}
return result, nil
}

View File

@@ -65,6 +65,7 @@ func (d *PagerDutyDriver) ListAccounts(ctx context.Context) ([]AccountRecord, er
var records []AccountRecord
const limit = 100
offset := 0
for range maxPaginationPages {
@@ -113,6 +114,7 @@ func (d *PagerDutyDriver) ListAccounts(ctx context.Context) ([]AccountRecord, er
if pageSize <= 0 {
pageSize = limit
}
offset += pageSize
}
@@ -130,12 +132,14 @@ func (d *PagerDutyDriver) queryUsers(ctx context.Context, offset, limit int) (*p
if err != nil {
return nil, fmt.Errorf("cannot create pagerduty users request: %w", err)
}
req.Header.Set("Accept", "application/vnd.pagerduty+json;version=2")
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot execute pagerduty users request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {

View File

@@ -54,6 +54,7 @@ func (d *ResendDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
}
var records []AccountRecord
for _, k := range resp.Data {
record := AccountRecord{
FullName: k.Name,
@@ -96,6 +97,7 @@ func (d *ResendDriver) fetchAPIKeys(ctx context.Context) (*resendAPIKeysResponse
if err != nil {
return nil, fmt.Errorf("cannot execute resend api-keys request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -67,12 +67,14 @@ func (d *SentryDriver) resolveOrgSlug(ctx context.Context) (string, error) {
if err != nil {
return "", fmt.Errorf("cannot create sentry organizations request: %w", err)
}
req.Header.Set("Accept", "application/json")
resp, err := d.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("cannot fetch sentry organizations: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
@@ -100,6 +102,7 @@ func (d *SentryDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
if err != nil {
return nil, fmt.Errorf("cannot resolve sentry organization slug: %w", err)
}
orgSlug = slug
}
@@ -130,6 +133,7 @@ func (d *SentryDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
isAdmin := m.OrgRole == "admin" || m.OrgRole == "owner"
mfaStatus := coredata.MFAStatusUnknown
if m.User != nil {
if m.User.Has2FA {
mfaStatus = coredata.MFAStatusEnabled
@@ -188,6 +192,7 @@ func (d *SentryDriver) queryMembers(ctx context.Context, url string) ([]sentryMe
if err != nil {
return nil, "", fmt.Errorf("cannot execute sentry members request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -221,8 +226,10 @@ func sentryAuthMethod(flags map[string]bool, user *sentryUser) coredata.AccessEn
if flags["sso:linked"] {
return coredata.AccessEntryAuthMethodSSO
}
if user != nil && user.HasPasswordAuth {
return coredata.AccessEntryAuthMethodPassword
}
return coredata.AccessEntryAuthMethodUnknown
}

View File

@@ -120,6 +120,7 @@ func (d *SlackDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
if resp.ResponseMetadata.NextCursor == "" {
return records, nil
}
cursor = resp.ResponseMetadata.NextCursor
}
@@ -134,15 +135,18 @@ func (d *SlackDriver) queryUsers(ctx context.Context, cursor string) (*slackUser
q := req.URL.Query()
q.Set("limit", "200")
if cursor != "" {
q.Set("cursor", cursor)
}
req.URL.RawQuery = q.Encode()
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot execute slack users.list request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -180,5 +184,6 @@ func slackMFAStatus(has2FA bool) coredata.MFAStatus {
if has2FA {
return coredata.MFAStatusEnabled
}
return coredata.MFAStatusDisabled
}

View File

@@ -36,12 +36,14 @@ func TestSlackDriver(t *testing.T) {
// Find the first human user (bots may not have email).
var r AccountRecord
for _, rec := range records {
if rec.Email != "" {
r = rec
break
}
}
require.NotEmpty(t, r.Email, "expected at least one record with an email")
assert.NotEmpty(t, r.ExternalID)
assert.NotEmpty(t, r.Role)

View File

@@ -53,6 +53,7 @@ func (d *SupabaseDriver) ListAccounts(ctx context.Context) ([]AccountRecord, err
}
var records []AccountRecord
for _, m := range members {
mfaStatus := coredata.MFAStatusDisabled
if m.MFAEnabled {
@@ -96,6 +97,7 @@ func (d *SupabaseDriver) queryMembers(ctx context.Context) ([]supabaseMember, er
if err != nil {
return nil, fmt.Errorf("cannot execute supabase members request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()

View File

@@ -89,6 +89,7 @@ func (d *TallyDriver) listUsers(ctx context.Context) ([]AccountRecord, error) {
if err != nil {
return nil, fmt.Errorf("cannot execute tally users request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -106,6 +107,7 @@ func (d *TallyDriver) listUsers(ctx context.Context) ([]AccountRecord, error) {
}
var records []AccountRecord
for _, u := range users {
mfaStatus := coredata.MFAStatusDisabled
if u.HasTwoFactorEnabled {
@@ -149,6 +151,7 @@ func (d *TallyDriver) listInvites(ctx context.Context) ([]AccountRecord, error)
if err != nil {
return nil, fmt.Errorf("cannot execute tally invites request: %w", err)
}
defer func() {
_ = httpResp.Body.Close()
}()
@@ -166,6 +169,7 @@ func (d *TallyDriver) listInvites(ctx context.Context) ([]AccountRecord, error)
}
var records []AccountRecord
for _, inv := range invites {
record := AccountRecord{
Email: inv.Email,

View File

@@ -57,6 +57,7 @@ func newRecorder(t *testing.T, cassettePath string, envVar string) *recorder.Rec
if mode == recorder.ModeReplayOnly {
t.Skipf("cassette not found (record with %s env var): %v", envVar, err)
}
t.Fatalf("cannot create vcr recorder: %v", err)
}
@@ -81,6 +82,7 @@ func (rt *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
if rt.authValue != "" {
req.Header.Set("Authorization", rt.authValue)
}
return rt.transport.RoundTrip(req)
}
@@ -89,6 +91,7 @@ func bearerAuth(token string) string {
if token == "" {
return ""
}
return "Bearer " + token
}
@@ -104,5 +107,6 @@ func newVCRClient(rec *recorder.Recorder, authValue string) *http.Client {
transport: transport,
}
}
return &http.Client{Transport: transport}
}

View File

@@ -111,6 +111,7 @@ func (d *VercelDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
if page.Pagination.Next == nil {
return records, nil
}
cursor = strconv.FormatInt(*page.Pagination.Next, 10)
}
@@ -120,9 +121,11 @@ func (d *VercelDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
func (d *VercelDriver) queryMembers(ctx context.Context, cursor string) (*vercelMembersPage, error) {
q := url.Values{}
q.Set("limit", "100")
if cursor != "" {
q.Set("until", cursor)
}
u := url.URL{
Scheme: "https",
Host: "api.vercel.com",
@@ -134,12 +137,14 @@ func (d *VercelDriver) queryMembers(ctx context.Context, cursor string) (*vercel
if err != nil {
return nil, fmt.Errorf("cannot create vercel members request: %w", err)
}
req.Header.Set("Accept", "application/json")
httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot execute vercel members request: %w", err)
}
defer func() { _ = httpResp.Body.Close() }()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {

View File

@@ -80,11 +80,13 @@ func (e *ReviewEngine) FetchSource(
if err := source.LoadByID(ctx, tx, e.scope, sourceID); err != nil {
return fmt.Errorf("cannot load access source %s: %w", sourceID, err)
}
if source.OrganizationID != campaign.OrganizationID {
return fmt.Errorf("cannot process access source: %s does not belong to campaign organization", sourceID)
}
var err error
driver, err = e.resolveDriver(ctx, tx, source)
if err != nil {
return fmt.Errorf("cannot resolve driver for source %s: %w", source.Name, err)
@@ -97,6 +99,7 @@ func (e *ReviewEngine) FetchSource(
}
} else {
entries := &coredata.AccessEntries{}
baseline, err = entries.LoadBaselineBySourceID(ctx, tx, e.scope, lastCompletedCampaign.ID, sourceID)
if err != nil {
return fmt.Errorf("cannot load baseline entries by source: %w", err)
@@ -117,10 +120,13 @@ func (e *ReviewEngine) FetchSource(
sourceCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
accounts, err := driver.ListAccounts(sourceCtx)
cancel()
if err != nil {
return 0, fmt.Errorf("cannot list accounts from source %s: %w", source.Name, err)
}
fetchedCount = len(accounts)
err = e.pg.WithTx(
@@ -132,6 +138,7 @@ func (e *ReviewEngine) FetchSource(
for _, account := range accounts {
accountKey := normalizeAccountKey(account.Email, account.ExternalID)
seenAccountKeys[accountKey] = struct{}{}
incrementalTag := coredata.AccessEntryIncrementalTagNew
if _, ok := previousByAccountKey[accountKey]; ok {
incrementalTag = coredata.AccessEntryIncrementalTagUnchanged
@@ -210,6 +217,7 @@ func (e *ReviewEngine) FetchSource(
func normalizeAccountKey(email, externalID string) string {
emailKey := strings.ToLower(strings.TrimSpace(email))
externalID = strings.TrimSpace(externalID)
if externalID != "" {
return emailKey + "|" + externalID
@@ -231,6 +239,7 @@ func (e *ReviewEngine) oauthClient(
return conn.RefreshableClient(ctx, *refreshCfg)
}
}
return conn.Client(ctx)
}
@@ -245,6 +254,7 @@ func (e *ReviewEngine) connectorHTTPClient(
if oauth2Conn, ok := dbConnector.Connection.(*connector.OAuth2Connection); ok {
return e.oauthClient(ctx, oauth2Conn, dbConnector.Provider)
}
return dbConnector.Connection.Client(ctx)
}
@@ -312,15 +322,19 @@ func (e *ReviewEngine) resolveDriver(
if err != nil {
return nil, fmt.Errorf("cannot read 1password users api settings: %w", err)
}
return drivers.NewOnePasswordUsersAPIDriver(httpClient, settings.AccountID, settings.Region), nil
}
onePasswordSettings, err := coredata.ConnectorSettings[coredata.OnePasswordConnectorSettings](dbConnector)
if err != nil {
return nil, fmt.Errorf("cannot read 1password connector settings: %w", err)
}
if onePasswordSettings.SCIMBridgeURL == "" {
return nil, fmt.Errorf("1password connector requires scim_bridge_url in settings")
}
return drivers.NewOnePasswordDriver(httpClient, onePasswordSettings.SCIMBridgeURL), nil
case coredata.ConnectorProviderHubSpot:
return drivers.NewHubSpotDriver(httpClient), nil
@@ -335,9 +349,11 @@ func (e *ReviewEngine) resolveDriver(
if err != nil {
return nil, fmt.Errorf("cannot read tally connector settings: %w", err)
}
if tallySettings.OrganizationID == "" {
return nil, fmt.Errorf("tally connector requires organization_id in settings")
}
return drivers.NewTallyDriver(httpClient, tallySettings.OrganizationID), nil
case coredata.ConnectorProviderCloudflare:
return drivers.NewCloudflareDriver(httpClient), nil
@@ -348,6 +364,7 @@ func (e *ReviewEngine) resolveDriver(
if err != nil {
return nil, fmt.Errorf("cannot read sentry connector settings: %w", err)
}
// OrganizationSlug may be empty for OAuth connections; the driver auto-discovers it.
return drivers.NewSentryDriver(httpClient, sentrySettings.OrganizationSlug), nil
case coredata.ConnectorProviderSupabase:
@@ -355,18 +372,22 @@ func (e *ReviewEngine) resolveDriver(
if err != nil {
return nil, fmt.Errorf("cannot read supabase connector settings: %w", err)
}
if supabaseSettings.OrganizationSlug == "" {
return nil, fmt.Errorf("supabase connector requires organization_slug in settings")
}
return drivers.NewSupabaseDriver(httpClient, supabaseSettings.OrganizationSlug), nil
case coredata.ConnectorProviderGitHub:
githubSettings, err := coredata.ConnectorSettings[coredata.GitHubConnectorSettings](dbConnector)
if err != nil {
return nil, fmt.Errorf("cannot read github connector settings: %w", err)
}
if githubSettings.Organization == "" {
return nil, fmt.Errorf("github connector requires organization in settings")
}
return drivers.NewGitHubDriver(httpClient, githubSettings.Organization, e.logger.Named("github")), nil
case coredata.ConnectorProviderIntercom:
return drivers.NewIntercomDriver(httpClient), nil
@@ -379,27 +400,33 @@ func (e *ReviewEngine) resolveDriver(
if err != nil {
return nil, fmt.Errorf("cannot read gitlab connector settings: %w", err)
}
if gitlabSettings.GroupID == "" {
return nil, fmt.Errorf("gitlab connector requires group_id in settings")
}
return drivers.NewGitLabDriver(httpClient, gitlabSettings.GroupID), nil
case coredata.ConnectorProviderBitbucket:
bitbucketSettings, err := coredata.ConnectorSettings[coredata.BitbucketConnectorSettings](dbConnector)
if err != nil {
return nil, fmt.Errorf("cannot read bitbucket connector settings: %w", err)
}
if bitbucketSettings.Workspace == "" {
return nil, fmt.Errorf("bitbucket connector requires workspace in settings")
}
return drivers.NewBitbucketDriver(httpClient, bitbucketSettings.Workspace), nil
case coredata.ConnectorProviderHeroku:
herokuSettings, err := coredata.ConnectorSettings[coredata.HerokuConnectorSettings](dbConnector)
if err != nil {
return nil, fmt.Errorf("cannot read heroku connector settings: %w", err)
}
if herokuSettings.TeamID == "" {
return nil, fmt.Errorf("heroku connector requires team_id in settings")
}
return drivers.NewHerokuDriver(httpClient, herokuSettings.TeamID), nil
case coredata.ConnectorProviderPagerDuty:
// PagerDuty's REST API uses the regional api.pagerduty.com host;
@@ -413,36 +440,44 @@ func (e *ReviewEngine) resolveDriver(
if err != nil {
return nil, fmt.Errorf("cannot read asana connector settings: %w", err)
}
if asanaSettings.WorkspaceGID == "" {
return nil, fmt.Errorf("asana connector requires workspace_gid in settings")
}
return drivers.NewAsanaDriver(httpClient, asanaSettings.WorkspaceGID), nil
case coredata.ConnectorProviderNetlify:
netlifySettings, err := coredata.ConnectorSettings[coredata.NetlifyConnectorSettings](dbConnector)
if err != nil {
return nil, fmt.Errorf("cannot read netlify connector settings: %w", err)
}
if netlifySettings.AccountSlug == "" {
return nil, fmt.Errorf("netlify connector requires account_slug in settings")
}
return drivers.NewNetlifyDriver(httpClient, netlifySettings.AccountSlug), nil
case coredata.ConnectorProviderClickUp:
clickupSettings, err := coredata.ConnectorSettings[coredata.ClickUpConnectorSettings](dbConnector)
if err != nil {
return nil, fmt.Errorf("cannot read clickup connector settings: %w", err)
}
if clickupSettings.TeamID == "" {
return nil, fmt.Errorf("clickup connector requires team_id in settings")
}
return drivers.NewClickUpDriver(httpClient, clickupSettings.TeamID), nil
case coredata.ConnectorProviderVercel:
vercelSettings, err := coredata.ConnectorSettings[coredata.VercelConnectorSettings](dbConnector)
if err != nil {
return nil, fmt.Errorf("cannot read vercel connector settings: %w", err)
}
if vercelSettings.TeamID == "" {
return nil, fmt.Errorf("vercel connector requires team_id in settings")
}
return drivers.NewVercelDriver(httpClient, vercelSettings.TeamID), nil
case coredata.ConnectorProviderMonday:
return drivers.NewMondayDriver(httpClient), nil

View File

@@ -78,6 +78,7 @@ func NewService(
} else {
fetchWorkerOpts = append(fetchWorkerOpts, worker.WithInterval(30*time.Second))
}
fetchWorkerOpts = append(fetchWorkerOpts, worker.WithMaxConcurrency(20))
s.fetchWorker = NewSourceFetchWorker(
@@ -137,11 +138,14 @@ func (s *Service) ResolveEntryOrganizationID(ctx context.Context, entryID gid.GI
ctx,
func(ctx context.Context, conn pg.Querier) error {
var err error
entry := &coredata.AccessEntry{}
organizationID, err = entry.LoadOrganizationID(ctx, conn, entryID)
if err != nil {
return fmt.Errorf("cannot load organization id: %w", err)
}
return nil
},
)

View File

@@ -79,6 +79,7 @@ func (h *sourceNameHandler) Claim(ctx context.Context) (coredata.AccessSource, e
if errors.Is(err, coredata.ErrNoAccessSourceNameSyncAvailable) {
return coredata.AccessSource{}, worker.ErrNoTask
}
return coredata.AccessSource{}, err
}
@@ -128,6 +129,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS
}
resolver = h.buildResolver(&dbConnector, httpClient)
return nil
},
)
@@ -136,6 +138,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS
log.String("source_id", source.ID.String()),
log.Error(err),
)
return nil
}
@@ -144,6 +147,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS
log.String("source_id", source.ID.String()),
log.String("provider", dbConnector.Provider.String()),
)
return h.markNameSynced(ctx, &source)
}
@@ -157,6 +161,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS
log.String("provider", dbConnector.Provider.String()),
log.Error(err),
)
return fmt.Errorf("cannot resolve instance name for source %s: %w", source.ID, err)
}
@@ -165,6 +170,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS
log.String("source_id", source.ID.String()),
log.String("provider", dbConnector.Provider.String()),
)
return h.markNameSynced(ctx, &source)
}
@@ -178,6 +184,7 @@ func (h *sourceNameHandler) Process(ctx context.Context, source coredata.AccessS
)
source.Name = newName
return h.markNameSynced(ctx, &source)
}
@@ -247,6 +254,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read tally connector settings", log.Error(err))
return nil
}
return drivers.NewTallyNameResolver(httpClient, tallySettings.OrganizationID)
case coredata.ConnectorProviderHubSpot:
return drivers.NewHubSpotNameResolver(httpClient)
@@ -260,6 +268,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read sentry connector settings", log.Error(err))
return nil
}
return drivers.NewSentryNameResolver(httpClient, sentrySettings.OrganizationSlug)
case coredata.ConnectorProviderGitHub:
githubSettings, err := coredata.ConnectorSettings[coredata.GitHubConnectorSettings](dbConnector)
@@ -267,6 +276,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read github connector settings", log.Error(err))
return nil
}
return drivers.NewGitHubNameResolver(httpClient, githubSettings.Organization)
case coredata.ConnectorProviderSupabase:
supabaseSettings, err := coredata.ConnectorSettings[coredata.SupabaseConnectorSettings](dbConnector)
@@ -274,6 +284,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read supabase connector settings", log.Error(err))
return nil
}
return drivers.NewSupabaseNameResolver(supabaseSettings.OrganizationSlug)
case coredata.ConnectorProviderIntercom:
return drivers.NewIntercomNameResolver(httpClient)
@@ -289,6 +300,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read gitlab connector settings", log.Error(err))
return nil
}
return drivers.NewGitLabNameResolver(httpClient, gitlabSettings.GroupID)
case coredata.ConnectorProviderBitbucket:
bitbucketSettings, err := coredata.ConnectorSettings[coredata.BitbucketConnectorSettings](dbConnector)
@@ -296,6 +308,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read bitbucket connector settings", log.Error(err))
return nil
}
return drivers.NewBitbucketNameResolver(httpClient, bitbucketSettings.Workspace)
case coredata.ConnectorProviderHeroku:
herokuSettings, err := coredata.ConnectorSettings[coredata.HerokuConnectorSettings](dbConnector)
@@ -303,6 +316,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read heroku connector settings", log.Error(err))
return nil
}
return drivers.NewHerokuNameResolver(httpClient, herokuSettings.TeamID)
case coredata.ConnectorProviderPagerDuty:
pdSettings, err := coredata.ConnectorSettings[coredata.PagerDutyConnectorSettings](dbConnector)
@@ -310,6 +324,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read pagerduty connector settings", log.Error(err))
return nil
}
return drivers.NewPagerDutyNameResolver(pdSettings.Subdomain)
case coredata.ConnectorProviderAsana:
asanaSettings, err := coredata.ConnectorSettings[coredata.AsanaConnectorSettings](dbConnector)
@@ -317,6 +332,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read asana connector settings", log.Error(err))
return nil
}
return drivers.NewAsanaNameResolver(httpClient, asanaSettings.WorkspaceGID)
case coredata.ConnectorProviderNetlify:
netlifySettings, err := coredata.ConnectorSettings[coredata.NetlifyConnectorSettings](dbConnector)
@@ -324,6 +340,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read netlify connector settings", log.Error(err))
return nil
}
return drivers.NewNetlifyNameResolver(httpClient, netlifySettings.AccountSlug)
case coredata.ConnectorProviderClickUp:
clickupSettings, err := coredata.ConnectorSettings[coredata.ClickUpConnectorSettings](dbConnector)
@@ -331,6 +348,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read clickup connector settings", log.Error(err))
return nil
}
return drivers.NewClickUpNameResolver(httpClient, clickupSettings.TeamID)
case coredata.ConnectorProviderVercel:
vercelSettings, err := coredata.ConnectorSettings[coredata.VercelConnectorSettings](dbConnector)
@@ -338,6 +356,7 @@ func (h *sourceNameHandler) buildResolver(
h.logger.Error("cannot read vercel connector settings", log.Error(err))
return nil
}
return drivers.NewVercelNameResolver(httpClient, vercelSettings.TeamID)
case coredata.ConnectorProviderMonday:
return drivers.NewMondayNameResolver(httpClient)

View File

@@ -77,12 +77,14 @@ func (h *sourceFetchHandler) Claim(ctx context.Context) (coredata.AccessReviewCa
if err := sourceFetch.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update source fetch status: %w", err)
}
return nil
},
); err != nil {
if errors.Is(err, coredata.ErrNoAccessReviewCampaignSourceFetchAvailable) {
return coredata.AccessReviewCampaignSourceFetch{}, worker.ErrNoTask
}
return coredata.AccessReviewCampaignSourceFetch{}, fmt.Errorf("cannot claim source fetch: %w", err)
}
@@ -101,6 +103,7 @@ func (h *sourceFetchHandler) RecoverStale(ctx context.Context) error {
ctx,
func(ctx context.Context, tx pg.Tx) error {
var fetches coredata.AccessReviewCampaignSourceFetches
count, err := fetches.RecoverStale(ctx, tx, staleThreshold, now)
if err != nil {
return fmt.Errorf("cannot recover stale source fetches: %w", err)
@@ -135,6 +138,7 @@ func (h *sourceFetchHandler) handle(
if commitErr != nil {
return fmt.Errorf("cannot load campaign: %w, and cannot commit failed source fetch: %w", err, commitErr)
}
return fmt.Errorf("cannot load campaign: %w", err)
}
@@ -148,6 +152,7 @@ func (h *sourceFetchHandler) handle(
if finalizeErr := h.finalizeCampaignFetchLifecycle(ctx, sourceFetch.TenantID, sourceFetch.AccessReviewCampaignID); finalizeErr != nil {
return fmt.Errorf("cannot finalize campaign after failed source fetch: %w", finalizeErr)
}
return fmt.Errorf("cannot fetch source: %w", err)
}
@@ -250,6 +255,7 @@ func (h *sourceFetchHandler) finalizeCampaignFetchLifecycle(
campaign.Status = coredata.AccessReviewCampaignStatusPendingActions
campaign.UpdatedAt = time.Now()
return campaign.Update(ctx, tx, scope)
},
)

View File

@@ -124,6 +124,7 @@ func (a *Agent) Clone(opts ...Option) *Agent {
copy(newApproval.ToolNames, a.approval.ToolNames)
newApproval.toolNameSet = buildToolNameSet(newApproval.ToolNames)
}
cp.approval = &newApproval
}
@@ -205,6 +206,7 @@ func WithMaxTurns(n int) Option {
if n < 1 {
n = 1
}
a.maxTurns = n
}
}
@@ -217,6 +219,7 @@ func WithMaxEmptyOutputRetries(n int) Option {
if n < 0 {
n = 0
}
a.maxEmptyOutputRetries = n
}
}
@@ -226,6 +229,7 @@ func WithMaxToolDepth(n int) Option {
if n < 1 {
n = 1
}
a.maxToolDepth = n
}
}
@@ -348,6 +352,7 @@ func WithMCPServers(servers ...*MCPServer) Option {
func WithApproval(config ApprovalConfig) Option {
config.toolNameSet = buildToolNameSet(config.ToolNames)
return func(a *Agent) {
a.approval = &config
}
@@ -369,6 +374,7 @@ func (a *Agent) resolveTools(ctx context.Context) ([]ToolDescriptor, map[string]
if err != nil {
return nil, nil, fmt.Errorf("cannot resolve MCP tools from %q: %w", s.name, err)
}
for _, t := range mcpTools {
all = append(all, t)
}
@@ -380,6 +386,7 @@ func (a *Agent) resolveTools(ctx context.Context) ([]ToolDescriptor, map[string]
if _, exists := toolMap[name]; exists {
return nil, nil, fmt.Errorf("cannot resolve tools: duplicate tool name %q", name)
}
toolMap[name] = t
}

View File

@@ -35,8 +35,10 @@ func (m *mockProvider) ChatCompletion(_ context.Context, _ *llm.ChatCompletionRe
if m.calls >= len(m.responses) {
return nil, errors.New("no more mock responses")
}
resp := m.responses[m.calls]
m.calls++
return resp, nil
}
@@ -56,6 +58,7 @@ func (s *mockChatStream) Next() bool {
func (s *mockChatStream) Event() llm.ChatCompletionStreamEvent {
ev := s.events[s.pos]
s.pos++
return ev
}
@@ -89,8 +92,10 @@ func (p *mockMultiStreamProvider) ChatCompletionStream(_ context.Context, _ *llm
if p.calls >= len(p.streams) {
return nil, errors.New("no more mock streams")
}
s := p.streams[p.calls]
p.calls++
return s, nil
}
@@ -115,6 +120,7 @@ func (g *blockingGuardrail) Check(_ context.Context, messages []llm.Message) (*a
}
}
}
return nil, nil
}
@@ -129,6 +135,7 @@ func (g *outputBlocker) Check(_ context.Context, message llm.Message) (*agent.Gu
Message: "output blocked",
}, nil
}
return nil, nil
}
@@ -192,6 +199,7 @@ func (s *testSession) Load(_ context.Context, sessionID string) ([]llm.Message,
msgs := s.messages[sessionID]
cp := make([]llm.Message, len(msgs))
copy(cp, msgs)
return cp, nil
}
@@ -199,6 +207,7 @@ func (s *testSession) Save(_ context.Context, sessionID string, messages []llm.M
cp := make([]llm.Message, len(messages))
copy(cp, messages)
s.messages[sessionID] = cp
return nil
}
@@ -410,6 +419,7 @@ func TestRun(t *testing.T) {
}
type Params struct{}
noopTool := agent.FunctionTool[Params](
"noop",
"No-op",
@@ -432,6 +442,7 @@ func TestRun(t *testing.T) {
)
require.Error(t, err)
var maxTurnsErr *agent.MaxTurnsExceededError
require.ErrorAs(t, err, &maxTurnsErr)
assert.Equal(t, 2, maxTurnsErr.MaxTurns)
@@ -444,6 +455,7 @@ func TestRun(t *testing.T) {
t.Parallel()
type Params struct{}
makeTool := func(name string) agent.Tool {
tool := agent.FunctionTool[Params](
name,
@@ -452,6 +464,7 @@ func TestRun(t *testing.T) {
return agent.ToolResult{Content: "ok"}, nil
},
)
return tool
}
@@ -600,11 +613,13 @@ func TestRun(t *testing.T) {
assert.Equal(t, "Both done.", result.FinalMessage().Text())
var toolMsgs []llm.Message
for _, m := range result.Messages {
if m.Role == llm.RoleTool {
toolMsgs = append(toolMsgs, m)
}
}
require.Len(t, toolMsgs, 2)
assert.Equal(t, "tc_1", toolMsgs[0].ToolCallID)
assert.Equal(t, "result_1", toolMsgs[0].Text())
@@ -667,11 +682,13 @@ func TestRun(t *testing.T) {
assert.Equal(t, "Handled both.", result.FinalMessage().Text())
var toolMsgs []llm.Message
for _, m := range result.Messages {
if m.Role == llm.RoleTool {
toolMsgs = append(toolMsgs, m)
}
}
require.Len(t, toolMsgs, 2)
assert.Equal(t, "tc_ok", toolMsgs[0].ToolCallID)
assert.Equal(t, "success_result", toolMsgs[0].Text())
@@ -692,12 +709,14 @@ func TestRun(t *testing.T) {
var capturedTenantID string
type Params struct{}
tool := agent.FunctionTool[Params](
"check_tenant",
"Check current tenant",
func(ctx context.Context, _ Params) (agent.ToolResult, error) {
rc := agent.RunContextFrom[*RequestContext](ctx)
capturedTenantID = rc.TenantID
return agent.ToolResult{Content: "tenant: " + rc.TenantID}, nil
},
)
@@ -912,11 +931,13 @@ func TestRun_Handoff(t *testing.T) {
specialist,
agent.WithHandoffInputFilter(func(data agent.HandoffInputData) []llm.Message {
var filtered []llm.Message
for _, m := range data.NewItems {
if m.Role == llm.RoleUser {
filtered = append(filtered, m)
}
}
return filtered
}),
),
@@ -1017,6 +1038,7 @@ func TestRun_Guardrails(t *testing.T) {
)
require.Error(t, err)
var tripErr *agent.InputGuardrailTrippedError
require.ErrorAs(t, err, &tripErr)
assert.Equal(t, "blocker", tripErr.Guardrail)
@@ -1048,6 +1070,7 @@ func TestRun_Guardrails(t *testing.T) {
)
require.Error(t, err)
var tripErr *agent.OutputGuardrailTrippedError
require.ErrorAs(t, err, &tripErr)
assert.Equal(t, "output_blocker", tripErr.Guardrail)
@@ -1064,6 +1087,7 @@ func TestRun_Hooks(t *testing.T) {
t.Parallel()
type Params struct{}
noopTool := agent.FunctionTool[Params](
"noop",
"No-op",
@@ -1348,6 +1372,7 @@ func TestRun_ToolUseBehavior(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool[Params](
"compute",
"Compute something",
@@ -1440,6 +1465,7 @@ func TestRun_ToolUseBehavior(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool[Params](
"noop",
"No-op",
@@ -1482,6 +1508,7 @@ func TestRun_ToolUseBehavior(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool[Params](
"compute",
"Compute something",
@@ -1594,6 +1621,7 @@ func TestRun_Approval(t *testing.T) {
)
require.Error(t, err)
var interrupted *agent.InterruptedError
require.ErrorAs(t, err, &interrupted)
assert.Len(t, interrupted.ToolCalls, 1)
@@ -2107,8 +2135,10 @@ func TestRunStreamed(t *testing.T) {
[]llm.Message{userMessage("Hi")},
)
var deltas []string
var gotComplete bool
var (
deltas []string
gotComplete bool
)
for ev := range sr.Events {
switch ev.Type {
@@ -2134,6 +2164,7 @@ func TestRunStreamed(t *testing.T) {
t.Parallel()
type Params struct{}
tool := agent.FunctionTool[Params](
"noop",
"No-op",
@@ -2186,6 +2217,7 @@ func TestRunStreamed(t *testing.T) {
)
var gotToolStart, gotToolEnd, gotComplete bool
for ev := range sr.Events {
switch ev.Type {
case agent.StreamEventToolStart:
@@ -2238,6 +2270,7 @@ func TestRunStreamed(t *testing.T) {
)
var gotComplete, gotError bool
for ev := range sr.Events {
switch ev.Type {
case agent.StreamEventComplete:
@@ -2287,22 +2320,29 @@ func TestRunStreamed(t *testing.T) {
)
var collected []agent.StreamEvent
done := make(chan struct{})
go func() {
defer close(done)
for ev := range sr.Events {
collected = append(collected, ev)
}
}()
result, err := sr.Wait()
<-done
require.NoError(t, err)
assert.Equal(t, "Hello world!", result.FinalMessage().Text())
var deltaCount int
var gotAgentStart, gotAgentEnd, gotComplete bool
var (
deltaCount int
gotAgentStart, gotAgentEnd, gotComplete bool
)
for _, ev := range collected {
switch ev.Type {
case agent.StreamEventLLMDelta:
@@ -2359,6 +2399,7 @@ func TestClone(t *testing.T) {
t.Parallel()
type Params struct{}
tool1 := agent.FunctionTool[Params](
"t1",
"desc",
@@ -2474,6 +2515,7 @@ func TestGenerateSchema_EmbeddedStruct(t *testing.T) {
ID string `json:"id" jsonschema:"unique identifier"`
Kind string `json:"kind"`
}
type Params struct {
Base
Name string `json:"name"`
@@ -2653,6 +2695,7 @@ func TestRun_UnknownToolCall(t *testing.T) {
}
type Params struct{}
tool := agent.FunctionTool[Params](
"real_tool",
"A real tool",
@@ -2746,6 +2789,7 @@ func TestClone_WithApprovalConfig(t *testing.T) {
)
require.Error(t, err)
var interrupted *agent.InterruptedError
require.ErrorAs(t, err, &interrupted)
assert.Len(t, interrupted.PendingApprovals, 1)
@@ -2771,6 +2815,7 @@ func TestRun_HandoffWithPreHandoffTools(t *testing.T) {
var executionOrder []string
type Params struct{}
tool1 := agent.FunctionTool[Params](
"prepare",
"Prepare data",
@@ -2830,6 +2875,7 @@ func TestRun_HandoffWithPreHandoffTools(t *testing.T) {
t.Parallel()
type Params struct{}
tool1 := agent.FunctionTool[Params](
"prepare",
"Prepare data",
@@ -2892,6 +2938,7 @@ func TestRun_HandoffWithPreHandoffTools(t *testing.T) {
assert.Equal(t, "specialist", result.LastAgent.Name())
var toolMsgs []llm.Message
for _, m := range result.Messages {
if m.Role == llm.RoleTool {
toolMsgs = append(toolMsgs, m)
@@ -2914,6 +2961,7 @@ func TestRun_HandoffWithPreHandoffTools(t *testing.T) {
t.Parallel()
type Params struct{}
failingTool := agent.FunctionTool[Params](
"prepare",
"Prepare data",

View File

@@ -47,6 +47,7 @@ func agentToolDepth(ctx context.Context) int {
if v, ok := ctx.Value(agentToolDepthKey{}).(int); ok {
return v
}
return 0
}
@@ -124,6 +125,7 @@ func (t *agentTool) Execute(ctx context.Context, arguments string) (ToolResult,
if len(preview) > 500 {
preview = preview[:500] + "... (truncated)"
}
return ToolResult{
Content: fmt.Sprintf("Sub-agent %q returned invalid JSON. Raw output:\n%s", t.agent.name, preview),
IsError: true,

Some files were not shown because too many files have changed in this diff Show More