-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReferenceProtector.IntegrationTests.cs
More file actions
356 lines (327 loc) · 12.9 KB
/
ReferenceProtector.IntegrationTests.cs
File metadata and controls
356 lines (327 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
using Xunit;
namespace ReferenceProtector.IntegrationTests;
/// <summary>
/// This class tests the consumption of the ReferenceProtector package.
/// </summary>
public class ReferenceProtectorIntegrationTests : TestBase
{
/// <summary>
/// Initializes a new instance of the <see cref="ReferenceProtectorIntegrationTests"/> class.
/// </summary>
public ReferenceProtectorIntegrationTests(ITestOutputHelper output) : base(output)
{
}
/// <summary>
/// Referencing the ReferenceProtector package without providing a DependencyRulesFile should produce a warning.
/// </summary>
[Fact]
public async Task PackageReference_ProducesWarning_Async()
{
CreateProject("A");
var warnings = await Build();
var warning = Assert.Single(warnings);
Assert.Equal(new Warning()
{
Message = "RP0001: Provide DependencyRulesFile property to specify valid dependency rules file. Current path: N/A.",
Project = "A/A.csproj",
}, warning);
}
/// <summary>
/// Referencing the ReferenceProtector package with a valid DependencyRulesFile should not produce any warnings.
/// </summary>
[Fact]
public async Task PackageReference_DependencyRules_NoWarnings_Async()
{
CreateProject("A");
var testRulesPath = Path.Combine(TestDirectory, "testRules.json");
File.WriteAllText(testRulesPath, """
{
}
""");
var warnings = await Build(additionalArgs:
$"/p:DependencyRulesFile={testRulesPath}");
Assert.Empty(warnings);
}
/// <summary>
/// Referencing the ReferenceProtector package with an invalid DependencyRulesFile should produce a warning.
/// </summary>
[Fact]
public async Task PackageReference_DependencyRulesInvalid_ProducesWarnings_Async()
{
CreateProject("A");
var testRulesPath = Path.Combine(TestDirectory, "testRules.json");
File.WriteAllText(testRulesPath, """
bad json
""");
var warnings = await Build(additionalArgs:
$"/p:DependencyRulesFile={testRulesPath}");
var warning = Assert.Single(warnings);
Assert.Equal(new Warning()
{
Message = $"RP0002: Make sure the dependency rules file '{testRulesPath}' is in the correct json format",
Project = "A/A.csproj",
}, warning);
}
/// <summary>
/// Validates that dependency policy violation will produce a build warning.
/// </summary>
[Fact]
public async Task PackageReference_DependencyRuleViolated_ProducesWarnings_Async()
{
var projectA = CreateProject("A");
var projectB = CreateProject("B");
await AddProjectReference("A", "B");
var testRulesPath = Path.Combine(TestDirectory, "testRules.json");
File.WriteAllText(testRulesPath, $$"""
{
"ProjectDependencies": [
{
"From": "*",
"To": "{{projectB.Replace("\\", "\\\\")}}",
"LinkType": "Direct",
"Policy": "Forbidden",
"Description": "test rule"
}
]
}
""");
var warnings = await Build(additionalArgs:
$"/p:DependencyRulesFile={testRulesPath}");
var warning = Assert.Single(warnings);
Assert.Equal(new Warning()
{
Message = $"RP0004: Project reference '{projectA}' ==> '{projectB}' violates dependency rule 'test rule' or one of its exceptions. Please remove the dependency or update '{testRulesPath}' file to allow it.",
Project = "A/A.csproj",
}, warning);
}
/// <summary>
/// Validates that dependency policy violation for package references will produce a build warning.
/// </summary>
[Fact]
public async Task PackageReference_PackageDependencyRuleViolated_ProducesWarnings_Async()
{
var projectA = CreateProject("A");
var packageX = "System.Text.Json";
await AddPackageReference("A", packageX);
var testRulesPath = Path.Combine(TestDirectory, "testRules.json");
File.WriteAllText(testRulesPath, $$"""
{
"PackageDependencies": [
{
"From": "*",
"To": "System.Text.*",
"Policy": "Forbidden",
"Description": "test rule"
}
]
}
""");
var warnings = await Build(additionalArgs:
$"/p:DependencyRulesFile={testRulesPath}");
var warning = Assert.Single(warnings);
Assert.Equal(new Warning()
{
Message = $"RP0005: Package reference '{projectA}' ==> '{packageX}' violates dependency rule 'test rule' or one of its exceptions. Please remove the dependency or update '{testRulesPath}' file to allow it.",
Project = "A/A.csproj",
}, warning);
}
/// <summary>
/// Validates that dependency policy violation will not produce a build warning when the feature is disabled.
/// </summary>
[Fact]
public async Task PackageReference_DependencyRuleViolated_FeatureDisabled_NoWarnings_Async()
{
var projectA = CreateProject("A");
var projectB = CreateProject("B");
await AddProjectReference("A", "B");
var testRulesPath = Path.Combine(TestDirectory, "testRules.json");
File.WriteAllText(testRulesPath, $$"""
{
"ProjectDependencies": [
{
"From": "*",
"To": "{{projectB.Replace("\\", "\\\\")}}",
"LinkType": "Direct",
"Policy": "Forbidden",
"Description": "test rule"
}
]
}
""");
var warnings = await Build(additionalArgs:
$"/p:DependencyRulesFile={testRulesPath} /p:EnableReferenceProtector=false");
Assert.Empty(warnings);
}
/// <summary>
/// Validates that a stale tech debt exception (one that no longer matches any declared reference) produces an RP0006 warning.
/// </summary>
[Fact]
public async Task TechDebtException_NoLongerNeeded_ProducesWarning_Async()
{
var projectA = CreateProject("A");
var projectB = CreateProject("B");
await AddProjectReference("A", "B");
var testRulesPath = Path.Combine(TestDirectory, "testRules.json");
File.WriteAllText(testRulesPath, $$"""
{
"ProjectDependencies": [
{
"From": "{{projectA.Replace("\\", "\\\\")}}",
"To": "*",
"LinkType": "Direct",
"Policy": "Forbidden",
"Description": "test rule",
"Exceptions": [
{
"From": "{{projectA.Replace("\\", "\\\\")}}",
"To": "{{projectB.Replace("\\", "\\\\")}}",
"Justification": "Current tech debt",
"IsTechDebt": true
},
{
"From": "{{projectA.Replace("\\", "\\\\")}}",
"To": "*OldProject.csproj",
"Justification": "Stale tech debt",
"IsTechDebt": true
}
]
}
]
}
""");
var warnings = await Build(additionalArgs:
$"/p:DependencyRulesFile={testRulesPath}");
// The exception for A→B still matches, so no RP0006 for it.
// The exception for A→*OldProject.csproj is stale, so RP0006 is expected.
var warning = Assert.Single(warnings);
Assert.Equal(new Warning()
{
Message = $"RP0006: Tech debt exception '{projectA}' ==> '*OldProject.csproj' in rule 'test rule' no longer matches any declared reference and can be removed from '{testRulesPath}'",
Project = "A/A.csproj",
}, warning);
}
/// <summary>
/// Validates that a tech debt exception that still matches a declared reference does NOT produce an RP0006 warning.
/// </summary>
[Fact]
public async Task TechDebtException_StillNeeded_NoWarning_Async()
{
var projectA = CreateProject("A");
var projectB = CreateProject("B");
await AddProjectReference("A", "B");
var testRulesPath = Path.Combine(TestDirectory, "testRules.json");
File.WriteAllText(testRulesPath, $$"""
{
"ProjectDependencies": [
{
"From": "{{projectA.Replace("\\", "\\\\")}}",
"To": "*",
"LinkType": "Direct",
"Policy": "Forbidden",
"Description": "test rule",
"Exceptions": [
{
"From": "{{projectA.Replace("\\", "\\\\")}}",
"To": "{{projectB.Replace("\\", "\\\\")}}",
"Justification": "Current tech debt - still needed",
"IsTechDebt": true
}
]
}
]
}
""");
var warnings = await Build(additionalArgs:
$"/p:DependencyRulesFile={testRulesPath}");
// The tech debt exception still matches A→B, so no RP0006.
// And the exception suppresses the RP0004 violation.
Assert.Empty(warnings);
}
/// <summary>
/// Validates that a tech debt exception scoped to a different project does NOT produce RP0006 during the current project's compilation.
/// </summary>
[Fact]
public async Task TechDebtException_ForDifferentProject_NoWarning_Async()
{
var projectA = CreateProject("A");
var projectB = CreateProject("B");
await AddProjectReference("A", "B");
var testRulesPath = Path.Combine(TestDirectory, "testRules.json");
// Use a From that references a project NOT in this solution (NonExistent.csproj),
// simulating a broad rule with tech debt exceptions for projects compiled separately.
File.WriteAllText(testRulesPath, $$"""
{
"ProjectDependencies": [
{
"From": "*",
"To": "*",
"LinkType": "Direct",
"Policy": "Forbidden",
"Description": "test rule",
"Exceptions": [
{
"From": "{{projectA.Replace("\\", "\\\\")}}",
"To": "{{projectB.Replace("\\", "\\\\")}}",
"Justification": "Tech debt for A",
"IsTechDebt": true
},
{
"From": "*NonExistent.csproj",
"To": "*SomeLib.csproj",
"Justification": "Tech debt for a project not in this compilation",
"IsTechDebt": true
}
]
}
]
}
""");
var warnings = await Build(additionalArgs:
$"/p:DependencyRulesFile={testRulesPath}");
// A→B is covered by the first exception (still needed), so no RP0004 or RP0006 for A.
// The second exception (NonExistent→SomeLib) doesn't match any project being compiled,
// so it should NOT trigger RP0006 from either A or B.
Assert.Empty(warnings);
}
/// <summary>
/// Validates that a non-tech-debt exception that no longer matches does NOT produce an RP0006 warning.
/// </summary>
[Fact]
public async Task NonTechDebtException_NoLongerNeeded_NoWarning_Async()
{
var projectA = CreateProject("A");
var projectB = CreateProject("B");
await AddProjectReference("A", "B");
var testRulesPath = Path.Combine(TestDirectory, "testRules.json");
File.WriteAllText(testRulesPath, $$"""
{
"ProjectDependencies": [
{
"From": "{{projectA.Replace("\\", "\\\\")}}",
"To": "*",
"LinkType": "Direct",
"Policy": "Forbidden",
"Description": "test rule",
"Exceptions": [
{
"From": "{{projectA.Replace("\\", "\\\\")}}",
"To": "{{projectB.Replace("\\", "\\\\")}}",
"Justification": "Legitimate exception"
},
{
"From": "{{projectA.Replace("\\", "\\\\")}}",
"To": "*OldProject.csproj",
"Justification": "Legitimate exception, not tech debt"
}
]
}
]
}
""");
var warnings = await Build(additionalArgs:
$"/p:DependencyRulesFile={testRulesPath}");
// No RP0006 because neither exception has IsTechDebt=true.
// A→B is covered by the first exception, so no RP0004 either.
Assert.Empty(warnings);
}
}