20
.golangci.yml
Normal file
20
.golangci.yml
Normal 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
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ import (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testutil.Setup()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
testutil.Teardown()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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 := `
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 := `
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -32,9 +32,11 @@ func ProseMirrorTextDoc(text string) string {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -23,7 +23,9 @@ import (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testutil.Setup()
|
||||
|
||||
code := m.Run()
|
||||
|
||||
testutil.Teardown()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ func TestMCP_ListThirdPartyContacts(t *testing.T) {
|
||||
"email": factory.SafeEmail(),
|
||||
}, &result)
|
||||
require.NotEmpty(t, result.ThirdPartyContact.ID)
|
||||
|
||||
_ = i
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,7 @@ func TestMCP_ListThirdPartyServices(t *testing.T) {
|
||||
"name": factory.SafeName("Service"),
|
||||
}, &result)
|
||||
require.NotEmpty(t, result.ThirdPartyService.ID)
|
||||
|
||||
_ = i
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user