• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

samsmithnz / RepoGovernance / 16896588119

12 Aug 2025 01:36AM UTC coverage: 45.093% (+0.9%) from 44.176%
16896588119

push

github

web-flow
Fix isContributor parameter passing throughout application navigation (#1003)

* Initial plan

* Implement isContributor parameter passing throughout navigation

Co-authored-by: samsmithnz <8389039+samsmithnz@users.noreply.github.com>

* Complete isContributor parameter implementation with testing guide

Co-authored-by: samsmithnz <8389039+samsmithnz@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: samsmithnz <8389039+samsmithnz@users.noreply.github.com>

423 of 1186 branches covered (35.67%)

Branch coverage included in aggregate %.

14 of 90 new or added lines in 5 files covered. (15.56%)

3 existing lines in 3 files now uncovered.

1098 of 2187 relevant lines covered (50.21%)

25.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

49.03
/src/RepoGovernance.Web/Controllers/HomeController.cs
1
using Microsoft.AspNetCore.Mvc;
2
using RepoAutomation.Core.Models;
3
using RepoGovernance.Core.Models;
4
using RepoGovernance.Web.Models;
5
using RepoGovernance.Web.Services;
6
using System.Diagnostics;
7
using System.Text.RegularExpressions;
8

9
namespace RepoGovernance.Web.Controllers;
10

11
public class HomeController : Controller
12
{
13
    private readonly ISummaryItemsServiceApiClient _ServiceApiClient;
14

15
    public HomeController(ISummaryItemsServiceApiClient ServiceApiClient)
19✔
16
    {
19✔
17
        _ServiceApiClient = ServiceApiClient;
19✔
18
    }
19✔
19

20
    /// <summary>
21
    /// Validates that a repository name is safe for use in URL fragments.
22
    /// GitHub repository names can only contain alphanumeric characters, hyphens, underscores, and periods.
23
    /// </summary>
24
    /// <param name="repoName">The repository name to validate</param>
25
    /// <returns>True if the repository name is safe, false otherwise</returns>
26
    private static bool IsValidRepoName(string? repoName)
27
    {
×
28
        if (string.IsNullOrEmpty(repoName))
×
29
            return false;
×
30
        
31
        // GitHub repository names can only contain alphanumeric characters, hyphens, underscores, and periods
32
        // and cannot start or end with special characters
33
        return Regex.IsMatch(repoName, @"^[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$");
×
34
    }
×
35

36
    public async Task<IActionResult> Index(bool isContributor = false)
37
    {
2✔
38
        if (!ModelState.IsValid)
2!
39
        {
×
40
            return RedirectToAction("Index");
×
41
        }
42

43
        ViewBag.IsContributor = isContributor;
2✔
44

45
        string currentUser = "samsmithnz";
2✔
46
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(currentUser);
2✔
47
        List<RepoLanguage> repoLanguages = new();
2✔
48
        Dictionary<string, int> repoLanguagesDictonary = new();
2✔
49
        int total = 0;
2✔
50
        foreach (SummaryItem summaryItem in summaryItems)
6!
51
        {
×
52
            foreach (RepoLanguage repoLanguage in summaryItem.RepoLanguages)
×
53
            {
×
54
                total += repoLanguage.Total;
×
55
                if (repoLanguage.Name != null)
×
56
                {
×
57
                    if (repoLanguagesDictonary.ContainsKey(repoLanguage.Name))
×
58
                    {
×
59
                        repoLanguagesDictonary[repoLanguage.Name] += repoLanguage.Total;
×
60
                    }
×
61
                    else
62
                    {
×
63
                        repoLanguagesDictonary.Add(repoLanguage.Name, repoLanguage.Total);
×
64
                    }
×
65
                    if (repoLanguages.Find(x => x.Name == repoLanguage.Name) == null)
×
66
                    {
×
67
                        repoLanguages.Add(new RepoLanguage
×
68
                        {
×
69
                            Name = repoLanguage.Name,
×
70
                            Total = repoLanguage.Total,
×
71
                            Color = repoLanguage.Color,
×
72
                            Percent = repoLanguage.Percent
×
73
                        });
×
74
                    }
×
75
                }
×
76
            }
×
77
        }
×
78
        //Update the percent
79
        foreach (KeyValuePair<string, int> sortedLanguage in repoLanguagesDictonary.OrderByDescending(x => x.Value))
6!
80
        {
×
81
            RepoLanguage? repoLanguage = repoLanguages.Find(x => x.Name == sortedLanguage.Key);
×
82
            if (repoLanguage != null)
×
83
            {
×
84
                repoLanguage.Total = sortedLanguage.Value;
×
85
                repoLanguage.Percent = Math.Round((decimal)repoLanguage.Total / (decimal)total * 100M, 1);
×
86
            }
×
87
        }
×
88

89
        SummaryItemsIndex summaryItemsIndex = new()
2✔
90
        {
2✔
91
            SummaryItems = summaryItems,
2✔
92
            SummaryRepoLanguages = repoLanguages.OrderByDescending(x => x.Total).ToList(),
×
93
            IsContributor = isContributor
2✔
94
        };
2✔
95
        return View(summaryItemsIndex);
2✔
96
    }
2✔
97

98
    public async Task<IActionResult> Details(string user, string owner, string repo, bool isContributor = false)
99
    {
2✔
100
        if (!ModelState.IsValid)
2!
101
        {
×
NEW
102
            if (isContributor)
×
NEW
103
            {
×
NEW
104
                return RedirectToAction("Index", new { isContributor = true });
×
105
            }
106
            else
NEW
107
            {
×
NEW
108
                return RedirectToAction("Index");
×
109
            }
110
        }
111

112
        SummaryItem result = null;
2✔
113
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(user);
2✔
114
        foreach (SummaryItem summaryItem in summaryItems)
8✔
115
        {
2✔
116
            if (summaryItem.Owner == owner && summaryItem.Repo == repo)
2!
117
            {
2✔
118
                result = summaryItem;
2✔
119
                break;
2✔
120
            }
121
        }
×
122

123
        ViewBag.IsContributor = isContributor;
2✔
124

125
        SummaryItemDetails summaryItemDetails = new()
2✔
126
        {
2✔
127
            SummaryItem = result,
2✔
128
            IsContributor = isContributor
2✔
129
        };
2✔
130

131
        return View(summaryItemDetails);
2✔
132
    }
2✔
133

134

135
    public async Task<IActionResult> UpdateRow(string user, string owner, string repo, bool isContributor = false)
136
    {
×
137
        if (!ModelState.IsValid)
×
138
        {
×
139
            return RedirectToAction("Index");
×
140
        }
141

142
        await _ServiceApiClient.UpdateSummaryItem(user, owner, repo);
×
143

144
        // Safely pass repo name as query parameter for client-side scrolling
145
        // Validate repository name to prevent injection attacks
146
        object routeValues;
147
        if (IsValidRepoName(repo))
×
148
        {
×
149
            if (isContributor)
×
150
            {
×
151
                routeValues = new { isContributor = true, scrollTo = repo };
×
152
            }
×
153
            else
154
            {
×
155
                routeValues = new { scrollTo = repo };
×
156
            }
×
157
        }
×
158
        else
159
        {
×
160
            if (isContributor)
×
161
            {
×
162
                routeValues = new { isContributor = true };
×
163
            }
×
164
            else
165
            {
×
166
                routeValues = new { };
×
167
            }
×
168
        }
×
169

170
        return RedirectToAction("Index", routeValues);
×
171
    }
×
172

173
    public async Task<IActionResult> UpdateAll(bool isContributor = false)
174
    {
2✔
175
        if (!ModelState.IsValid)
2!
176
        {
×
177
            return RedirectToAction("Index");
×
178
        }
179

180
        string currentUser = "samsmithnz";
2✔
181
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(currentUser);
2✔
182
        foreach (SummaryItem summaryItem in summaryItems)
6!
183
        {
×
184
            await _ServiceApiClient.UpdateSummaryItem(summaryItem.User, summaryItem.Owner, summaryItem.Repo);
×
185
        }
×
186

187
        //This is a hack for now - hide controls behind this iscontributor flag, but never show iscontributor=false in query string
188
        if (isContributor)
2✔
189
        {
1✔
190
            return RedirectToAction("Index", new { isContributor = true });
1✔
191
        }
192
        else
193
        {
1✔
194
            return RedirectToAction("Index");
1✔
195
        }
196
    }
2✔
197

198
    public async Task<IActionResult> ApprovePRsForAllRepos(bool isContributor = false)
199
    {
2✔
200
        if (!ModelState.IsValid)
2!
201
        {
×
202
            return RedirectToAction("Index");
×
203
        }
204

205
        string currentUser = "samsmithnz";
2✔
206
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(currentUser);
2✔
207
        foreach (SummaryItem summaryItem in summaryItems)
6!
208
        {
×
209
            await _ServiceApiClient.ApproveSummaryItemPRs(summaryItem.Owner, summaryItem.Repo, currentUser);
×
210
        }
×
211

212
        //This is a hack for now - hide controls behind this iscontributor flag, but never show iscontributor=false in query string
213
        if (isContributor)
2✔
214
        {
1✔
215
            return RedirectToAction("Index", new { isContributor = true });
1✔
216
        }
217
        else
218
        {
1✔
219
            return RedirectToAction("Index");
1✔
220
        }
221
    }
2✔
222

223
    public async Task<IActionResult> Config(string user, string owner, string repo, bool isContributor = false)
224
    {
×
225
        if (!ModelState.IsValid)
×
226
        {
×
NEW
227
            if (isContributor)
×
NEW
228
            {
×
NEW
229
                return RedirectToAction("Index", new { isContributor = true });
×
230
            }
231
            else
NEW
232
            {
×
NEW
233
                return RedirectToAction("Index");
×
234
            }
235
        }
236

NEW
237
        ViewBag.IsContributor = isContributor;
×
238

UNCOV
239
        SummaryItem? summaryItem = await _ServiceApiClient.GetSummaryItem(user, owner, repo);
×
240

241
        SummaryItemConfig summaryItemConfig = new()
×
242
        {
×
243
            SummaryItem = summaryItem,
×
244
            IsContributor = isContributor
×
245
        };
×
246

247
        return View(summaryItemConfig);
×
248
    }
×
249

250
    public async Task<IActionResult> TaskList(bool isContributor = false)
251
    {
2✔
252
        if (!ModelState.IsValid)
2!
253
        {
×
NEW
254
            if (isContributor)
×
NEW
255
            {
×
NEW
256
                return RedirectToAction("Index", new { isContributor = true });
×
257
            }
258
            else
NEW
259
            {
×
NEW
260
                return RedirectToAction("Index");
×
261
            }
262
        }
263

264
        ViewBag.IsContributor = isContributor;
2✔
265

266
        string currentUser = "samsmithnz";
2✔
267
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(currentUser);
2✔
268
        
269
        List<TaskItem> tasks = new List<TaskItem>();
2✔
270
        
271
        foreach (SummaryItem item in summaryItems)
6!
272
        {
×
273
            // Add repository settings recommendations
274
            foreach (string recommendation in item.RepoSettingsRecommendations)
×
275
            {
×
276
                tasks.Add(new TaskItem(item.Owner, item.Repo, "Repository Settings", recommendation));
×
277
            }
×
278
            
279
            // Add branch policies recommendations  
280
            foreach (string recommendation in item.BranchPoliciesRecommendations)
×
281
            {
×
282
                tasks.Add(new TaskItem(item.Owner, item.Repo, "Branch Policies", recommendation));
×
283
            }
×
284
            
285
            // Add action recommendations
286
            foreach (string recommendation in item.ActionRecommendations)
×
287
            {
×
288
                tasks.Add(new TaskItem(item.Owner, item.Repo, "GitHub Actions", recommendation));
×
289
            }
×
290
            
291
            // Add dependabot recommendations
292
            foreach (string recommendation in item.DependabotRecommendations)
×
293
            {
×
294
                tasks.Add(new TaskItem(item.Owner, item.Repo, "Dependabot", recommendation));
×
295
            }
×
296
            
297
            // Add git version recommendations
298
            foreach (string recommendation in item.GitVersionRecommendations)
×
299
            {
×
300
                tasks.Add(new TaskItem(item.Owner, item.Repo, "Git Version", recommendation));
×
301
            }
×
302
            
303
            // Add .NET framework recommendations
304
            foreach (string recommendation in item.DotNetFrameworksRecommendations)
×
305
            {
×
306
                tasks.Add(new TaskItem(item.Owner, item.Repo, ".NET Frameworks", recommendation));
×
307
            }
×
308
            
309
            // Add NuGet package upgrades
310
            if (item.NuGetPackages != null && item.NuGetPackages.Count > 0)
×
311
            {
×
312
                tasks.Add(new TaskItem(item.Owner, item.Repo, "NuGet Packages", 
×
313
                    $"{item.NuGetPackages.Count} NuGet packages require upgrades"));
×
314
            }
×
315
            
316
            // Add security issues
317
            if (item.SecurityIssuesCount > 0)
×
318
            {
×
319
                tasks.Add(new TaskItem(item.Owner, item.Repo, "Security", 
×
320
                    $"{item.SecurityIssuesCount} Security alerts detected"));
×
321
            }
×
322
        }
×
323

324
        // Filter out ignored recommendations
325
        List<IgnoredRecommendation> ignoredRecommendations = await _ServiceApiClient.GetAllIgnoredRecommendations(currentUser);
2✔
326
        List<string> ignoredIds = ignoredRecommendations.Select(ir => ir.GetUniqueId()).ToList();
2✔
327
        
328
        List<TaskItem> filteredTasks = tasks.Where(t => !ignoredIds.Contains(t.Id)).ToList();
2✔
329

330
        TaskList taskList = new TaskList()
2✔
331
        {
2✔
332
            Tasks = filteredTasks.OrderBy(t => t.Owner).ThenBy(t => t.Repository).ThenBy(t => t.RecommendationType).ToList(),
×
333
            IsContributor = isContributor
2✔
334
        };
2✔
335

336
        return View(taskList);
2✔
337
    }
2✔
338

339
    public async Task<IActionResult> RepoDetails(string owner, string repo, bool isContributor = false)
340
    {
3✔
341
        if (string.IsNullOrEmpty(owner) || string.IsNullOrEmpty(repo))
3✔
342
        {
2✔
343
            if (isContributor)
2!
NEW
344
            {
×
NEW
345
                return RedirectToAction("TaskList", new { isContributor = true });
×
346
            }
347
            else
348
            {
2✔
349
                return RedirectToAction("TaskList");
2✔
350
            }
351
        }
352

353
        if (!ModelState.IsValid)
1!
354
        {
×
NEW
355
            if (isContributor)
×
NEW
356
            {
×
NEW
357
                return RedirectToAction("TaskList", new { isContributor = true });
×
358
            }
359
            else
NEW
360
            {
×
NEW
361
                return RedirectToAction("TaskList");
×
362
            }
363
        }
364

365
        ViewBag.IsContributor = isContributor;
1✔
366

367
        string currentUser = "samsmithnz";
1✔
368
        List<SummaryItem> summaryItems = await _ServiceApiClient.GetSummaryItems(currentUser);
1✔
369
        
370
        // Find the specific repository
371
        SummaryItem? repoItem = summaryItems.FirstOrDefault(s => s.Owner.Equals(owner, StringComparison.OrdinalIgnoreCase) 
2!
372
                                                                && s.Repo.Equals(repo, StringComparison.OrdinalIgnoreCase));
2✔
373
        
374
        if (repoItem == null)
1!
375
        {
×
NEW
376
            if (isContributor)
×
NEW
377
            {
×
NEW
378
                return RedirectToAction("TaskList", new { isContributor = true });
×
379
            }
380
            else
NEW
381
            {
×
NEW
382
                return RedirectToAction("TaskList");
×
383
            }
384
        }
385

386
        List<TaskItem> allRecommendations = new List<TaskItem>();
1✔
387
        
388
        // Generate all recommendations for this repository (same logic as TaskList)
389
        foreach (string recommendation in repoItem.RepoSettingsRecommendations)
5✔
390
        {
1✔
391
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "Repository Settings", recommendation));
1✔
392
        }
1✔
393
        
394
        foreach (string recommendation in repoItem.BranchPoliciesRecommendations)
5✔
395
        {
1✔
396
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "Branch Policies", recommendation));
1✔
397
        }
1✔
398
        
399
        foreach (string recommendation in repoItem.ActionRecommendations)
5✔
400
        {
1✔
401
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "GitHub Actions", recommendation));
1✔
402
        }
1✔
403
        
404
        foreach (string recommendation in repoItem.DependabotRecommendations)
5✔
405
        {
1✔
406
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "Dependabot", recommendation));
1✔
407
        }
1✔
408
        
409
        foreach (string recommendation in repoItem.GitVersionRecommendations)
5✔
410
        {
1✔
411
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "Git Version", recommendation));
1✔
412
        }
1✔
413
        
414
        foreach (string recommendation in repoItem.DotNetFrameworksRecommendations)
5✔
415
        {
1✔
416
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, ".NET Frameworks", recommendation));
1✔
417
        }
1✔
418
        
419
        if (repoItem.NuGetPackages != null && repoItem.NuGetPackages.Count > 0)
1!
420
        {
1✔
421
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "NuGet Packages", 
1✔
422
                $"{repoItem.NuGetPackages.Count} NuGet packages require upgrades"));
1✔
423
        }
1✔
424
        
425
        if (repoItem.SecurityIssuesCount > 0)
1✔
426
        {
1✔
427
            allRecommendations.Add(new TaskItem(repoItem.Owner, repoItem.Repo, "Security", 
1✔
428
                $"{repoItem.SecurityIssuesCount} Security alerts detected"));
1✔
429
        }
1✔
430

431
        // Get ignored recommendations
432
        List<IgnoredRecommendation> ignoredRecommendations = await _ServiceApiClient.GetIgnoredRecommendations(currentUser, owner, repo);
1✔
433
        List<string> ignoredIds = ignoredRecommendations.Select(ir => ir.GetUniqueId()).ToList();
1✔
434
        
435
        // Separate active and ignored recommendations
436
        List<TaskItem> activeRecommendations = allRecommendations.Where(r => !ignoredIds.Contains(r.Id)).ToList();
9✔
437
        List<TaskItem> ignoredTaskItems = allRecommendations.Where(r => ignoredIds.Contains(r.Id)).ToList();
9✔
438

439
        RepoDetails repoDetails = new RepoDetails(owner, repo)
1✔
440
        {
1✔
441
            Recommendations = activeRecommendations.OrderBy(r => r.RecommendationType).ToList(),
8✔
442
            IgnoredRecommendations = ignoredTaskItems.OrderBy(r => r.RecommendationType).ToList(),
×
443
            IsContributor = isContributor
1✔
444
        };
1✔
445

446
        return View(repoDetails);
1✔
447
    }
3✔
448

449
    [HttpPost]
450
    public async Task<IActionResult> IgnoreRecommendation(string owner, string repository, string recommendationType, string recommendationDetails)
451
    {
2✔
452
        if (string.IsNullOrEmpty(owner) || string.IsNullOrEmpty(repository) || 
2✔
453
            string.IsNullOrEmpty(recommendationType) || string.IsNullOrEmpty(recommendationDetails))
2✔
454
        {
1✔
455
            return Json(new { success = false, message = "Missing required parameters" });
1✔
456
        }
457

458
        string currentUser = "samsmithnz";
1✔
459
        bool success = await _ServiceApiClient.IgnoreRecommendation(currentUser, owner, repository, recommendationType, recommendationDetails);
1✔
460
        
461
        return Json(new { success = success });
1✔
462
    }
2✔
463

464
    [HttpPost]
465
    public async Task<IActionResult> UnignoreRecommendation(string owner, string repository, string recommendationType, string recommendationDetails)
466
    {
2✔
467
        if (string.IsNullOrEmpty(owner) || string.IsNullOrEmpty(repository) || 
2✔
468
            string.IsNullOrEmpty(recommendationType) || string.IsNullOrEmpty(recommendationDetails))
2✔
469
        {
1✔
470
            return Json(new { success = false, message = "Missing required parameters" });
1✔
471
        }
472

473
        string currentUser = "samsmithnz";
1✔
474
        bool success = await _ServiceApiClient.RestoreRecommendation(currentUser, owner, repository, recommendationType, recommendationDetails);
1✔
475
        
476
        return Json(new { success = success });
1✔
477
    }
2✔
478

479
    public IActionResult Privacy(bool isContributor = false)
480
    {
2✔
481
        ViewBag.IsContributor = isContributor;
2✔
482
        return View();
2✔
483
    }
2✔
484

485
    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
486
    public IActionResult Error()
487
    {
×
488
        return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
×
489
    }
×
490
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc