Skip to content

Commit 5c2b616

Browse files
committed
test: add coverage for mixed operators metadata helpers and warning lint
Add TestMixedOperatorsMetadata to pkg/namespace/metadata_test.go covering all code paths in HasMixedOperatorsWithoutParens, GetMixedOperatorsPosition, and SetMixedOperatorsWithoutParens (nil metadata, empty metadata, non-permission relation, permission with/without flag, set/clear operations, creation of new metadata entries). Add additional warning test cases to pkg/development/warnings_test.go for mixed intersection+exclusion, intersection+union operator combinations, same-operators-repeated (no warning), and relation-referencing-parent for a non-permission relation.
1 parent be4dcfe commit 5c2b616

File tree

2 files changed

+340
-0
lines changed

2 files changed

+340
-0
lines changed

pkg/development/warnings_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,70 @@ func TestWarnings(t *testing.T) {
312312
SourceCode: "view",
313313
},
314314
},
315+
{
316+
name: "mixed intersection and exclusion without parentheses",
317+
schema: `definition user {}
318+
319+
definition document {
320+
relation viewer: user
321+
relation editor: user
322+
relation admin: user
323+
permission view = viewer & editor - admin
324+
}
325+
`,
326+
expectedWarning: &developerv1.DeveloperWarning{
327+
Message: "Permission \"view\" mixes operators (union, intersection, exclusion) at the same level without explicit parentheses; consider adding parentheses to clarify precedence (mixed-operators-without-parentheses)",
328+
Line: 7,
329+
Column: 23,
330+
SourceCode: "view",
331+
},
332+
},
333+
{
334+
name: "mixed intersection and union without parentheses",
335+
schema: `definition user {}
336+
337+
definition document {
338+
relation viewer: user
339+
relation editor: user
340+
relation admin: user
341+
permission view = viewer & editor + admin
342+
}
343+
`,
344+
expectedWarning: &developerv1.DeveloperWarning{
345+
Message: "Permission \"view\" mixes operators (union, intersection, exclusion) at the same level without explicit parentheses; consider adding parentheses to clarify precedence (mixed-operators-without-parentheses)",
346+
Line: 7,
347+
Column: 32,
348+
SourceCode: "view",
349+
},
350+
},
351+
{
352+
name: "same operators repeated does not warn",
353+
schema: `definition user {}
354+
355+
definition document {
356+
relation viewer: user
357+
relation editor: user
358+
relation admin: user
359+
permission view = viewer + editor + admin
360+
}
361+
`,
362+
expectedWarning: nil,
363+
},
364+
{
365+
name: "relation name referencing parent is a relation not permission",
366+
schema: `definition user {}
367+
368+
definition document {
369+
relation viewer_document: user
370+
permission view = viewer_document
371+
}`,
372+
expectedWarning: &developerv1.DeveloperWarning{
373+
Message: "Relation \"viewer_document\" references parent type \"document\" in its name; it is recommended to drop the suffix (relation-name-references-parent)",
374+
Line: 4,
375+
Column: 5,
376+
SourceCode: "viewer_document",
377+
},
378+
},
315379
}
316380

317381
for _, tc := range tcs {

pkg/namespace/metadata_test.go

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,282 @@ func TestMetadata(t *testing.T) {
6161
require.Equal(iv1.RelationMetadata_PERMISSION, GetRelationKind(ns.Relation[0]))
6262
}
6363

64+
func TestMixedOperatorsMetadata(t *testing.T) {
65+
tests := []struct {
66+
name string
67+
setupRelation func() *core.Relation
68+
performSet bool
69+
setMixed bool
70+
setPosition *core.SourcePosition
71+
expectedHasMixed bool
72+
expectedPosition *core.SourcePosition
73+
}{
74+
{
75+
name: "has mixed returns false for nil metadata",
76+
setupRelation: func() *core.Relation {
77+
return &core.Relation{Name: "test"}
78+
},
79+
expectedHasMixed: false,
80+
expectedPosition: nil,
81+
},
82+
{
83+
name: "has mixed returns false for empty metadata",
84+
setupRelation: func() *core.Relation {
85+
return &core.Relation{
86+
Name: "test",
87+
Metadata: &core.Metadata{},
88+
}
89+
},
90+
expectedHasMixed: false,
91+
expectedPosition: nil,
92+
},
93+
{
94+
name: "has mixed returns false for non-permission relation",
95+
setupRelation: func() *core.Relation {
96+
relationMetadata := &iv1.RelationMetadata{Kind: iv1.RelationMetadata_RELATION}
97+
metadataAny, _ := anypb.New(relationMetadata)
98+
return &core.Relation{
99+
Name: "test",
100+
Metadata: &core.Metadata{
101+
MetadataMessage: []*anypb.Any{metadataAny},
102+
},
103+
}
104+
},
105+
expectedHasMixed: false,
106+
expectedPosition: nil,
107+
},
108+
{
109+
name: "has mixed returns false for permission without mixed flag",
110+
setupRelation: func() *core.Relation {
111+
relationMetadata := &iv1.RelationMetadata{Kind: iv1.RelationMetadata_PERMISSION}
112+
metadataAny, _ := anypb.New(relationMetadata)
113+
return &core.Relation{
114+
Name: "test",
115+
Metadata: &core.Metadata{
116+
MetadataMessage: []*anypb.Any{metadataAny},
117+
},
118+
}
119+
},
120+
expectedHasMixed: false,
121+
expectedPosition: nil,
122+
},
123+
{
124+
name: "has mixed returns true when flag is set",
125+
setupRelation: func() *core.Relation {
126+
relationMetadata := &iv1.RelationMetadata{
127+
Kind: iv1.RelationMetadata_PERMISSION,
128+
HasMixedOperatorsWithoutParentheses: true,
129+
MixedOperatorsPosition: &core.SourcePosition{
130+
ZeroIndexedLineNumber: 5,
131+
ZeroIndexedColumnPosition: 10,
132+
},
133+
}
134+
metadataAny, _ := anypb.New(relationMetadata)
135+
return &core.Relation{
136+
Name: "test",
137+
Metadata: &core.Metadata{
138+
MetadataMessage: []*anypb.Any{metadataAny},
139+
},
140+
}
141+
},
142+
expectedHasMixed: true,
143+
expectedPosition: &core.SourcePosition{
144+
ZeroIndexedLineNumber: 5,
145+
ZeroIndexedColumnPosition: 10,
146+
},
147+
},
148+
{
149+
name: "has mixed returns true with nil position",
150+
setupRelation: func() *core.Relation {
151+
relationMetadata := &iv1.RelationMetadata{
152+
Kind: iv1.RelationMetadata_PERMISSION,
153+
HasMixedOperatorsWithoutParentheses: true,
154+
}
155+
metadataAny, _ := anypb.New(relationMetadata)
156+
return &core.Relation{
157+
Name: "test",
158+
Metadata: &core.Metadata{
159+
MetadataMessage: []*anypb.Any{metadataAny},
160+
},
161+
}
162+
},
163+
expectedHasMixed: true,
164+
expectedPosition: nil,
165+
},
166+
{
167+
name: "set mixed on relation with no metadata",
168+
setupRelation: func() *core.Relation {
169+
return &core.Relation{Name: "test"}
170+
},
171+
performSet: true,
172+
setMixed: true,
173+
setPosition: &core.SourcePosition{
174+
ZeroIndexedLineNumber: 3,
175+
ZeroIndexedColumnPosition: 7,
176+
},
177+
expectedHasMixed: true,
178+
expectedPosition: &core.SourcePosition{
179+
ZeroIndexedLineNumber: 3,
180+
ZeroIndexedColumnPosition: 7,
181+
},
182+
},
183+
{
184+
name: "set mixed false on relation with no metadata is no-op",
185+
setupRelation: func() *core.Relation {
186+
return &core.Relation{Name: "test"}
187+
},
188+
performSet: true,
189+
setMixed: false,
190+
setPosition: nil,
191+
expectedHasMixed: false,
192+
expectedPosition: nil,
193+
},
194+
{
195+
name: "set mixed updates existing permission metadata",
196+
setupRelation: func() *core.Relation {
197+
relationMetadata := &iv1.RelationMetadata{
198+
Kind: iv1.RelationMetadata_PERMISSION,
199+
}
200+
metadataAny, _ := anypb.New(relationMetadata)
201+
return &core.Relation{
202+
Name: "test",
203+
Metadata: &core.Metadata{
204+
MetadataMessage: []*anypb.Any{metadataAny},
205+
},
206+
}
207+
},
208+
performSet: true,
209+
setMixed: true,
210+
setPosition: &core.SourcePosition{
211+
ZeroIndexedLineNumber: 2,
212+
ZeroIndexedColumnPosition: 15,
213+
},
214+
expectedHasMixed: true,
215+
expectedPosition: &core.SourcePosition{
216+
ZeroIndexedLineNumber: 2,
217+
ZeroIndexedColumnPosition: 15,
218+
},
219+
},
220+
{
221+
name: "set mixed on relation with non-permission metadata creates new entry",
222+
setupRelation: func() *core.Relation {
223+
docComment := &iv1.DocComment{Comment: "test comment"}
224+
docAny, _ := anypb.New(docComment)
225+
return &core.Relation{
226+
Name: "test",
227+
Metadata: &core.Metadata{
228+
MetadataMessage: []*anypb.Any{docAny},
229+
},
230+
}
231+
},
232+
performSet: true,
233+
setMixed: true,
234+
setPosition: &core.SourcePosition{
235+
ZeroIndexedLineNumber: 1,
236+
ZeroIndexedColumnPosition: 5,
237+
},
238+
expectedHasMixed: true,
239+
expectedPosition: &core.SourcePosition{
240+
ZeroIndexedLineNumber: 1,
241+
ZeroIndexedColumnPosition: 5,
242+
},
243+
},
244+
{
245+
name: "set mixed false clears existing flag",
246+
setupRelation: func() *core.Relation {
247+
relationMetadata := &iv1.RelationMetadata{
248+
Kind: iv1.RelationMetadata_PERMISSION,
249+
HasMixedOperatorsWithoutParentheses: true,
250+
MixedOperatorsPosition: &core.SourcePosition{
251+
ZeroIndexedLineNumber: 5,
252+
ZeroIndexedColumnPosition: 10,
253+
},
254+
}
255+
metadataAny, _ := anypb.New(relationMetadata)
256+
return &core.Relation{
257+
Name: "test",
258+
Metadata: &core.Metadata{
259+
MetadataMessage: []*anypb.Any{metadataAny},
260+
},
261+
}
262+
},
263+
performSet: true,
264+
setMixed: false,
265+
setPosition: nil,
266+
expectedHasMixed: false,
267+
expectedPosition: nil,
268+
},
269+
{
270+
name: "metadata with doc comment before relation metadata",
271+
setupRelation: func() *core.Relation {
272+
docComment := &iv1.DocComment{Comment: "a comment"}
273+
docAny, _ := anypb.New(docComment)
274+
relationMetadata := &iv1.RelationMetadata{
275+
Kind: iv1.RelationMetadata_PERMISSION,
276+
HasMixedOperatorsWithoutParentheses: true,
277+
MixedOperatorsPosition: &core.SourcePosition{
278+
ZeroIndexedLineNumber: 8,
279+
ZeroIndexedColumnPosition: 20,
280+
},
281+
}
282+
metadataAny, _ := anypb.New(relationMetadata)
283+
return &core.Relation{
284+
Name: "test",
285+
Metadata: &core.Metadata{
286+
MetadataMessage: []*anypb.Any{docAny, metadataAny},
287+
},
288+
}
289+
},
290+
expectedHasMixed: true,
291+
expectedPosition: &core.SourcePosition{
292+
ZeroIndexedLineNumber: 8,
293+
ZeroIndexedColumnPosition: 20,
294+
},
295+
},
296+
{
297+
name: "set mixed false on relation with non-permission metadata only",
298+
setupRelation: func() *core.Relation {
299+
relationMetadata := &iv1.RelationMetadata{Kind: iv1.RelationMetadata_RELATION}
300+
metadataAny, _ := anypb.New(relationMetadata)
301+
return &core.Relation{
302+
Name: "test",
303+
Metadata: &core.Metadata{
304+
MetadataMessage: []*anypb.Any{metadataAny},
305+
},
306+
}
307+
},
308+
performSet: true,
309+
setMixed: false,
310+
setPosition: nil,
311+
expectedHasMixed: false,
312+
expectedPosition: nil,
313+
},
314+
}
315+
316+
for _, tt := range tests {
317+
t.Run(tt.name, func(t *testing.T) {
318+
relation := tt.setupRelation()
319+
320+
if tt.performSet {
321+
err := SetMixedOperatorsWithoutParens(relation, tt.setMixed, tt.setPosition)
322+
require.NoError(t, err)
323+
}
324+
325+
hasMixed := HasMixedOperatorsWithoutParens(relation)
326+
require.Equal(t, tt.expectedHasMixed, hasMixed)
327+
328+
position := GetMixedOperatorsPosition(relation)
329+
if tt.expectedPosition == nil {
330+
require.Nil(t, position)
331+
} else {
332+
require.NotNil(t, position)
333+
require.Equal(t, tt.expectedPosition.ZeroIndexedLineNumber, position.ZeroIndexedLineNumber)
334+
require.Equal(t, tt.expectedPosition.ZeroIndexedColumnPosition, position.ZeroIndexedColumnPosition)
335+
}
336+
})
337+
}
338+
}
339+
64340
func TestTypeAnnotations(t *testing.T) {
65341
tests := []struct {
66342
name string

0 commit comments

Comments
 (0)