Skip to content

Commit 65e38db

Browse files
committed
fix(gitlab): treat access level 0 ("No access") as a provided value
A runtime reference can resolve to the integer 0, but the block still gated access-level presence with truthiness: required ops rejected 0 as missing and optional ops silently omitted it. Add hasGitLabAccessLevel(value) (0 and '0' are provided; undefined/null/'' are not) and use it for all six presence gates so "No access" can be granted or set via a reference.
1 parent 7531008 commit 65e38db

4 files changed

Lines changed: 88 additions & 8 deletions

File tree

apps/sim/blocks/blocks/gitlab.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,43 @@ describe('GitLabBlock access operations', () => {
6969
expect(typeof byName?.accessLevel).toBe('number')
7070
})
7171

72+
it('accepts a runtime-resolved numeric 0 ("No access") on a required op', () => {
73+
// A reference can resolve to the number 0; a truthiness guard would wrongly
74+
// reject it as missing. It must pass and coerce to 0.
75+
const byZero = block.tools.config.params?.({
76+
accessToken: 'pat',
77+
operation: 'gitlab_add_member',
78+
resourceType: 'group',
79+
resourceId: '42',
80+
userId: '7',
81+
accessLevel: 0,
82+
})
83+
expect(byZero).toMatchObject({ userId: 7, accessLevel: 0 })
84+
expect(byZero?.accessLevel).toBe(0)
85+
})
86+
87+
it('sends a numeric 0 on optional ops instead of silently omitting it', () => {
88+
const approve = block.tools.config.params?.({
89+
accessToken: 'pat',
90+
operation: 'gitlab_approve_access_request',
91+
resourceType: 'group',
92+
resourceId: '42',
93+
userId: '7',
94+
accessLevel: 0,
95+
})
96+
expect(approve?.accessLevel).toBe(0)
97+
98+
const invite = block.tools.config.params?.({
99+
accessToken: 'pat',
100+
operation: 'gitlab_update_invitation',
101+
resourceType: 'group',
102+
resourceId: '42',
103+
email: 'a@b.com',
104+
invitationAccessLevel: 0,
105+
})
106+
expect(invite?.accessLevel).toBe(0)
107+
})
108+
72109
it('throws loudly when a resolved access level is not a valid GitLab level', () => {
73110
expect(() =>
74111
block.tools.config.params?.({

apps/sim/blocks/blocks/gitlab.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { GitLabIcon } from '@/components/icons'
22
import type { BlockConfig, BlockMeta } from '@/blocks/types'
33
import { AuthMode, IntegrationType } from '@/blocks/types'
44
import type { GitLabResponse } from '@/tools/gitlab/types'
5-
import { coerceGitLabAccessLevel, GITLAB_ACCESS_LEVEL_OPTIONS } from '@/tools/gitlab/utils'
5+
import {
6+
coerceGitLabAccessLevel,
7+
GITLAB_ACCESS_LEVEL_OPTIONS,
8+
hasGitLabAccessLevel,
9+
} from '@/tools/gitlab/utils'
610
import { getTrigger } from '@/triggers'
711

812
/**
@@ -2498,7 +2502,7 @@ Return ONLY the JSON array - no explanations, no extra text.`,
24982502
if (
24992503
!params.resourceId?.trim() ||
25002504
(!addMemberUserId && !params.username?.trim()) ||
2501-
!params.accessLevel
2505+
!hasGitLabAccessLevel(params.accessLevel)
25022506
) {
25032507
throw new Error(
25042508
'Project / Group ID, User ID (or Username), and Access Level are required.'
@@ -2517,7 +2521,11 @@ Return ONLY the JSON array - no explanations, no extra text.`,
25172521
}
25182522

25192523
case 'gitlab_update_member':
2520-
if (!params.resourceId?.trim() || !params.userId || !params.memberAccessLevel) {
2524+
if (
2525+
!params.resourceId?.trim() ||
2526+
!params.userId ||
2527+
!hasGitLabAccessLevel(params.memberAccessLevel)
2528+
) {
25212529
throw new Error('Project / Group ID, User ID, and Access Level are required.')
25222530
}
25232531
return {
@@ -2544,7 +2552,11 @@ Return ONLY the JSON array - no explanations, no extra text.`,
25442552
}
25452553

25462554
case 'gitlab_invite_member':
2547-
if (!params.resourceId?.trim() || !params.email?.trim() || !params.accessLevel) {
2555+
if (
2556+
!params.resourceId?.trim() ||
2557+
!params.email?.trim() ||
2558+
!hasGitLabAccessLevel(params.accessLevel)
2559+
) {
25482560
throw new Error('Project / Group ID, Email, and Access Level are required.')
25492561
}
25502562
return {
@@ -2576,7 +2588,7 @@ Return ONLY the JSON array - no explanations, no extra text.`,
25762588
throw new Error('Project / Group ID and Email are required.')
25772589
}
25782590
if (
2579-
!params.invitationAccessLevel &&
2591+
!hasGitLabAccessLevel(params.invitationAccessLevel) &&
25802592
!params.expiresAt?.trim() &&
25812593
!params.clearExpiresAt
25822594
) {
@@ -2591,7 +2603,7 @@ Return ONLY the JSON array - no explanations, no extra text.`,
25912603
email: params.email.trim(),
25922604
// Only send access_level when a level is chosen; "Leave unchanged"
25932605
// ('') keeps the invitation's current level instead of resetting it.
2594-
accessLevel: params.invitationAccessLevel
2606+
accessLevel: hasGitLabAccessLevel(params.invitationAccessLevel)
25952607
? coerceGitLabAccessLevel(params.invitationAccessLevel)
25962608
: undefined,
25972609
expiresAt: params.clearExpiresAt ? '' : params.expiresAt?.trim() || undefined,
@@ -2629,7 +2641,7 @@ Return ONLY the JSON array - no explanations, no extra text.`,
26292641
resourceType: params.resourceType || 'project',
26302642
resourceId: params.resourceId.trim(),
26312643
userId: Number(params.userId),
2632-
accessLevel: params.accessLevel
2644+
accessLevel: hasGitLabAccessLevel(params.accessLevel)
26332645
? coerceGitLabAccessLevel(params.accessLevel)
26342646
: undefined,
26352647
}
@@ -2655,7 +2667,11 @@ Return ONLY the JSON array - no explanations, no extra text.`,
26552667
}
26562668

26572669
case 'gitlab_add_saml_group_link':
2658-
if (!params.groupId?.trim() || !params.samlGroupName?.trim() || !params.accessLevel) {
2670+
if (
2671+
!params.groupId?.trim() ||
2672+
!params.samlGroupName?.trim() ||
2673+
!hasGitLabAccessLevel(params.accessLevel)
2674+
) {
26592675
throw new Error('Group ID, SAML Group Name, and Access Level are required.')
26602676
}
26612677
return {

apps/sim/tools/gitlab/utils.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
coerceGitLabAccessLevel,
77
getGitLabApiBase,
88
getGitLabResourcePath,
9+
hasGitLabAccessLevel,
910
InvalidGitLabAccessLevelError,
1011
normalizeGitLabHost,
1112
UnsafeGitLabHostError,
@@ -136,3 +137,18 @@ describe('coerceGitLabAccessLevel', () => {
136137
expect(() => coerceGitLabAccessLevel('boss')).toThrow(/Developer \(30\)/)
137138
})
138139
})
140+
141+
describe('hasGitLabAccessLevel', () => {
142+
it('treats numeric zero ("No access") and its string form as provided', () => {
143+
expect(hasGitLabAccessLevel(0)).toBe(true)
144+
expect(hasGitLabAccessLevel('0')).toBe(true)
145+
expect(hasGitLabAccessLevel(30)).toBe(true)
146+
expect(hasGitLabAccessLevel('Developer')).toBe(true)
147+
})
148+
149+
it('treats undefined, null, and the empty-string sentinel as not provided', () => {
150+
expect(hasGitLabAccessLevel(undefined)).toBe(false)
151+
expect(hasGitLabAccessLevel(null)).toBe(false)
152+
expect(hasGitLabAccessLevel('')).toBe(false)
153+
})
154+
})

apps/sim/tools/gitlab/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,17 @@ export class InvalidGitLabAccessLevelError extends Error {
113113
}
114114
}
115115

116+
/**
117+
* Whether an access-level field carries a value to send. Distinguishes a real
118+
* level - including numeric `0` ("No access") - from "not provided" (undefined,
119+
* null, or the empty-string "leave unchanged" sentinel). A bare truthiness check
120+
* would wrongly treat a runtime-resolved `0` as absent, rejecting it on required
121+
* operations and silently omitting it on optional ones.
122+
*/
123+
export function hasGitLabAccessLevel(value: unknown): boolean {
124+
return value !== undefined && value !== null && value !== ''
125+
}
126+
116127
/**
117128
* Coerces a runtime access-level value to the GitLab integer. Accepts an integer
118129
* (`30`), a numeric string (`'30'`), or a level name (`'Developer'`,

0 commit comments

Comments
 (0)