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

Aldaviva / GamesDoneQuickCalendarFactory / 24123769802

08 Apr 2026 07:34AM UTC coverage: 77.609% (-0.04%) from 77.652%
24123769802

push

github

Aldaviva
Updated dependencies

124 of 205 branches covered (60.49%)

14 of 15 new or added lines in 5 files covered. (93.33%)

409 of 527 relevant lines covered (77.61%)

148.16 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
string[] varyHeaderValue = [HeaderNames.AcceptEncoding];
10✔
26

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

32
builder.Logging.AddUnfuckedConsole();
10✔
33

34
builder.Configuration.AlsoSearchForJsonFilesInExecutableDirectory();
10✔
35

36
// 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
37
builder.Services
10✔
38
    .Configure<Configuration>(builder.Configuration)
10✔
39
    .AddOutputCache()
10✔
40
    .AddSingleton<ICalendarGenerator, CalendarGenerator>()
10✔
41
    .AddSingleton<IEventDownloader, EventDownloader>()
10✔
42
    .AddSingleton<IGdqClient, GdqClient>()
10✔
43
    .AddSingleton<ICalendarPoller, CalendarPoller>()
10✔
44
    .AddSingleton<IGoogleCalendarSynchronizer, GoogleCalendarSynchronizer>()
10✔
45
    .AddSingleton<IClock>(SystemClock.Instance)
10✔
46
    .AddSingleton<HttpClient>(new UnfuckedHttpClient(new SocketsHttpHandler { PooledConnectionLifetime = (Minutes) 15 }) { Timeout = (Seconds) 15 }
10✔
47
        .Property(PropertyKey.JsonSerializerOptions, JsonSerializerGlobalOptions.JSON_SERIALIZER_OPTIONS));
10✔
48

49
builder.Services.AddSingleton(await State.load(filename: ((IEnumerable<string>) [builder.Environment.ContentRootPath, "."]).Select(static dir => Path.Combine(dir, "state.json")).MaxBy(File.Exists)!));
30✔
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

10✔
60
        if (await calendarPoller.mostRecentlyPolledCalendar.ExceptionsToNull() is {} mostRecentlyPolledCalendar) {
10✔
61
            responseHeaders.CacheControl               = new CacheControlHeaderValue { Public = true, MaxAge = calendarPoller.getPollingInterval() }; // longer cache when no event running
10✔
62
            context.Response.Headers[HeaderNames.Vary] = varyHeaderValue;
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] static async Task ([FromServices] ICalendarPoller calendarPoller, HttpResponse response) => {
10✔
70
    try {
10✔
71
        if (await calendarPoller.mostRecentlyPolledCalendar is {} mostRecentlyPolledCalendar) {
4✔
72
            response.GetTypedHeaders().ContentType = ICALENDAR_CONTENT_TYPE;
4✔
73
            await new CalendarSerializer().SerializeAsync(mostRecentlyPolledCalendar.calendar, response.Body, Encoding.UTF8);
4✔
74
        } else {
10✔
75
            response.StatusCode = StatusCodes.Status204NoContent;
×
76
        }
10✔
77
    } catch (Exception e) when (e is not OutOfMemoryException) {
4✔
78
        response.StatusCode  = StatusCodes.Status500InternalServerError;
×
79
        response.ContentType = MediaTypeNames.Text.Plain;
×
NEW
80
        await using StreamWriter bodyWriter = new(response.Body, Encoding.UTF8);
×
81
        await bodyWriter.WriteAsync(e.ToString());
×
82
    }
×
83
});
10✔
84

85
webApp.MapGet("/badge.json", [OutputCache] static 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
// #84: don't await, because Windows will time out this service startup if the Internet is down (server won boot race against modem and router), so the service will be stopped with no retry
95
_ = webApp.Services.GetRequiredService<IGoogleCalendarSynchronizer>().start();
10✔
96

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

103
await webApp.RunAsync();
10✔
104

105
internal sealed partial class Program {
106

107
    private static readonly MediaTypeHeaderValue ICALENDAR_CONTENT_TYPE = new("text/calendar") { Charset = Encoding.UTF8.WebName };
2✔
108

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

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