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

karanshukla / openresto / 28881805559

07 Jul 2026 04:23PM UTC coverage: 94.253% (+0.07%) from 94.185%
28881805559

Pull #209

github

web-flow
Merge 55996179b into 108a871bd
Pull Request #209: Massive Refactor

4316 of 4939 branches covered (87.39%)

Branch coverage included in aggregate %.

1466 of 1601 new or added lines in 120 files covered. (91.57%)

65 existing lines in 27 files now uncovered.

13954 of 14445 relevant lines covered (96.6%)

83.46 hits per line

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

91.39
/OpenRestoApi/Extensions/ServiceCollectionExtensions.cs
1
using System.Text;
2
using System.Threading.RateLimiting;
3
using MailKit.Net.Smtp;
4
using Microsoft.AspNetCore.Authentication.JwtBearer;
5
using Microsoft.AspNetCore.DataProtection;
6
using Microsoft.AspNetCore.HttpOverrides;
7
using Microsoft.IdentityModel.Tokens;
8
using OpenRestoApi.Core.Application.Interfaces;
9
using OpenRestoApi.Core.Application.Services;
10
using OpenRestoApi.Infrastructure.Holds;
11
using OpenRestoApi.Infrastructure.Persistence.Repositories;
12

13
namespace OpenRestoApi.Extensions;
14

15
public static class ServiceCollectionExtensions
16
{
17
    public static IServiceCollection AddCustomCors(this IServiceCollection services, IConfiguration configuration)
18
    {
19
        string? configCorsOrigins = configuration["Cors:Origins"];
34✔
20
        string? corsOrigins = string.IsNullOrWhiteSpace(configCorsOrigins)
34✔
21
            ? Environment.GetEnvironmentVariable("CORS_ORIGINS")
34✔
22
            : configCorsOrigins;
34✔
23

24
        if (string.IsNullOrWhiteSpace(corsOrigins) || corsOrigins.Trim() == "*")
34!
25
        {
26
            throw new InvalidOperationException(
2✔
27
                "Cors:Origins must be explicitly configured with allowed origins (comma-separated). " +
2✔
28
                "Wildcards are not permitted. Set via Cors:Origins config or CORS_ORIGINS env var.");
2✔
29
        }
30

31
        string[] origins = corsOrigins.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
32✔
32

33
        services.AddCors(options =>
32✔
34
        {
32✔
35
            options.AddPolicy("AllowFrontend",
30!
36
                builder =>
30✔
37
                {
30✔
38
                    builder.WithOrigins(origins)
30✔
39
                           .AllowAnyMethod()
30✔
40
                           .AllowAnyHeader()
30✔
41
                           .AllowCredentials();
30✔
42
                });
60✔
43
        });
62✔
44

45
        return services;
32✔
46
    }
47

48
    public static IServiceCollection AddCustomRateLimiting(this IServiceCollection services, IWebHostEnvironment env)
49
    {
50
        bool isTesting = env.EnvironmentName == "Testing";
32✔
51
        int authLimit = isTesting ? 10000 : 10;   // per IP: brute-force protection on /login
32✔
52
        int publicLimit = isTesting ? 10000 : 120;  // per IP: ~2 req/s, covers normal browsing
32✔
53
        int globalLimit = isTesting ? 10000 : 300;  // per IP: overall ceiling
32✔
54

55
        static string IpKey(HttpContext ctx) =>
56
            ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown";
1,108!
57

58
        services.AddRateLimiter(options =>
32✔
59
        {
32✔
60
            options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
30✔
61

32✔
62
            options.AddPolicy("auth", ctx =>
30!
63
                RateLimitPartition.GetFixedWindowLimiter(IpKey(ctx), _ => new FixedWindowRateLimiterOptions
142✔
64
                {
142✔
65
                    AutoReplenishment = true,
142✔
66
                    PermitLimit = authLimit,
142✔
67
                    Window = TimeSpan.FromMinutes(1),
142✔
68
                    QueueLimit = 0,
142✔
69
                }));
142✔
70

32✔
71
            options.AddPolicy("public", ctx =>
30!
72
                RateLimitPartition.GetFixedWindowLimiter(IpKey(ctx), _ => new FixedWindowRateLimiterOptions
376✔
73
                {
376✔
74
                    AutoReplenishment = true,
376✔
75
                    PermitLimit = publicLimit,
376✔
76
                    Window = TimeSpan.FromMinutes(1),
376✔
77
                    QueueLimit = 0,
376✔
78
                }));
376✔
79

32✔
80
            options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
30!
81
                RateLimitPartition.GetFixedWindowLimiter(IpKey(ctx), _ => new FixedWindowRateLimiterOptions
740✔
82
                {
740✔
83
                    AutoReplenishment = true,
740✔
84
                    PermitLimit = globalLimit,
740✔
85
                    QueueLimit = 0,
740✔
86
                    Window = TimeSpan.FromMinutes(1),
740✔
87
                }));
740✔
88
        });
62✔
89

90
        return services;
32✔
91
    }
92

93
    public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
94
    {
95
        string? configJwtKey = configuration["Jwt:Key"];
32✔
96
        string? jwtKey = string.IsNullOrWhiteSpace(configJwtKey)
32✔
97
            ? Environment.GetEnvironmentVariable("JWT_KEY")
32✔
98
            : configJwtKey;
32✔
99

100
        if (string.IsNullOrWhiteSpace(jwtKey) || jwtKey.Length < 32)
32!
101
        {
102
            throw new InvalidOperationException(
×
103
                "Jwt:Key must be set (via config or JWT_KEY env var) and be at least 32 characters. " +
×
104
                "Generate one with: openssl rand -base64 48");
×
105
        }
106

107
        services
32✔
108
            .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
32✔
109
            .AddJwtBearer(options =>
32✔
110
            {
32✔
111
                options.TokenValidationParameters = new TokenValidationParameters
30!
112
                {
30✔
113
                    ValidateIssuerSigningKey = true,
30✔
114
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
30✔
115
                    ValidateIssuer = true,
30✔
116
                    ValidIssuer = configuration["Jwt:Issuer"] ?? "openresto-api",
30✔
117
                    ValidateAudience = true,
30✔
118
                    ValidAudience = configuration["Jwt:Audience"] ?? "openresto-admin",
30✔
119
                    ValidateLifetime = true,
30✔
120
                    ClockSkew = TimeSpan.Zero,
30✔
121
                };
30✔
122

32✔
123
                // Read JWT from HttpOnly cookie if no Authorization header is present
32✔
124
                options.Events = new JwtBearerEvents
30✔
125
                {
30✔
126
                    OnMessageReceived = context =>
30✔
127
                    {
30✔
128
                        if (string.IsNullOrEmpty(context.Token) && context.Request.Cookies.TryGetValue("openresto_auth", out string? cookie))
680!
129
                        {
30✔
130
                            context.Token = cookie;
8✔
131
                        }
30✔
132
                        return Task.CompletedTask;
680✔
133
                    }
30✔
134
                };
30✔
135
            });
62✔
136

137
        services.AddAuthorization();
32✔
138

139
        return services;
32✔
140
    }
141

142
    public static IServiceCollection AddProjectDependencies(this IServiceCollection services)
143
    {
144
        services.Configure<ForwardedHeadersOptions>(options =>
36✔
145
        {
36✔
146
            options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
30✔
147
            options.ForwardLimit = 1; // only process the immediate upstream hop; prevents X-Forwarded-For spoofing
30✔
148
            options.KnownIPNetworks.Clear();
30✔
149
            options.KnownProxies.Clear();
30✔
150
        });
66✔
151

152
        services.AddControllers();
36✔
153
        services.AddOpenApi();
36✔
154
        services.AddDistributedMemoryCache();
36✔
155

156
        // HoldService must be Singleton — the in-memory dictionary must survive across requests
157
        services.AddSingleton<ISystemClock, SystemClock>();
36✔
158
        services.AddSingleton<IHoldService, HoldService>();
36✔
159
        services.AddScoped<IHoldPolicyService, HoldPolicyService>();
36✔
160

161
        services.AddScoped<IBookingRepository, BookingRepository>();
36✔
162
        services.AddScoped<IBookingFilterRepository, BookingFilterRepository>();
36✔
163
        services.AddScoped<ITableRepository, TableRepository>();
36✔
164
        services.AddScoped<ISectionRepository, SectionRepository>();
36✔
165
        services.AddScoped<IRestaurantRepository, RestaurantRepository>();
36✔
166
        services.AddScoped<IAdminNotificationRepository, AdminNotificationRepository>();
36✔
167
        services.AddScoped<IAdminPushSubscriptionRepository, AdminPushSubscriptionRepository>();
36✔
168
        services.AddScoped<IAdminCredentialRepository, AdminCredentialRepository>();
36✔
169
        services.AddScoped<IBrandSettingsRepository, BrandSettingsRepository>();
36✔
170
        services.AddScoped<IEmailSettingsRepository, EmailSettingsRepository>();
36✔
171
        services.AddScoped<IEmailFailureRepository, EmailFailureRepository>();
36✔
172
        services.AddScoped<IHighlightRepository, HighlightRepository>();
36✔
173
        services.AddScoped<ISocialLinkRepository, SocialLinkRepository>();
36✔
174

175
        services.AddScoped<IPasswordService, PasswordService>();
36✔
176
        services.AddScoped<IJwtTokenService, JwtTokenService>();
36✔
177
        services.AddScoped<ISecurityQuestionsService, SecurityQuestionsService>();
36✔
178
        services.AddScoped<IAuthService, AuthService>();
36✔
179
        services.AddScoped<BookingService>();
36✔
180
        services.AddScoped<AdminService>();
36✔
181
        services.AddScoped<RestaurantManagementService>();
36✔
182
        services.AddScoped<BrandService>();
36✔
183
        services.AddScoped<EmailSettingsService>();
36✔
184
        services.AddScoped<HighlightService>();
36✔
185
        services.AddScoped<SocialLinkService>();
36✔
186
        services.AddScoped<IAvailabilityService, AvailabilityService>();
36✔
187
        services.AddScoped<MediaService>();
36✔
188

189
        services.AddSingleton<OpenRestoApi.Core.Application.Mappings.BookingMapper>();
36✔
190

191
        var dpKeysPath = Environment.GetEnvironmentVariable("DATA_PROTECTION_KEYS_PATH");
36✔
192
        var dpBuilder = services.AddDataProtection().SetApplicationName("openresto");
36✔
193
        if (!string.IsNullOrEmpty(dpKeysPath))
36✔
194
            dpBuilder.PersistKeysToFileSystem(new DirectoryInfo(dpKeysPath));
2✔
195
        services.AddSingleton<OpenRestoApi.Infrastructure.Email.CredentialProtector>();
36✔
196
        services.AddSingleton<OpenRestoApi.Infrastructure.Cookies.RecentBookingsCookie>();
36✔
197
        services.AddSingleton<IAuthCookieService, AuthCookieService>();
36✔
198
        services.AddScoped<Func<ISmtpClient>>(_ => () => new SmtpClient());
36✔
199
        services.AddScoped<IEmailService, OpenRestoApi.Infrastructure.Email.EmailService>();
36✔
200
        services.AddScoped<IEmailTemplateService, OpenRestoApi.Core.Application.Services.EmailTemplateService>();
36✔
201
        services.AddScoped<IBookingConfirmationService, OpenRestoApi.Core.Application.Services.BookingConfirmationService>();
36✔
202
        services.AddScoped<INotificationService, OpenRestoApi.Core.Application.Services.NotificationService>();
36✔
203
        services.AddScoped<IBookingNotificationService, OpenRestoApi.Core.Application.Services.BookingNotificationService>();
36✔
204
        services.AddOptions<OpenRestoApi.Core.Application.Settings.VapidSettings>()
36✔
205
                .BindConfiguration("Vapid");
36✔
206

207
        services.AddSingleton<OpenRestoApi.Infrastructure.Notifications.NotificationQueue>();
36✔
208
        services.AddSingleton<INotificationQueue>(sp =>
36✔
209
            sp.GetRequiredService<OpenRestoApi.Infrastructure.Notifications.NotificationQueue>());
36✔
210
        services.AddHostedService<OpenRestoApi.Infrastructure.Notifications.NotificationWorker>();
36✔
211

212
        services.AddSession(options =>
36✔
213
        {
36✔
UNCOV
214
            options.IdleTimeout = TimeSpan.FromSeconds(10);
×
UNCOV
215
            options.Cookie.HttpOnly = true;
×
UNCOV
216
            options.Cookie.IsEssential = true;
×
217
        });
36✔
218

219
        return services;
36✔
220
    }
221
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc