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

Aldaviva / GamesDoneQuickCalendarFactory / 22162788867

19 Feb 2026 12:02AM UTC coverage: 76.527% (-1.1%) from 77.672%
22162788867

push

github

Aldaviva
On network IO errors like GDQ server timeouts (which are very frequent), retry for a minute, and also keep returning the last good calendar if possible, instead of returning an error. Reduce HTTP client timeout from 30 seconds to 15, to allow more frequent attempts.

122 of 199 branches covered (61.31%)

13 of 17 new or added lines in 5 files covered. (76.47%)

24 existing lines in 3 files now uncovered.

401 of 524 relevant lines covered (76.53%)

148.88 hits per line

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

91.04
/GamesDoneQuickCalendarFactory/Program.cs
1
using Bom.Squad;
2
using GamesDoneQuickCalendarFactory;
3
using GamesDoneQuickCalendarFactory.Data;
4
using GamesDoneQuickCalendarFactory.Data.Marshal;
5
using GamesDoneQuickCalendarFactory.Services;
6
using Ical.Net.Serialization;
7
using Microsoft.AspNetCore.Http.Headers;
8
using Microsoft.AspNetCore.HttpOverrides;
9
using Microsoft.AspNetCore.Mvc;
10
using Microsoft.AspNetCore.OutputCaching;
11
using Microsoft.Net.Http.Headers;
12
using NodaTime;
13
using RuntimeUpgrade.Notifier;
14
using RuntimeUpgrade.Notifier.Data;
15
using System.Net.Mime;
16
using System.Text;
17
using System.Text.RegularExpressions;
18
using Unfucked;
19
using Unfucked.DateTime;
20
using Unfucked.HTTP;
21
using Unfucked.HTTP.Config;
22

23
BomSquad.DefuseUtf8Bom();
10✔
24

25
Encoding             responseEncoding     = Encoding.UTF8;
10✔
26
MediaTypeHeaderValue icalendarContentType = new("text/calendar") { Charset = responseEncoding.WebName };
10✔
27
string[]             varyHeaderValue      = [HeaderNames.AcceptEncoding];
10✔
28

29
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
10✔
30
builder.Host
10✔
31
    .UseWindowsService()
10✔
32
    .UseSystemd();
10✔
33

34
builder.Logging.AddUnfuckedConsole();
10✔
35

36
builder.Configuration.AlsoSearchForJsonFilesInExecutableDirectory();
10✔
37

38
// GZIP response compression is handled by Apache httpd, not Kestrel, per https://learn.microsoft.com/en-us/aspnet/core/performance/response-compression?view=aspnetcore-8.0#when-to-use-response-compression-middleware
39
builder.Services
10✔
40
    .Configure<Configuration>(builder.Configuration)
10✔
41
    .AddOutputCache()
10✔
42
    .AddSingleton<ICalendarGenerator, CalendarGenerator>()
10✔
43
    .AddSingleton<IEventDownloader, EventDownloader>()
10✔
44
    .AddSingleton<IGdqClient, GdqClient>()
10✔
45
    .AddSingleton<ICalendarPoller, CalendarPoller>()
10✔
46
    .AddSingleton<IGoogleCalendarSynchronizer, GoogleCalendarSynchronizer>()
10✔
47
    .AddSingleton<IClock>(SystemClock.Instance)
10✔
48
    .AddSingleton<HttpClient>(new UnfuckedHttpClient(new SocketsHttpHandler { PooledConnectionLifetime = (Minutes) 15 }) { Timeout = (Seconds) 15 }
10✔
49
        .Property(PropertyKey.JsonSerializerOptions, JsonSerializerGlobalOptions.JSON_SERIALIZER_OPTIONS));
10✔
50

51
await using WebApplication webApp = builder.Build();
10✔
52

53
webApp
10✔
54
    .UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto })
10✔
55
    .UseOutputCache()
10✔
56
    .Use(async (context, next) => {
10✔
57
        ICalendarPoller calendarPoller  = context.RequestServices.GetRequiredService<ICalendarPoller>();
10✔
58
        ResponseHeaders responseHeaders = context.Response.GetTypedHeaders();
10✔
59
        responseHeaders.CacheControl               = new CacheControlHeaderValue { Public = true, MaxAge = calendarPoller.getPollingInterval() }; // longer cache when no event running
10✔
60
        context.Response.Headers[HeaderNames.Vary] = varyHeaderValue;
10✔
61

10✔
62
        if (await calendarPoller.mostRecentlyPolledCalendar.ResultOrNullForException() is {} mostRecentlyPolledCalendar) {
10✔
63
            responseHeaders.ETag         = mostRecentlyPolledCalendar.etag;
10✔
64
            responseHeaders.LastModified = mostRecentlyPolledCalendar.dateModified.ToDateTimeOffset();
10✔
65
        }
10✔
66
        await next();
10✔
67
    });
20✔
68

69
webApp.MapGet("/", [OutputCache] async Task ([FromServices] ICalendarPoller calendarPoller, HttpResponse response) => {
10✔
70
    try {
10✔
71
        if (await calendarPoller.mostRecentlyPolledCalendar is {} mostRecentlyPolledCalendar) {
4✔
72
            response.GetTypedHeaders().ContentType = icalendarContentType;
4✔
73
            await new CalendarSerializer().SerializeAsync(mostRecentlyPolledCalendar.calendar, response.Body, responseEncoding);
4✔
74
        } else {
10✔
UNCOV
75
            response.StatusCode = StatusCodes.Status204NoContent;
×
76
        }
10✔
77
    } catch (Exception e) when (e is not OutOfMemoryException) {
4✔
UNCOV
78
        response.StatusCode  = StatusCodes.Status500InternalServerError;
×
UNCOV
79
        response.ContentType = MediaTypeNames.Text.Plain;
×
80
        await using StreamWriter bodyWriter = new(response.Body, responseEncoding);
×
81
        await bodyWriter.WriteAsync(e.ToString());
×
82
    }
×
83
});
10✔
84

85
webApp.MapGet("/badge.json", [OutputCache] async ([FromServices] IEventDownloader eventDownloader) =>
10✔
86
await eventDownloader.downloadSchedule() is {} schedule
16✔
87
    ? new ShieldsBadgeResponse(
16✔
88
        label: shortNamePattern().Replace(schedule.shortTitle, " ").ToLower(), // add spaces to abbreviation
16✔
89
        message: $"{schedule.runs.Count} {(schedule.runs.Count == 1 ? "run" : "runs")}",
16✔
90
        color: "success",
16✔
91
        logoSvg: Resources.gdqDpadBadgeLogo)
16✔
92
    : new ShieldsBadgeResponse("gdq", "no event now", "inactive", false, Resources.gdqDpadBadgeLogo));
16✔
93

94
await webApp.Services.GetRequiredService<IGoogleCalendarSynchronizer>().start();
10✔
95

96
using IRuntimeUpgradeNotifier runtimeUpgradeNotifier = new RuntimeUpgradeNotifier {
10✔
97
    LoggerFactory   = webApp.Services.GetRequiredService<ILoggerFactory>(),
10✔
98
    RestartStrategy = RestartStrategy.AutoRestartService,
10✔
99
    ExitStrategy    = new HostedLifetimeExit(webApp)
10✔
100
};
10✔
101

102
await webApp.RunAsync();
10✔
103

104
internal partial class Program {
105

106
    [GeneratedRegex(@"(?<=\D)(?=\d)|(?<=[a-z])(?=[A-Z])")]
107
    private static partial Regex shortNamePattern();
108

109
}
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

© 2026 Coveralls, Inc