5-improve-encrypted-storage #6
12 changed files with 267 additions and 30 deletions
179
IdentityShroud.Api.Tests/Apis/ClientApiTests.cs
Normal file
179
IdentityShroud.Api.Tests/Apis/ClientApiTests.cs
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using IdentityShroud.Core;
|
||||||
|
using IdentityShroud.Core.Model;
|
||||||
|
using IdentityShroud.Core.Tests.Fixtures;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Api.Tests.Apis;
|
||||||
|
|
||||||
|
public class ClientApiTests : IClassFixture<ApplicationFactory>
|
||||||
|
{
|
||||||
|
private readonly ApplicationFactory _factory;
|
||||||
|
|
||||||
|
public ClientApiTests(ApplicationFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
|
||||||
|
using var scope = _factory.Services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<Db>();
|
||||||
|
if (!db.Database.EnsureCreated())
|
||||||
|
{
|
||||||
|
db.Database.ExecuteSqlRaw("TRUNCATE realm CASCADE;");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null, false, "ClientId")]
|
||||||
|
[InlineData("", false, "ClientId")]
|
||||||
|
[InlineData("my-client", true, "")]
|
||||||
|
public async Task Create_Validation(string? clientId, bool succeeds, string fieldName)
|
||||||
|
{
|
||||||
|
// setup
|
||||||
|
Realm realm = await CreateRealmAsync("test-realm", "Test Realm");
|
||||||
|
|
||||||
|
var client = _factory.CreateClient();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.PostAsync(
|
||||||
|
$"/api/v1/realms/{realm.Id}/clients",
|
||||||
|
JsonContent.Create(new { ClientId = clientId }),
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (succeeds)
|
||||||
|
{
|
||||||
|
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var problemDetails =
|
||||||
|
await response.Content.ReadFromJsonAsync<ValidationProblemDetails>(
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.Contains(problemDetails!.Errors, e => e.Key == fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_Success_ReturnsCreatedWithLocation()
|
||||||
|
{
|
||||||
|
// setup
|
||||||
|
Realm realm = await CreateRealmAsync("create-realm", "Create Realm");
|
||||||
|
|
||||||
|
var client = _factory.CreateClient();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await client.PostAsync(
|
||||||
|
$"/api/v1/realms/{realm.Id}/clients",
|
||||||
|
JsonContent.Create(new { ClientId = "new-client", Name = "New Client" }),
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// verify
|
||||||
|
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||||
|
|
||||||
|
var body = await response.Content.ReadFromJsonAsync<ClientCreateReponse>(
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.NotNull(body);
|
||||||
|
Assert.Equal("new-client", body.ClientId);
|
||||||
|
Assert.True(body.Id > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Create_UnknownRealm_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
var client = _factory.CreateClient();
|
||||||
|
|
||||||
|
var response = await client.PostAsync(
|
||||||
|
$"/api/v1/realms/{Guid.NewGuid()}/clients",
|
||||||
|
JsonContent.Create(new { ClientId = "some-client" }),
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Get_Success()
|
||||||
|
{
|
||||||
|
// setup
|
||||||
|
Realm realm = await CreateRealmAsync("get-realm", "Get Realm");
|
||||||
|
Client dbClient = await CreateClientAsync(realm, "get-client", "Get Client");
|
||||||
|
|
||||||
|
var httpClient = _factory.CreateClient();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await httpClient.GetAsync(
|
||||||
|
$"/api/v1/realms/{realm.Id}/clients/{dbClient.Id}",
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
string contents = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// verify
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
|
||||||
|
var body = await response.Content.ReadFromJsonAsync<ClientRepresentation>(
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
Assert.NotNull(body);
|
||||||
|
Assert.Equal(dbClient.Id, body.Id);
|
||||||
|
Assert.Equal("get-client", body.ClientId);
|
||||||
|
Assert.Equal("Get Client", body.Name);
|
||||||
|
Assert.Equal(realm.Id, body.RealmId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Get_UnknownClient_ReturnsNotFound()
|
||||||
|
{
|
||||||
|
// setup
|
||||||
|
Realm realm = await CreateRealmAsync("notfound-realm", "NotFound Realm");
|
||||||
|
|
||||||
|
var httpClient = _factory.CreateClient();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var response = await httpClient.GetAsync(
|
||||||
|
$"/api/v1/realms/{realm.Id}/clients/99999",
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
// verify
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Realm> CreateRealmAsync(string slug, string name)
|
||||||
|
{
|
||||||
|
using var scope = _factory.Services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<Db>();
|
||||||
|
var realm = new Realm { Slug = slug, Name = name };
|
||||||
|
db.Realms.Add(realm);
|
||||||
|
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Client> CreateClientAsync(Realm realm, string clientId, string? name = null)
|
||||||
|
{
|
||||||
|
using var scope = _factory.Services.CreateScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<Db>();
|
||||||
|
var client = new Client
|
||||||
|
{
|
||||||
|
RealmId = realm.Id,
|
||||||
|
ClientId = clientId,
|
||||||
|
Name = name,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
db.Clients.Add(client);
|
||||||
|
await db.SaveChangesAsync(TestContext.Current.CancellationToken);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
using FluentResults;
|
using FluentResults;
|
||||||
|
using IdentityShroud.Api.Mappers;
|
||||||
using IdentityShroud.Core.Contracts;
|
using IdentityShroud.Core.Contracts;
|
||||||
using IdentityShroud.Core.Messages.Realm;
|
|
||||||
using IdentityShroud.Core.Model;
|
using IdentityShroud.Core.Model;
|
||||||
using IdentityShroud.Core.Services;
|
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace IdentityShroud.Api;
|
namespace IdentityShroud.Api;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public record ClientCreateReponse(int Id, string ClientId);
|
public record ClientCreateReponse(int Id, string ClientId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -34,13 +34,18 @@ public static class ClientApi
|
||||||
.WithName(ClientGetRouteName);
|
.WithName(ClientGetRouteName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task ClientGet(HttpContext context)
|
private static Ok<ClientRepresentation> ClientGet(
|
||||||
|
Guid realmId,
|
||||||
|
int clientId,
|
||||||
|
HttpContext context)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
Client client = (Client)context.Items["ClientEntity"]!;
|
||||||
|
return TypedResults.Ok(new ClientMapper().ToDto(client));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<Results<CreatedAtRoute<ClientCreateReponse>, InternalServerError>>
|
private static async Task<Results<CreatedAtRoute<ClientCreateReponse>, InternalServerError>>
|
||||||
ClientCreate(
|
ClientCreate(
|
||||||
|
Guid realmId,
|
||||||
ClientCreateRequest request,
|
ClientCreateRequest request,
|
||||||
[FromServices] IClientService service,
|
[FromServices] IClientService service,
|
||||||
HttpContext context,
|
HttpContext context,
|
||||||
|
|
@ -64,6 +69,5 @@ public static class ClientApi
|
||||||
["realmId"] = realm.Id,
|
["realmId"] = realm.Id,
|
||||||
["clientId"] = client.Id,
|
["clientId"] = client.Id,
|
||||||
});
|
});
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
16
IdentityShroud.Api/Apis/Dto/ClientRepresentation.cs
Normal file
16
IdentityShroud.Api/Apis/Dto/ClientRepresentation.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace IdentityShroud.Api;
|
||||||
|
|
||||||
|
public record ClientRepresentation
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public Guid RealmId { get; set; }
|
||||||
|
public required string ClientId { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
public string? SignatureAlgorithm { get; set; }
|
||||||
|
|
||||||
|
public bool AllowClientCredentialsFlow { get; set; } = false;
|
||||||
|
|
||||||
|
public required DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,9 @@ public class ClientIdValidationFilter(IClientService clientService) : IEndpointF
|
||||||
{
|
{
|
||||||
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
|
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
|
||||||
{
|
{
|
||||||
|
Guid realmId = context.Arguments.OfType<Guid>().First();
|
||||||
int id = context.Arguments.OfType<int>().First();
|
int id = context.Arguments.OfType<int>().First();
|
||||||
Client? client = await clientService.FindById(id, context.HttpContext.RequestAborted);
|
Client? client = await clientService.FindById(realmId, id, context.HttpContext.RequestAborted);
|
||||||
if (client is null)
|
if (client is null)
|
||||||
{
|
{
|
||||||
return Results.NotFound();
|
return Results.NotFound();
|
||||||
|
|
|
||||||
11
IdentityShroud.Api/Apis/Mappers/ClientMapper.cs
Normal file
11
IdentityShroud.Api/Apis/Mappers/ClientMapper.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
using IdentityShroud.Core.Model;
|
||||||
|
using Riok.Mapperly.Abstractions;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Api.Mappers;
|
||||||
|
|
||||||
|
[Mapper]
|
||||||
|
public partial class ClientMapper
|
||||||
|
{
|
||||||
|
[MapperIgnoreSource(nameof(Client.Secrets))]
|
||||||
|
public partial ClientRepresentation ToDto(Client client);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
using FluentValidation;
|
||||||
|
using IdentityShroud.Core.Contracts;
|
||||||
|
|
||||||
|
namespace IdentityShroud.Api;
|
||||||
|
|
||||||
|
public class ClientCreateRequestValidator : AbstractValidator<ClientCreateRequest>
|
||||||
|
{
|
||||||
|
// most of standard ascii minus the control characters and space
|
||||||
|
private const string ClientIdPattern = "^[\x21-\x7E]+";
|
||||||
|
|
||||||
|
private string[] AllowedAlgorithms = [ "RS256", "ES256" ];
|
||||||
|
|
||||||
|
public ClientCreateRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(e => e.ClientId).NotEmpty().MaximumLength(40).Matches(ClientIdPattern);
|
||||||
|
RuleFor(e => e.Name).MaximumLength(80);
|
||||||
|
RuleFor(e => e.Description).MaximumLength(2048);
|
||||||
|
RuleFor(e => e.SignatureAlgorithm)
|
||||||
|
.Must(v => v is null || AllowedAlgorithms.Contains(v))
|
||||||
|
.WithMessage($"SignatureAlgorithm must be one of {string.Join(", ", AllowedAlgorithms)} or null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
|
||||||
|
<PackageReference Include="Riok.Mapperly" Version="4.3.1" />
|
||||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ public class ClientServiceTests : IClassFixture<DbFixture>
|
||||||
await using var actContext = _dbFixture.CreateDbContext();
|
await using var actContext = _dbFixture.CreateDbContext();
|
||||||
// Act
|
// Act
|
||||||
ClientService sut = new(actContext, _encryptionService, _clock);
|
ClientService sut = new(actContext, _encryptionService, _clock);
|
||||||
Client? result = await sut.GetByClientId(clientId, TestContext.Current.CancellationToken);
|
Client? result = await sut.GetByClientId(_realmId, clientId, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
if (shouldFind)
|
if (shouldFind)
|
||||||
|
|
@ -143,7 +143,7 @@ public class ClientServiceTests : IClassFixture<DbFixture>
|
||||||
await using var actContext = _dbFixture.CreateDbContext();
|
await using var actContext = _dbFixture.CreateDbContext();
|
||||||
// Act
|
// Act
|
||||||
ClientService sut = new(actContext, _encryptionService, _clock);
|
ClientService sut = new(actContext, _encryptionService, _clock);
|
||||||
Client? result = await sut.FindById(searchId, TestContext.Current.CancellationToken);
|
Client? result = await sut.FindById(_realmId, searchId, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
if (shouldFind)
|
if (shouldFind)
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,6 @@ using IdentityShroud.Core.Model;
|
||||||
|
|
||||||
namespace IdentityShroud.Core.Contracts;
|
namespace IdentityShroud.Core.Contracts;
|
||||||
|
|
||||||
//public record CreateClientRequest(Guid RealmId, string ClientId, string? Description);
|
|
||||||
|
|
||||||
public class ClientCreateRequest
|
|
||||||
{
|
|
||||||
public required string ClientId { get; set; }
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public string? Description { get; set; }
|
|
||||||
public string? SignatureAlgorithm { get; set; }
|
|
||||||
public bool? AllowClientCredentialsFlow { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface IClientService
|
public interface IClientService
|
||||||
{
|
{
|
||||||
Task<Result<Client>> Create(
|
Task<Result<Client>> Create(
|
||||||
|
|
@ -21,6 +9,6 @@ public interface IClientService
|
||||||
ClientCreateRequest request,
|
ClientCreateRequest request,
|
||||||
CancellationToken ct = default);
|
CancellationToken ct = default);
|
||||||
|
|
||||||
Task<Client?> GetByClientId(string clientId, CancellationToken ct = default);
|
Task<Client?> GetByClientId(Guid realmId, string clientId, CancellationToken ct = default);
|
||||||
Task<Client?> FindById(int id, CancellationToken ct = default);
|
Task<Client?> FindById(Guid realmId, int id, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
10
IdentityShroud.Core/DTO/Client/ClientCreateRequest.cs
Normal file
10
IdentityShroud.Core/DTO/Client/ClientCreateRequest.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace IdentityShroud.Core.Contracts;
|
||||||
|
|
||||||
|
public class ClientCreateRequest
|
||||||
|
{
|
||||||
|
public required string ClientId { get; set; }
|
||||||
|
public string? Name { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? SignatureAlgorithm { get; set; }
|
||||||
|
public bool? AllowClientCredentialsFlow { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -34,14 +34,20 @@ public class ClientService(
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Client?> GetByClientId(string clientId, CancellationToken ct = default)
|
public async Task<Client?> GetByClientId(
|
||||||
|
Guid realmId,
|
||||||
|
string clientId,
|
||||||
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await db.Clients.FirstOrDefaultAsync(c => c.ClientId == clientId, ct);
|
return await db.Clients.FirstOrDefaultAsync(c => c.ClientId == clientId && c.RealmId == realmId, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Client?> FindById(int id, CancellationToken ct = default)
|
public async Task<Client?> FindById(
|
||||||
|
Guid realmId,
|
||||||
|
int id,
|
||||||
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await db.Clients.FirstOrDefaultAsync(c => c.Id == id, ct);
|
return await db.Clients.FirstOrDefaultAsync(c => c.Id == id && c.RealmId == realmId, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientSecret CreateSecret()
|
private ClientSecret CreateSecret()
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,11 @@
|
||||||
<s:String x:Key="/Default/dotCover/Editor/HighlightingSourceSnapshotLocation/@EntryValue">/home/eelke/.cache/JetBrains/Rider2025.3/resharper-host/temp/Rider/vAny/CoverageData/_IdentityShroud.-1277985570/Snapshot/snapshot.utdcvr</s:String>
|
<s:String x:Key="/Default/dotCover/Editor/HighlightingSourceSnapshotLocation/@EntryValue">/home/eelke/.cache/JetBrains/Rider2025.3/resharper-host/temp/Rider/vAny/CoverageData/_IdentityShroud.-1277985570/Snapshot/snapshot.utdcvr</s:String>
|
||||||
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue">/home/eelke/.dotnet/dotnet</s:String>
|
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/DotNetCliExePath/@EntryValue">/home/eelke/.dotnet/dotnet</s:String>
|
||||||
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/home/eelke/.dotnet/sdk/10.0.102/MSBuild.dll</s:String>
|
<s:String x:Key="/Default/Environment/Hierarchy/Build/BuildTool/CustomBuildToolPath/@EntryValue">/home/eelke/.dotnet/sdk/10.0.102/MSBuild.dll</s:String>
|
||||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=4bf578c0_002Dc8f9_002D46e4_002D9bdc_002D38da0a3f253a/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=92a0e31a_002D2dfa_002D4c9d_002D994b_002D2d5679155267/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||||
<Solution />
|
<Solution />
|
||||||
</SessionState></s:String>
|
</SessionState></s:String>
|
||||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=7ce84f90_002D6ae2_002D4e9e_002D860e_002Deb90f45871f3/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="Junie Session" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
|
||||||
<ProjectFile>DC887623-8680-4D3B-B23A-D54F7DA91891/d:Services/f:ClientServiceTests.cs</ProjectFile>
|
|
||||||
</SessionState></s:String>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue