Skip to content
This repository has been archived by the owner on Mar 1, 2024. It is now read-only.

Commit

Permalink
Merge pull request #251 from rinkudesu/ignore-http-when-filtering
Browse files Browse the repository at this point in the history
Ignore "http(s)://"  when filtering and sorting
  • Loading branch information
KowalskiPiotr98 authored Jun 4, 2023
2 parents 7f12f18 + 9b67e97 commit defb2d1
Show file tree
Hide file tree
Showing 14 changed files with 310 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ public LinkDbContext(DbContextOptions<LinkDbContext> options) : base(options)

public DbSet<Link> Links => Set<Link>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Link>().Property(l => l.SearchableUrl).HasComputedColumnSql(@"upper(regexp_replace(""LinkUrl"", '^https?:\/\/', ''))", stored: true);
}

public void ClearTracked()
{
ChangeTracker.Clear();
}
}
}
}
13 changes: 10 additions & 3 deletions Rinkudesu.Services.Links/Rinkudesu.Services.Links.Models/Link.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@ public class Link
public Guid Id { get; set; }
[DataType(DataType.Url)]
[Required]
[SuppressMessage("Design", "CA1056", MessageId = "URI-like properties should not be strings")]
[MaxLength(200)]
public string LinkUrl { get; set; }
public Uri LinkUrl { get; set; } = null!;
/// <summary>
/// This field can be safely used to filter and sort links.
/// </summary>
/// <remarks>
/// Please note that this field is set by the database, so it will not have a value before it's saved and then retrieved.
/// </remarks>
[MaxLength(200)]
[SuppressMessage("Design", "CA1056:URI-like properties should not be strings")]
public string SearchableUrl { get; private set; } = null!;
[Required]
[DataType(DataType.Text)]
[MaxLength(250)]
Expand All @@ -40,7 +48,6 @@ public Link()
{
CreationDate = DateTime.UtcNow;
LastUpdate = CreationDate;
LinkUrl = string.Empty;
Title = string.Empty;
CreatingUserId = Guid.Empty;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using Rinkudesu.Services.Links.Models;

namespace Rinkudesu.Services.Links.Repositories.QueryModels
{
public class LinkListQueryModel
public partial class LinkListQueryModel
{
/// <summary>
/// UserId creating the link
Expand Down Expand Up @@ -94,7 +95,8 @@ public IQueryable<Link> FilterUrlContains(IQueryable<Link> links)
{
if (UrlContains != null)
{
return links.Where(l => l.LinkUrl.Contains(UrlContains));
var actionUrlContains = HttpStartRegex().Replace(UrlContains, string.Empty).ToUpperInvariant();
return links.Where(l => l.SearchableUrl.Contains(actionUrlContains));
}
return links;
}
Expand Down Expand Up @@ -141,8 +143,8 @@ public IQueryable<Link> SortLinks(IQueryable<Link> links) =>
? links.OrderByDescending(l => l.Title)
: links.OrderBy(l => l.Title),
LinkListSortOptions.Url => SortDescending
? links.OrderByDescending(l => l.LinkUrl)
: links.OrderBy(l => l.LinkUrl),
? links.OrderByDescending(l => l.SearchableUrl)
: links.OrderBy(l => l.SearchableUrl),
LinkListSortOptions.CreationDate => SortDescending
? links.OrderByDescending(l => l.CreationDate)
: links.OrderBy(l => l.CreationDate),
Expand All @@ -156,6 +158,9 @@ public override string ToString()
{
return System.Text.Json.JsonSerializer.Serialize(this);
}

[GeneratedRegex("^https?://", RegexOptions.IgnoreCase, "en-PL")]
private static partial Regex HttpStartRegex();
}

public enum LinkListSortOptions
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Rinkudesu.Gateways.Utils;
using Rinkudesu.Services.Links.Models;
using Rinkudesu.Services.Links.Repositories.QueryModels;
using Xunit;
Expand All @@ -16,13 +18,17 @@ private void PopulateLinks()
{
links = new List<Link>
{
new Link(),
new Link { LinkUrl = "http://localhost/" },
new Link { Title = "ayaya" },
new Link { Description = "tuturu*" },
new Link { PrivacyOptions = Link.LinkPrivacyOptions.Public },
new Link { CreatingUserId = _userId }
new Link { LinkUrl = Guid.NewGuid().ToString().ToUri() },
new Link { LinkUrl = "http://localhost/".ToUri() },
new Link { Title = "ayaya", LinkUrl = Guid.NewGuid().ToString().ToUri() },
new Link { Description = "tuturu*", LinkUrl = Guid.NewGuid().ToString().ToUri() },
new Link { PrivacyOptions = Link.LinkPrivacyOptions.Public, LinkUrl = Guid.NewGuid().ToString().ToUri() },
new Link { CreatingUserId = _userId, LinkUrl = Guid.NewGuid().ToString().ToUri() },
};
foreach (var link in links)
{
FillSearchableUrl(link);
}
}

[Fact]
Expand Down Expand Up @@ -130,7 +136,7 @@ public void LinkListQueryModelFilterUrlContains_UrlContainsExistingValue_SingleL
var result = model.FilterUrlContains(links.AsQueryable());

Assert.Single(result);
Assert.Contains(url, result.First().LinkUrl, StringComparison.InvariantCultureIgnoreCase);
Assert.Contains(url, result.First().LinkUrl.ToString(), StringComparison.InvariantCultureIgnoreCase);
}

[Fact]
Expand Down Expand Up @@ -215,27 +221,27 @@ private static List<Link> GetSortTestLinks()
{
new Link
{
Title = "a", LinkUrl = "z", CreationDate = DateTime.Now.AddDays(-6),
Title = "a", LinkUrl = "z".ToUri(), CreationDate = DateTime.Now.AddDays(-6),
LastUpdate = DateTime.Now.AddDays(-10)
},
new Link
{
Title = "e", LinkUrl = "a", CreationDate = DateTime.Now.AddDays(-3),
Title = "e", LinkUrl = "a".ToUri(), CreationDate = DateTime.Now.AddDays(-3),
LastUpdate = DateTime.Now.AddDays(-2)
},
new Link
{
Title = "j", LinkUrl = "s", CreationDate = DateTime.Now.AddDays(-2),
Title = "j", LinkUrl = "s".ToUri(), CreationDate = DateTime.Now.AddDays(-2),
LastUpdate = DateTime.Now.AddDays(-4)
},
new Link
{
Title = "b", LinkUrl = "i", CreationDate = DateTime.Now.AddDays(-4),
Title = "b", LinkUrl = "i".ToUri(), CreationDate = DateTime.Now.AddDays(-4),
LastUpdate = DateTime.Now.AddDays(-1)
},
new Link
{
Title = "z", LinkUrl = "j", CreationDate = DateTime.Now.AddDays(-9), LastUpdate = DateTime.Now
Title = "z", LinkUrl = "j".ToUri(), CreationDate = DateTime.Now.AddDays(-9), LastUpdate = DateTime.Now
},
};
}
Expand Down Expand Up @@ -278,7 +284,7 @@ public void LinkListQueryModelSortLinks_SortAscByUrl_SortedCorrectly()

var result = model.SortLinks(testLinks.AsQueryable()).ToList();

var sortedLinks = testLinks.OrderBy(l => l.LinkUrl).ToList();
var sortedLinks = testLinks.OrderBy(l => l.SearchableUrl).ToList();
for (int i = 0; i < testLinks.Count; i++)
{
Assert.Equal(sortedLinks[i].Title, result[i].Title);
Expand All @@ -293,7 +299,7 @@ public void LinkListQueryModelSortLinks_SortDescByUrl_SortedCorrectly()

var result = model.SortLinks(testLinks.AsQueryable()).ToList();

var sortedLinks = testLinks.OrderByDescending(l => l.LinkUrl).ToList();
var sortedLinks = testLinks.OrderByDescending(l => l.SearchableUrl).ToList();
for (int i = 0; i < testLinks.Count; i++)
{
Assert.Equal(sortedLinks[i].Title, result[i].Title);
Expand Down Expand Up @@ -391,5 +397,12 @@ public void LinkListQueryModelSkipTake_ValuesProvided_ReturnedCorrectCollection(
Assert.Equal(sortedLinks[i].Title, result[i - 1].Title);
}
}

// this uses reflections as normally this would be set by the database and doing anything to allow this field to be set programmatically would be "tests only" anyway
private static void FillSearchableUrl(Link link)
{
var property = link.GetType().GetProperty(nameof(Link.SearchableUrl));
property!.SetValue(link, link.LinkUrl.ToString().Replace("https://", string.Empty).Replace("http://", string.Empty).ToUpperInvariant());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Rinkudesu.Gateways.Utils;
using Rinkudesu.Kafka.Dotnet.Base;
using Rinkudesu.Kafka.Dotnet.Exceptions;
using Rinkudesu.Services.Links.MessageQueues.Messages;
Expand Down Expand Up @@ -33,12 +34,12 @@ private async Task PopulateLinksAsync()
{
links = new List<Link>
{
new Link { CreatingUserId = _userId, LinkUrl = Guid.NewGuid().ToString() },
new Link { LinkUrl = "http://localhost/", CreatingUserId = _userId },
new Link { Title = "ayaya", ShareableKey = "test", CreatingUserId = _userId, LinkUrl = Guid.NewGuid().ToString() },
new Link { Description = "tuturu*", CreatingUserId = Guid.NewGuid(), LinkUrl = Guid.NewGuid().ToString() },
new Link { PrivacyOptions = Link.LinkPrivacyOptions.Public, CreatingUserId = Guid.NewGuid(), LinkUrl = Guid.NewGuid().ToString() },
new Link { CreatingUserId = _userId, LinkUrl = Guid.NewGuid().ToString() }
new Link { CreatingUserId = _userId, LinkUrl = Guid.NewGuid().ToString().ToUri() },
new Link { LinkUrl = "http://localhost/".ToUri(), CreatingUserId = _userId },
new Link { Title = "ayaya", ShareableKey = "test", CreatingUserId = _userId, LinkUrl = Guid.NewGuid().ToString().ToUri() },
new Link { Description = "tuturu*", CreatingUserId = Guid.NewGuid(), LinkUrl = Guid.NewGuid().ToString().ToUri() },
new Link { PrivacyOptions = Link.LinkPrivacyOptions.Public, CreatingUserId = Guid.NewGuid(), LinkUrl = Guid.NewGuid().ToString().ToUri() },
new Link { CreatingUserId = _userId, LinkUrl = Guid.NewGuid().ToString().ToUri() }
};
_context.Links.AddRange(links);
await _context.SaveChangesAsync();
Expand Down Expand Up @@ -103,7 +104,7 @@ public async Task LinkRepositoryCreateLink_NewLink_LinkAddedCorrectly()
{
await PopulateLinksAsync();
var repo = CreateRepository();
var link = new Link();
var link = new Link { LinkUrl = "http://localhost/".ToUri() };

await repo.CreateLinkAsync(link);

Expand Down Expand Up @@ -131,6 +132,7 @@ public async Task LinkRepositoryUpdateLink_UpdateExistingLinkWithValidUserId_Lin
{
Id = link.Id,
Description = "test",
LinkUrl = Guid.NewGuid().ToString().ToUri(),
CreatingUserId = _userId //TODO: remove this assignment as this should not be changeable here
};
_context.ClearTracked();
Expand Down Expand Up @@ -209,7 +211,7 @@ public async Task LinkRepositoryDeleteLink_DeleteLinkWithInvalidId_DataNotFoundT
public async Task CreateLink_CreationAndUpdateDatesSet_CustomValuesIgnored()
{
var repo = CreateRepository();
var link = new Link { CreationDate = DateTime.MinValue, LastUpdate = DateTime.MaxValue };
var link = new Link { CreationDate = DateTime.MinValue, LastUpdate = DateTime.MaxValue, LinkUrl = "http://localhost/".ToUri() };

await repo.CreateLinkAsync(link);

Expand All @@ -222,12 +224,12 @@ public async Task CreateLink_CreationAndUpdateDatesSet_CustomValuesIgnored()
public async Task UpdateLink_CreationAndUpdateDatesSet_CustomValuesIgnored()
{
var userId = Guid.NewGuid();
_context.Links.Add(new Link { CreationDate = DateTime.MinValue, CreatingUserId = userId });
_context.Links.Add(new Link { CreationDate = DateTime.MinValue, CreatingUserId = userId, LinkUrl = Guid.NewGuid().ToString().ToUri() });
await _context.SaveChangesAsync();
var id = _context.Links.First().Id;
_context.ClearTracked();
var repo = CreateRepository();
var link = new Link { Id = id, CreationDate = DateTime.MaxValue, LastUpdate = DateTime.MinValue };
var link = new Link { Id = id, CreationDate = DateTime.MaxValue, LastUpdate = DateTime.MinValue, LinkUrl = Guid.NewGuid().ToString().ToUri() };

await repo.UpdateLinkAsync(link, userId);

Expand Down Expand Up @@ -261,7 +263,7 @@ public async Task LinkRepositoryCreateLink_NewLinkQueuePublishFails_LinkNotAdded
{
_mockKafkaHandler.Setup(k => k.Produce(It.IsAny<string>(), It.IsAny<LinkMessage>(), It.IsAny<CancellationToken>())).ThrowsAsync(new KafkaProduceException());
var repo = CreateRepository();
var link = new Link();
var link = new Link { LinkUrl = "http://localhost/".ToUri() };

await Assert.ThrowsAsync<KafkaProduceException>(() => repo.CreateLinkAsync(link));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Rinkudesu.Gateways.Utils;
using Rinkudesu.Services.Links.Models;
using Rinkudesu.Services.Links.Repositories;
using Rinkudesu.Services.Links.Repositories.Exceptions;
Expand Down Expand Up @@ -125,9 +126,9 @@ public async Task GetKey_LinkSharedUserAuthorised_ReturnsLink()

private List<Link> GetLinks() => new()
{
new() { CreatingUserId = _userInfo.UserId, LinkUrl = Guid.NewGuid().ToString() },
new() { CreatingUserId = _userInfo.UserId, ShareableKey = "test", LinkUrl = Guid.NewGuid().ToString() },
new() { CreatingUserId = Guid.NewGuid(), LinkUrl = Guid.NewGuid().ToString() },
new() { CreatingUserId = Guid.NewGuid(), ShareableKey = "test", LinkUrl = Guid.NewGuid().ToString() },
new() { CreatingUserId = _userInfo.UserId, LinkUrl = Guid.NewGuid().ToString().ToUri() },
new() { CreatingUserId = _userInfo.UserId, ShareableKey = "test", LinkUrl = Guid.NewGuid().ToString().ToUri() },
new() { CreatingUserId = Guid.NewGuid(), LinkUrl = Guid.NewGuid().ToString().ToUri() },
new() { CreatingUserId = Guid.NewGuid(), ShareableKey = "test", LinkUrl = Guid.NewGuid().ToString().ToUri() },
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,14 @@ public async Task<ActionResult<LinkDto>> GetSingleByKey(string key)
[HttpPost]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<ActionResult<LinkDto>> Create([FromBody] LinkDto newLink)
public async Task<ActionResult<LinkDto>> Create([FromBody] LinkCreateDto newLink)
{
if (!ModelState.IsValid || !newLink.IsLinkUrlValid())
return BadRequest();

using var scope = _logger.BeginScope("Creating a link {link}", newLink);
var link = _mapper.Map<Link>(newLink);
link.CreatingUserId = User.GetIdAsGuid();
if (!TryValidateModel(link))
{
return BadRequest();
}
try
{
await _repository.CreateLinkAsync(link).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using AutoMapper;
using Rinkudesu.Services.Links.DataTransferObjects.V1;
using Rinkudesu.Services.Links.Models;
Expand All @@ -12,11 +11,8 @@ public class LinkMappingProfile : Profile
{
public LinkMappingProfile()
{
CreateMap<Link, LinkDto>().ForMember(m => m.LinkUrl, c => c.MapFrom(source => new Uri(source.LinkUrl, UriKind.RelativeOrAbsolute)));
CreateMap<LinkDto, Link>().ForMember(m => m.Id, options => options.Ignore())
.ForMember(m => m.CreationDate, options => options.Ignore())
.ForMember(m => m.LastUpdate, options => options.Ignore())
.ForMember(m => m.CreatingUserId, options => options.Ignore());
CreateMap<Link, LinkDto>();
CreateMap<LinkCreateDto, Link>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using Rinkudesu.Services.Links.Models;

namespace Rinkudesu.Services.Links.DataTransferObjects.V1
{
/// <summary>
/// Data transfer object to send and receive <see cref="Link"/> objects
/// </summary>
[ExcludeFromCodeCoverage]
public class LinkCreateDto
{
/// <summary>
/// URL the link is pointing to
/// </summary>
[DataType(DataType.Url)]
[MaxLength(200)]
[SuppressMessage("Design", "CA1056:URI-like properties should not be strings")]
public string LinkUrl { get; set; } = null!;
/// <summary>
/// Title of the link
/// </summary>
[MaxLength(250)]
public string Title { get; set; } = null!;
/// <summary>
/// Description of the link
/// </summary>
[MaxLength(1000)]
public string? Description { get; set; }
/// <summary>
/// Privacy configuration of the link
/// </summary>
public Link.LinkPrivacyOptions PrivacyOptions { get; set; }

/// <summary>
/// Verifies whether <see cref="LinkUrl"/> is a valid absolute url.
/// </summary>
public bool IsLinkUrlValid() => Uri.TryCreate(LinkUrl, UriKind.Absolute, out _);
}
}
Loading

0 comments on commit defb2d1

Please sign in to comment.