Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,27 @@
// .NET lib/sdk debugging
// https://github.com/OmniSharp/omnisharp-vscode/wiki/Debugging-into-the-.NET-Framework-itself
"justMyCode": false,
// symbolOptions tell the debugger to try to download symbols for external libs. After you have debugged and downloaded all the symbols that you need and are available, comment this out so the debugger doesn't keep looking for symbols that aren't available.
/*"symbolOptions": {
/* symbolOptions tell the debugger to try to download symbols for external libs. After you have debugged and downloaded all the symbols that you need and are available, comment this out so the debugger doesn't keep looking for symbols that aren't available.
"symbolOptions": {
"searchMicrosoftSymbolServer": true,
"searchNuGetOrgSymbolServer": true
},*/
}, //*/
"suppressJITOptimizations": true,
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
"request": "attach",
"processName": "WebApi.exe",
// .NET lib/sdk debugging
// https://github.com/OmniSharp/omnisharp-vscode/wiki/Debugging-into-the-.NET-Framework-itself
"justMyCode": false,
/* symbolOptions tell the debugger to try to download symbols for external libs. After you have debugged and downloaded all the symbols that you need and are available, comment this out so the debugger doesn't keep looking for symbols that aren't available.
"symbolOptions": {
"searchMicrosoftSymbolServer": true,
"searchNuGetOrgSymbolServer": true
}, //*/
"suppressJITOptimizations": true,
},
]
}
6 changes: 3 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
// .NET lib/sdk debugging
// https://github.com/OmniSharp/omnisharp-vscode/wiki/Debugging-into-the-.NET-Framework-itself
"justMyCode": false,
// symbolOptions tell the debugger to try to download symbols for external libs. After you have debugged and downloaded all the symbols that you need and are available, comment this out so the debugger doesn't keep looking for symbols that aren't available.
/*"symbolOptions": {
/* symbolOptions tell the debugger to try to download symbols for external libs. After you have debugged and downloaded all the symbols that you need and are available, comment this out so the debugger doesn't keep looking for symbols that aren't available.
"symbolOptions": {
"searchMicrosoftSymbolServer": true,
"searchNuGetOrgSymbolServer": true
},*/
}, //*/
"suppressJITOptimizations": true,
"env": {
"COMPlus_ReadyToRun": "0"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ You can view the implementation of external libs (e.g. the .NET SDK) directly in

1) The simplest way is to turn on `omnisharp.enableDecompilationSupport` (only available in user settings, because a user agreement is required). When navigating to external code, libraries are then decompiled rather than only showing metadata. The decompiled code is, of course, different than the original source code.

2) The original source code can only be viewed while debugging by stepping into the code using the debugging tools (as far as I can figure out). Appropriate settings for this have been added to the [workspace settings](.vscode\settings.json) for debugging our unit tests and to the .NET [launch configuration](.vscode\launch.json) for debugging the application itself. In each case, a few lines need to be uncommented in order to tell the debugger to download the source code. This slows down the debugging process, so the lines should be commented out again once the libs have been downloaded.
2) The original source code can only be viewed while debugging by stepping into the code using the debugging tools (as far as I can figure out). Appropriate settings for this have been added to the [workspace settings](.vscode\settings.json) for debugging our unit tests and to the .NET [launch configurations](.vscode\launch.json) for debugging the application itself. In each case, a few lines need to be uncommented in order to tell the debugger to download the source code. This slows down the debugging process, so the lines should be commented out again once the libs have been downloaded.
2 changes: 1 addition & 1 deletion backend/DataAccess/DataServiceKernel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static void Setup(IServiceCollection services)
services.AddSingleton(provider => new MongoClient(provider.GetRequiredService<MongoClientSettings>()));

services.AddSingleton<SystemDbContext>();
services.AddSingleton<ProjectDbContext>();
services.AddScoped<ProjectDbContext>();
}

public static MongoClientSettings BuildMongoClientSettings(string connectionString, IServiceProvider provider)
Expand Down
1 change: 1 addition & 0 deletions backend/DataAccess/Entities/EntityDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public class EntityDocument<T> : EntityBase
{
[BsonId]
public required LfId<T> Id { get; init; }
public required DateTimeOffset DateCreated { get; init; }
}
58 changes: 56 additions & 2 deletions backend/DataAccess/Entities/Entry.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,60 @@
namespace LanguageForge.Api.Entities;

public class Entry
public class Entry : EntityDocument<Entry>
{
public required Dictionary<string, MultiTextValue> Lexeme { get; set; }
public required Dictionary<string, ValueWrapper>? Lexeme { get; set; }
public required Dictionary<string, ValueWrapper>? CitationForm { get; set; }
public required Dictionary<string, ValueWrapper>? Pronunciation { get; set; }
public required Dictionary<string, ValueWrapper>? CVPattern { get; set; }
public required Dictionary<string, ValueWrapper>? Tone { get; set; }
public required ValueWrapper? Location { get; set; }
public required Dictionary<string, ValueWrapper>? Etymology { get; set; }
public required Dictionary<string, ValueWrapper>? EtymologyGloss { get; set; }
public required Dictionary<string, ValueWrapper>? EtymologyComment { get; set; }
public required Dictionary<string, ValueWrapper>? EtymologySource { get; set; }
public required Dictionary<string, ValueWrapper>? Note { get; set; }
public required Dictionary<string, ValueWrapper>? LiteralMeaning { get; set; }
public required Dictionary<string, ValueWrapper>? Bibliography { get; set; }
public required Dictionary<string, ValueWrapper>? Restrictions { get; set; }
public required Dictionary<string, ValueWrapper>? SummaryDefinition { get; set; }
public required List<Sense>? Senses { get; set; }
}

public class Sense : EntityBase
{
public required Guid Guid { get; set; }
public required Dictionary<string, ValueWrapper>? Gloss { get; set; }
public required Dictionary<string, ValueWrapper>? Definition { get; set; }
public required List<File>? Pictures { get; set; }
public required ValueWrapper? PartOfSpeech { get; set; }
public required ValuesWrapper? SemanticDomain { get; set; }
public required Dictionary<string, ValueWrapper>? ScientificName { get; set; }
public required Dictionary<string, ValueWrapper>? AnthropologyNote { get; set; }
public required Dictionary<string, ValueWrapper>? Bibliography { get; set; }
public required Dictionary<string, ValueWrapper>? DiscourseNote { get; set; }
public required Dictionary<string, ValueWrapper>? EncyclopedicNote { get; set; }
public required Dictionary<string, ValueWrapper>? GeneralNote { get; set; }
public required Dictionary<string, ValueWrapper>? GrammarNote { get; set; }
public required Dictionary<string, ValueWrapper>? PhonologyNote { get; set; }
public required Dictionary<string, ValueWrapper>? Restrictions { get; set; }
public required Dictionary<string, ValueWrapper>? SemanticsNote { get; set; }
public required Dictionary<string, ValueWrapper>? SociolinguisticsNote { get; set; }
public required Dictionary<string, ValueWrapper>? Source { get; set; }
public required ValueWrapper? SenseType { get; set; }
public required ValueWrapper? Status { get; set; }
public required List<Example>? Examples { get; set; }
}

public class Example : EntityBase
{
public required Guid Guid { get; set; }
public required Dictionary<string, ValueWrapper>? Sentence { get; set; }
public required Dictionary<string, ValueWrapper>? Translation { get; set; }
public required Dictionary<string, ValueWrapper>? Reference { get; set; }
}

public class File : EntityBase
{
public required Guid Guid { get; set; }
public required string FileName { get; set; }
}
4 changes: 3 additions & 1 deletion backend/DataAccess/Entities/MultiTextValue.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
namespace LanguageForge.Api.Entities;

public record MultiTextValue(string Value);
public record ValueWrapper(string Value);

public record ValuesWrapper(List<string> Values);
15 changes: 15 additions & 0 deletions backend/DataAccess/Entities/OptionList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace LanguageForge.Api.Entities;

public class OptionList
{
public required string Code { get; init; }
public required List<Option> Items { get; init; }
}

public class Option
{
public required Guid Guid { get; init; }
public required string Key { get; init; }
public required string Value { get; init; }
public required string Abbreviation { get; init; }
}
3 changes: 1 addition & 2 deletions backend/DataAccess/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ public User(string role)
/// bcrypt hashed password
/// </summary>
public required string Password { get; init; }
public required DateTimeOffset DateCreated { get; init; }

[BsonElement("role")]
private readonly string _roleString;
Expand All @@ -30,7 +29,7 @@ public required UserRole Role
init => _roleString = UserRoleToString(value);
}

public required List<LfId<Project>> Projects { get; init; }
public required List<LfId<Project>>? Projects { get; init; }

private UserRole UserRoleFromString(string role)
{
Expand Down
6 changes: 6 additions & 0 deletions backend/DataAccess/LfProjectContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace LanguageForge.Api;

public interface ILfProjectContext
{
public string? ProjectCode { get; }
}
32 changes: 27 additions & 5 deletions backend/DataAccess/ProjectDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,41 @@ namespace LanguageForge.Api;
public class ProjectDbContext
{
private readonly MongoClient _mongoClient;
private readonly ILfProjectContext _projectContext;

public ProjectDbContext(MongoClient mongoClient)
private string ProjectCode
{
get
{
if (_projectContext.ProjectCode == null)
{
throw new InvalidOperationException($"{nameof(ILfProjectContext.ProjectCode)} can only be used in the context of individual projects.");
}
return _projectContext.ProjectCode;
}
}

public ProjectDbContext(MongoClient mongoClient, ILfProjectContext projectContext)
{
_mongoClient = mongoClient;
_projectContext = projectContext;
}

private IMongoCollection<T> GetCollection<T>(string collectionName)
{
return _mongoClient.GetDatabase($"sf_{ProjectCode}").GetCollection<T>(collectionName);
}

private IMongoCollection<T> GetCollection<T>(string projectCode, string collectionName)
public IMongoCollection<Entry> Entries()
{
return _mongoClient.GetDatabase($"sf_{projectCode}").GetCollection<T>(collectionName);
return GetCollection<Entry>("lexicon");
}

public IMongoCollection<Entry> Entries(string projectCode)
public Task<List<Option>> PartsOfSpeech()
{
return GetCollection<Entry>(projectCode, "lexicon");
return GetCollection<OptionList>("optionlists")
.Find(list => list.Code == "grammatical-info")
.Project(list => list.Items)
.SingleOrDefaultAsync();
}
}
1 change: 1 addition & 0 deletions backend/UnitTests/UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MongoDB.Analyzer" Version="1.1.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="Shouldly" Version="4.1.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
Expand Down
31 changes: 31 additions & 0 deletions backend/UnitTests/WebApi/Auth/AuthenticationServiceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using LanguageForge.UnitTests.Fixtures;
using LanguageForge.WebApi.Auth;
using Microsoft.Extensions.DependencyInjection;

namespace LanguageForge.UnitTests.WebApi.Auth;

public class AuthenticationServiceTest : IClassFixture<IntegrationTestFixture>
{
private readonly AuthenticationService _authService;

public AuthenticationServiceTest(IntegrationTestFixture iocFixture)
{
_authService = iocFixture.Services.GetRequiredService<AuthenticationService>();
}

[Fact]
public async Task TestAuthenticationWithUsernameEmailAndPassword()
{
(await _authService.Authenticate("admin", "password")).ShouldNotBeNull();
(await _authService.Authenticate("admin@example.com", "password")).ShouldNotBeNull();
(await _authService.Authenticate("admin_", "password")).ShouldBeNull();
(await _authService.Authenticate("admin", "password_")).ShouldBeNull();
}

[Fact]
public async Task TestAuthenticationWithEmail()
{
(await _authService.Authenticate("admin@example.com")).ShouldNotBeNull();
(await _authService.Authenticate("admin_@example.com")).ShouldBeNull();
}
}
55 changes: 55 additions & 0 deletions backend/UnitTests/WebApi/Auth/ProjectAuthorizationHandlerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using LanguageForge.Api.Entities;
using LanguageForge.WebApi.Auth;
using Microsoft.AspNetCore.Authorization;
using static LanguageForge.UnitTests.WebApi.ContextHelpers;

namespace LanguageForge.UnitTests.WebApi.Auth;

public class ProjectAuthorizationHandlerTest
{
[Fact]
public async Task FailsIfUserIsNotAuthorized()
{
// GIVEN a user that is not authorized
var user = new LfUser("test@testeroolaboom.fun", LfId<User>.Parse("User:6359f8855e3dc273d4662f2a"),
UserRole.User,
new[] {
new UserProjectRole("fun-language", ProjectRole.Manager),
new UserProjectRole("spooky-language", ProjectRole.Contributor),
});
var webContext = WebContext(user);
var projectContext = ProjectContext("missing-language");

// WHEN the handler is invoked
var handler = new ProjectAuthorizationHandler(webContext, projectContext);
var req = new ProjectAuthorizationRequirement();
var context = new AuthorizationHandlerContext(new IAuthorizationRequirement[] { req }, null, null);
await handler.HandleAsync(context);

// THEN the handler fails
context.HasFailed.ShouldBeTrue();
}

[Fact]
public async Task SucceedsIfUserIsAdmin()
{
// GIVEN an admin user
var user = new LfUser("test@testeroolaboom.fun", LfId<User>.Parse("User:6359f8855e3dc273d4662f2a"),
UserRole.SystemAdmin,
new[] {
new UserProjectRole("fun-language", ProjectRole.Manager),
new UserProjectRole("spooky-language", ProjectRole.Contributor),
});
var webContext = WebContext(user);
var projectContext = ProjectContext("missing-language");

// WHEN the handler is invoked
var handler = new ProjectAuthorizationHandler(webContext, projectContext);
var req = new ProjectAuthorizationRequirement();
var context = new AuthorizationHandlerContext(new IAuthorizationRequirement[] { req }, null, null);
await handler.HandleAsync(context);

// THEN the handler succeeds
context.HasSucceeded.ShouldBeTrue();
}
}
22 changes: 22 additions & 0 deletions backend/UnitTests/WebApi/ContextHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using LanguageForge.WebApi;
using LanguageForge.WebApi.Auth;
using Moq;

namespace LanguageForge.UnitTests.WebApi;

public static class ContextHelpers
{
public static ILfWebContext WebContext(LfUser user)
{
var contextMock = new Mock<ILfWebContext>();
contextMock.SetupGet(c => c.User).Returns(user);
return contextMock.Object;
}

public static ILfProjectContext ProjectContext(string projectCode)
{
var contextMock = new Mock<ILfProjectContext>();
contextMock.SetupGet(c => c.ProjectCode).Returns(projectCode);
return contextMock.Object;
}
}
15 changes: 15 additions & 0 deletions backend/UnitTests/WebApi/Services/EntryServiceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using LanguageForge.UnitTests.Fixtures;
using LanguageForge.WebApi.Services;
using Microsoft.Extensions.DependencyInjection;

namespace LanguageForge.UnitTests.WebApi.Services;

public class EntryServiceTest : IClassFixture<IntegrationTestFixture>
{
private readonly EntryService _entryService;

public EntryServiceTest(IntegrationTestFixture iocFixture)
{
_entryService = iocFixture.Services.GetRequiredService<EntryService>();
}
}
8 changes: 0 additions & 8 deletions backend/UnitTests/WebApi/Services/UserServiceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,6 @@ public async Task UpdateUserWorks()
user.Name.ShouldBe(newName);
}

[Fact]
public async Task FindLfUserWorks()
{
var user = await FirstUser();
var lfUser = await _userService.FindLfUser(user.Email);
lfUser.ShouldNotBeNull();
}

private async Task<UserDto> FirstUser()
{
var users = await _userService.ListUsers();
Expand Down
Loading