From f184224cf9e85b4835da3146d5871d47a9b33c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 20 Sep 2025 09:55:52 +0200 Subject: [PATCH 1/9] delete existing files --- .../Enums/MatchOutcomeType.cs | 27 --- src/Turnierplan.Adapter/Enums/MatchState.cs | 22 -- src/Turnierplan.Adapter/Enums/MatchType.cs | 32 --- src/Turnierplan.Adapter/Enums/Visibility.cs | 19 -- src/Turnierplan.Adapter/Models/Group.cs | 34 --- .../Models/GroupParticipant.cs | 23 -- src/Turnierplan.Adapter/Models/Match.cs | 82 -------- .../Models/MatchTeamInfo.cs | 23 -- src/Turnierplan.Adapter/Models/Ranking.cs | 27 --- src/Turnierplan.Adapter/Models/Team.cs | 23 -- .../Models/TeamGroupStatistics.cs | 53 ----- .../Models/TeamSelector.cs | 17 -- src/Turnierplan.Adapter/Models/Tournament.cs | 59 ------ .../Models/TournamentHeader.cs | 34 --- src/Turnierplan.Adapter/TurnierplanClient.cs | 197 ------------------ .../TurnierplanClientException.cs | 12 -- .../TurnierplanClientOptions.cs | 53 ----- 17 files changed, 737 deletions(-) delete mode 100644 src/Turnierplan.Adapter/Enums/MatchOutcomeType.cs delete mode 100644 src/Turnierplan.Adapter/Enums/MatchState.cs delete mode 100644 src/Turnierplan.Adapter/Enums/MatchType.cs delete mode 100644 src/Turnierplan.Adapter/Enums/Visibility.cs delete mode 100644 src/Turnierplan.Adapter/Models/Group.cs delete mode 100644 src/Turnierplan.Adapter/Models/GroupParticipant.cs delete mode 100644 src/Turnierplan.Adapter/Models/Match.cs delete mode 100644 src/Turnierplan.Adapter/Models/MatchTeamInfo.cs delete mode 100644 src/Turnierplan.Adapter/Models/Ranking.cs delete mode 100644 src/Turnierplan.Adapter/Models/Team.cs delete mode 100644 src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs delete mode 100644 src/Turnierplan.Adapter/Models/TeamSelector.cs delete mode 100644 src/Turnierplan.Adapter/Models/Tournament.cs delete mode 100644 src/Turnierplan.Adapter/Models/TournamentHeader.cs delete mode 100644 src/Turnierplan.Adapter/TurnierplanClient.cs delete mode 100644 src/Turnierplan.Adapter/TurnierplanClientException.cs delete mode 100644 src/Turnierplan.Adapter/TurnierplanClientOptions.cs diff --git a/src/Turnierplan.Adapter/Enums/MatchOutcomeType.cs b/src/Turnierplan.Adapter/Enums/MatchOutcomeType.cs deleted file mode 100644 index 8b37ec90..00000000 --- a/src/Turnierplan.Adapter/Enums/MatchOutcomeType.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Turnierplan.Adapter.Enums; - -/// -/// The match outcome type gives an indication of how a match ended. -/// -public enum MatchOutcomeType -{ - /// - /// Indicates that the match ended after regular time without penalty shootout. - /// - Standard, - - /// - /// Indicates that the match ended after over time but without penalty shootout. - /// - AfterOvertime, - - /// - /// Indicates that the match ended after penalty shootout. - /// - AfterPenalties, - - /// - /// Indicates that the match was not completed normally. Rather, the score is the result of some extraordinary circumstance. - /// - SpecialScoring -} diff --git a/src/Turnierplan.Adapter/Enums/MatchState.cs b/src/Turnierplan.Adapter/Enums/MatchState.cs deleted file mode 100644 index edddabaf..00000000 --- a/src/Turnierplan.Adapter/Enums/MatchState.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Turnierplan.Adapter.Enums; - -/// -/// Represents the state of a match. -/// -public enum MatchState -{ - /// - /// The match has not started yet. - /// - NotStarted, - - /// - /// The match is currently being played, i.e. a "LIVE" outcome was published. - /// - CurrentlyPlaying, - - /// - /// The match is finished, i.e. a "final" result was published. - /// - Finished -} diff --git a/src/Turnierplan.Adapter/Enums/MatchType.cs b/src/Turnierplan.Adapter/Enums/MatchType.cs deleted file mode 100644 index 4dbeb36b..00000000 --- a/src/Turnierplan.Adapter/Enums/MatchType.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Turnierplan.Adapter.Enums; - -/// -/// Represents the type of match. -/// -public enum MatchType -{ - /// - /// A group match between two group participants. - /// - GroupMatch, - - /// - /// A knockout (KO) match which is specifically not the final match, such as semi-final or quarter-final. - /// - NonFinalKnockout, - - /// - /// A playoff match which is specifically not the 3rd position playoff. - /// - AdditionalPlayoff, - - /// - /// The playoff match for 3rd place. - /// - ThirdPlacePlayoff, - - /// - /// The final match or in other words the playoff match for 1st place. - /// - Final -} diff --git a/src/Turnierplan.Adapter/Enums/Visibility.cs b/src/Turnierplan.Adapter/Enums/Visibility.cs deleted file mode 100644 index eae3d0e1..00000000 --- a/src/Turnierplan.Adapter/Enums/Visibility.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Turnierplan.Adapter.Models; - -namespace Turnierplan.Adapter.Enums; - -/// -/// Represents the visibility of a . -/// -public enum Visibility -{ - /// - /// The tournament is only accessible by an authenticated user or API key. - /// - Private = 1, - - /// - /// The tournament can be viewed by who has the correct link with the tournament ID. - /// - Public = 2 -} diff --git a/src/Turnierplan.Adapter/Models/Group.cs b/src/Turnierplan.Adapter/Models/Group.cs deleted file mode 100644 index cd1a7812..00000000 --- a/src/Turnierplan.Adapter/Models/Group.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Turnierplan.Adapter.Models; - -/// -/// Represents a single group in the context of a . -/// -public sealed record Group -{ - /// - /// The numerical ID of this group which is unique in the scope of a single tournament. - /// - public required int Id { get; init; } - - /// - /// The alphabetical ID of this group. This is always an upper-case character between A and Z. - /// - public required char AlphabeticalId { get; init; } - - /// - /// The display name of this group which is either a custom display name configured for this group - /// or the default group name containing the . - /// - /// - public required string DisplayName { get; init; } - - /// - /// Whether this group has a custom display name. - /// - public required bool HasCustomDisplayName { get; init; } - - /// - /// An array of all participants assigned to this group. - /// - public required GroupParticipant[] Participants { get; init; } -} diff --git a/src/Turnierplan.Adapter/Models/GroupParticipant.cs b/src/Turnierplan.Adapter/Models/GroupParticipant.cs deleted file mode 100644 index 08e18427..00000000 --- a/src/Turnierplan.Adapter/Models/GroupParticipant.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Turnierplan.Adapter.Models; - -/// -/// Represents the relationship between a team which is assigned to a group. -/// -public sealed record GroupParticipant -{ - /// - /// The numerical ID of the team. - /// - public required int TeamId { get; init; } - - /// - /// The assigned priority of this team which is used when other means of comparing - /// team results inside a group result in a draw. - /// - public required int Priority { get; init; } - - /// - /// The computed statistics resulting from this team's group matches. - /// - public required TeamGroupStatistics Statistics { get; init; } -} diff --git a/src/Turnierplan.Adapter/Models/Match.cs b/src/Turnierplan.Adapter/Models/Match.cs deleted file mode 100644 index 66ded3da..00000000 --- a/src/Turnierplan.Adapter/Models/Match.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; -using Turnierplan.Adapter.Enums; -using MatchType = Turnierplan.Adapter.Enums.MatchType; - -namespace Turnierplan.Adapter.Models; - -/// -/// Represents a single match in the context of a . A match has two participants -/// which are computed by evaluating the team selectors. Once the match is finished, each participant is -/// assigned a score. -/// -public sealed record Match -{ - /// - /// The numerical ID of this match which is unique in the scope of a single tournament. - /// - public required int Id { get; init; } - - /// - /// Ths index of this match. Upon match plan generation, all matches are assigned sequential indices - /// starting at 1. - /// - public required int Index { get; init; } - - /// - /// The index of the court where this match is played, starting at 0. - /// - public required short Court { get; init; } - - /// - /// The kickoff time of this match, or null if no kickoff time was defined. - /// - public DateTime? Kickoff { get; init; } - - /// - /// The type of this match. - /// - public required MatchType Type { get; init; } - - /// - /// The type of this match as a human-readable, localized value. - /// - /// Group Match or Semi-Final - public required string FormattedType { get; init; } - - /// - /// The of the if this match's outcome counts towards - /// the group statistics or null if this match is a deciding/ranking match. - /// - /// - public int? GroupId { get; init; } - - /// - /// true if the outcome of this match contributes to some group's statistics or false - /// if this match is a deciding/ranking match. - /// - /// - [JsonIgnore] - [MemberNotNullWhen(true, nameof(GroupId))] - public bool IsGroupMatch => GroupId is not null; - - /// - /// The "home" team of this match. - /// - public required MatchTeamInfo TeamA { get; init; } - - /// - /// The "aways" team of this match. - /// - public required MatchTeamInfo TeamB { get; init; } - - /// - /// The state of this match, indicating whether the team's scores and are set. - /// - public required MatchState State { get; init; } - - /// - /// The of this match or null if this match is not finished yet. - /// - public MatchOutcomeType? OutcomeType { get; init; } -} diff --git a/src/Turnierplan.Adapter/Models/MatchTeamInfo.cs b/src/Turnierplan.Adapter/Models/MatchTeamInfo.cs deleted file mode 100644 index ce115971..00000000 --- a/src/Turnierplan.Adapter/Models/MatchTeamInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Turnierplan.Adapter.Models; - -/// -/// Represents the "home" or "away" team of a match. Depending on the state of the tournament, the specific -/// can be available or not. However, the is always available. -/// -public sealed record MatchTeamInfo -{ - /// - /// The team selector for this team. - /// - public required TeamSelector TeamSelector { get; init; } - - /// - /// The of the or null if the team can currently not be evaluated. - /// - public int? TeamId { get; init; } - - /// - /// The score of team participating team or null if this match is not finished yet. - /// - public int? Score { get; init; } -} diff --git a/src/Turnierplan.Adapter/Models/Ranking.cs b/src/Turnierplan.Adapter/Models/Ranking.cs deleted file mode 100644 index 3040e1f9..00000000 --- a/src/Turnierplan.Adapter/Models/Ranking.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Turnierplan.Adapter.Models; - -/// -/// Represents a single ranking in the context of a . There are -/// equally many rankings in a tournament as there are teams. -/// -public sealed record Ranking -{ - /// - /// The placement rank, starting at 1 for the first place and counting up. - /// - public required int PlacementRank { get; init; } - - /// - /// Whether this ranking is currently defined or not. - /// - [MemberNotNullWhen(true, nameof(TeamId))] - public required bool IsDefined { get; init; } - - /// - /// The of the which occupies this ranking or null if - /// this ranking is currently not defined. - /// - public int? TeamId { get; init; } -} diff --git a/src/Turnierplan.Adapter/Models/Team.cs b/src/Turnierplan.Adapter/Models/Team.cs deleted file mode 100644 index c9fdfc7e..00000000 --- a/src/Turnierplan.Adapter/Models/Team.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Turnierplan.Adapter.Models; - -/// -/// Represents a single team in the context of a . -/// -public sealed record Team -{ - /// - /// The numerical ID of this team which is unique in the scope of a single tournament. - /// - public required int Id { get; init; } - - /// - /// The name of this team. - /// - public required string Name { get; init; } - - /// - /// Whether this team plays "out of competition". If it is true, this team will always - /// be regarded worse compared to any other team when sorting teams in a group, for example. - /// - public required bool OutOfCompetition { get; init; } -} diff --git a/src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs b/src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs deleted file mode 100644 index ac9b9459..00000000 --- a/src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Turnierplan.Adapter.Models; - -/// -/// Value object which holds a single s statistics in the context -/// of the to which this team is assigned to. -/// -public sealed record TeamGroupStatistics -{ - /// - /// The position of the team in the group, starting at 1 for the best team and counting up. - /// - public required int Position { get; init; } - - /// - /// The accumulated achieved score of the respective team in all group matches. - /// - public required int ScoreFor { get; init; } - - /// - /// The accumulated achieved score of the respective team's opponents in all group matches. - /// - public required int ScoreAgainst { get; init; } - - /// - /// The difference between and , calculated as ScoreFor - ScoreAgainst. - /// - public required int ScoreDifference { get; init; } - - /// - /// The number of finished group matches that the respective team has played. - /// - public required int MatchesPlayed { get; init; } - - /// - /// The number of finished group matches where the respective team has won. - /// - public required int MatchesWon { get; init; } - - /// - /// The number of finished group matches of the respective team which have ended in a draw. - /// - public required int MatchesDrawn { get; init; } - - /// - /// The number of finished group matches where the respective team has lost. - /// - public required int MatchesLost { get; init; } - - /// - /// The number of points awarded for the respective team's group match outcomes. - /// - public required int Points { get; init; } -} diff --git a/src/Turnierplan.Adapter/Models/TeamSelector.cs b/src/Turnierplan.Adapter/Models/TeamSelector.cs deleted file mode 100644 index 2aa49d91..00000000 --- a/src/Turnierplan.Adapter/Models/TeamSelector.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Turnierplan.Adapter.Models; - -/// -/// Represents a team selector which can be used to uniquely select a specific team in the context of a . -/// -public sealed record TeamSelector -{ - /// - /// The internal representation of the team selector. - /// - public required string Key { get; init; } - - /// - /// The team selector as a human-readable, localized value. - /// - public required string Localized { get; init; } -} diff --git a/src/Turnierplan.Adapter/Models/Tournament.cs b/src/Turnierplan.Adapter/Models/Tournament.cs deleted file mode 100644 index 8a7f203f..00000000 --- a/src/Turnierplan.Adapter/Models/Tournament.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Turnierplan.Adapter.Enums; - -namespace Turnierplan.Adapter.Models; - -/// -/// Represents a single tournament consisting of s, es and s -/// -public sealed record Tournament -{ - /// - /// The ID of the tournament which is an 11-character sequence of [A-Za-z0-9_-]. - /// - public required string Id { get; init; } - - /// - /// The name of the tournament. - /// - public required string Name { get; init; } - - /// - /// The name of the organization which owns the tournament. - /// - public required string OrganizationName { get; init; } - - /// - /// The name of the folder to which this tournament is assigned or null if not applicable. - /// - public required string? FolderName { get; init; } - - /// - /// The visibility of the tournament. - /// - public required Visibility Visibility { get; init; } - - /// - /// The number of page views i.e. how often this tournament was viewed via its public URL. - /// - public required int PublicPageViews { get; init; } - - /// - /// The teams of this tournament. - /// - public required Team[] Teams { get; init; } - - /// - /// The groups of this tournament. - /// - public required Group[] Groups { get; init; } - - /// - /// The matches of this tournament. - /// - public required Match[] Matches { get; init; } - - /// - /// The rankings of this tournament. - /// - public required Ranking[] Rankings { get; init; } -} diff --git a/src/Turnierplan.Adapter/Models/TournamentHeader.cs b/src/Turnierplan.Adapter/Models/TournamentHeader.cs deleted file mode 100644 index 1435177a..00000000 --- a/src/Turnierplan.Adapter/Models/TournamentHeader.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Turnierplan.Adapter.Enums; - -namespace Turnierplan.Adapter.Models; - -/// -/// Represents a single tournament but does not include any specific data for brevity. -/// -public sealed record TournamentHeader -{ - /// - /// The ID of the tournament which is an 11-character sequence of [A-Za-z0-9_-]. - /// - public required string Id { get; init; } - - /// - /// The name of the tournament. - /// - public required string Name { get; init; } - - /// - /// The name of the organization which owns the tournament. - /// - public required string OrganizationName { get; init; } - - /// - /// The name of the folder to which this tournament is assigned or null if not applicable. - /// - public required string? FolderName { get; init; } - - /// - /// The visibility of the tournament. - /// - public required Visibility Visibility { get; init; } -} diff --git a/src/Turnierplan.Adapter/TurnierplanClient.cs b/src/Turnierplan.Adapter/TurnierplanClient.cs deleted file mode 100644 index 2603dd47..00000000 --- a/src/Turnierplan.Adapter/TurnierplanClient.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Net.Http.Headers; -using Turnierplan.Adapter.Models; - -namespace Turnierplan.Adapter; - -/// -/// Provides an abstraction for accessing the turnierplan.NET public API in the context of a .NET application. -/// -public sealed partial class TurnierplanClient : IDisposable -{ - private const string TurnierplanVersionHeaderName = "x-turnierplan-version"; - - private static readonly string __turnierplanAdapterVersion = - typeof(TurnierplanClient).Assembly.GetName().Version?.ToString() - ?? throw new InvalidOperationException("Could not determine Turnierplan.Adapter version from assembly name."); - - private static readonly JsonSerializerOptions __serializerOptions = new() - { - PropertyNameCaseInsensitive = true, - Converters = { new JsonStringEnumConverter() } - }; - - private readonly HttpClient _httpClient; - private readonly bool _disposeHttpClient; - private readonly bool _disableIdVerification; - private readonly bool _disableVersionVerification; - - /// - /// Initializes a new instance of the class, using an - /// existing and the specified options. Any modifications to the - /// specified instance are not recognized after - /// this constructor has exited. - /// - /// The to use. - /// The options to use. - /// The specified is not disposed by this class. - public TurnierplanClient(HttpClient httpClient, TurnierplanClientOptions options) - { - ArgumentException.ThrowIfNullOrWhiteSpace(options.ApiKey); - ArgumentException.ThrowIfNullOrWhiteSpace(options.ApiKeySecret); - ArgumentException.ThrowIfNullOrWhiteSpace(options.UserAgent); - ArgumentException.ThrowIfNullOrWhiteSpace(options.ApplicationUrl); - - _httpClient = httpClient; - _httpClient.BaseAddress = new Uri(options.ApplicationUrl); - - _httpClient.DefaultRequestHeaders.Add("X-Api-Key", options.ApiKey); - _httpClient.DefaultRequestHeaders.Add("X-Api-Key-Secret", options.ApiKeySecret); - _httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, options.UserAgent); - - _disableIdVerification = options.DisableIdVerification; - _disableVersionVerification = options.DisableVersionVerification; - } - - /// - /// Initializes a new instance of the class and the - /// specified options. This constructor will create a new - /// instance which will be disposed automatically by this class. Any modifications to the - /// specified instance are not recognized after - /// this constructor has exited. - /// - /// The options to use. - public TurnierplanClient(TurnierplanClientOptions options) - : this(new HttpClient(), options) - { - _disposeHttpClient = true; - } - - /// - /// Fetches a single tournament from the API and returns the deserialized . - /// - /// The ID of the tournament to request. - /// An instance of the class which contains the data returned by the API. - /// - /// - /// Thrown if the API returns a non-200 status code or if the response body can not be deserialized. - /// Thrown if is false and the specified is not a valid ID. - /// Thrown if is false and the version of the server does not match the version of the Turnierplan.Adapter library. - /// - /// - /// To be considered valid, the ID must match the following pattern: [A-Za-z0-9_-]{11} - public async Task GetTournament(string tournamentId) - { - VerifyId(tournamentId); - - var request = new HttpRequestMessage(HttpMethod.Get, $"/api/tournaments/{tournamentId}"); - var response = await _httpClient.SendAsync(request); - - VerifyServerVersion(response); - - return await Deserialize(response); - } - - /// - /// Fetches a list of all tournaments in a specific folder from the API and returns the deserialized list with - /// 0..n entries of the type . - /// - /// The ID of the folder to request. - /// A list of instances which contains the data returned by the API. - /// - /// - /// Thrown if the API returns a non-200 status code or if the response body can not be deserialized. - /// Thrown if is false and the specified is not a valid ID. - /// Thrown if is false and the version of the server does not match the version of the Turnierplan.Adapter library. - /// - /// - /// To be considered valid, the ID must match the following pattern: [A-Za-z0-9_-]{11} - public async Task> GetTournaments(string folderId) - { - VerifyId(folderId); - - var query = new QueryBuilder { { "folderId", folderId } }; - var request = new HttpRequestMessage(HttpMethod.Get, $"/api/tournaments{query}"); - var response = await _httpClient.SendAsync(request); - - VerifyServerVersion(response); - - return await Deserialize>(response); - } - - /// - /// If this instance was created using a pre-existing instance, this method does nothing. - /// If this instance was created without specifying a pre-existing instance, this method - /// will dispose the internally created . - /// - public void Dispose() - { - if (_disposeHttpClient) - { - _httpClient.Dispose(); - } - } - - private void VerifyId(string id) - { - if (_disableIdVerification) - { - return; - } - - if (!ValidIdRegex().IsMatch(id)) - { - throw new TurnierplanClientException("The specified ID does not match the expected format. Specify a correct ID or disable this check using 'DisableIdVerification'."); - } - } - - private void VerifyServerVersion(HttpResponseMessage response) - { - if (_disableVersionVerification) - { - return; - } - - if (!response.Headers.TryGetValues(TurnierplanVersionHeaderName, out var headerValue)) - { - throw new TurnierplanClientException($"Could not get '{TurnierplanVersionHeaderName}' header from response. Fix the issue or disable the version check using 'DisableServerVersionVerification'."); - } - - var serverVersion = headerValue.FirstOrDefault(); - - if (string.IsNullOrWhiteSpace(serverVersion)) - { - throw new TurnierplanClientException($"Server returned an empty '{TurnierplanVersionHeaderName}' header. Fix the issue or disable the version check using 'DisableServerVersionVerification'."); - } - - if (!serverVersion.Equals(__turnierplanAdapterVersion)) - { - throw new TurnierplanClientException($"Server version '{serverVersion}' does not match the Turnierplan.Adapter version '{__turnierplanAdapterVersion}'. Fix the issue or disable the version check using 'DisableServerVersionVerification'."); - } - } - - private static async Task Deserialize(HttpResponseMessage response) - { - if (response.StatusCode != HttpStatusCode.OK) - { - throw new TurnierplanClientException($"API returned unexpected status code: {response.StatusCode}"); - } - - var data = await response.Content.ReadFromJsonAsync(__serializerOptions); - - if (data is null) - { - throw new TurnierplanClientException($"Failed to deserialize API response of type '{typeof(T).Name}'."); - } - - return data; - } - - [GeneratedRegex("^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-]{11}$")] - private static partial Regex ValidIdRegex(); -} diff --git a/src/Turnierplan.Adapter/TurnierplanClientException.cs b/src/Turnierplan.Adapter/TurnierplanClientException.cs deleted file mode 100644 index b8ff0cbc..00000000 --- a/src/Turnierplan.Adapter/TurnierplanClientException.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Turnierplan.Adapter; - -/// -/// Represents an error that occurred in the API abstraction layer. -/// -public sealed class TurnierplanClientException : Exception -{ - internal TurnierplanClientException(string? message) - : base(message) - { - } -} diff --git a/src/Turnierplan.Adapter/TurnierplanClientOptions.cs b/src/Turnierplan.Adapter/TurnierplanClientOptions.cs deleted file mode 100644 index 2908e50b..00000000 --- a/src/Turnierplan.Adapter/TurnierplanClientOptions.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Turnierplan.Adapter; - -/// -/// Represents the options necessary for constructing an instance of . -/// -public sealed record TurnierplanClientOptions -{ - /// - /// Initializes a new instance of the class with the two required parameters representing the API key to use. - /// - /// The base URL of the turnierplan.NET instance that should be queried. - /// The API key ID as displayed in the turnierplan.NET Portal. - /// The API key secret as displayed in the turnierplan.NET Portal. - public TurnierplanClientOptions(string applicationUrl, string apiKey, string apiKeySecret) - { - ApplicationUrl = applicationUrl; - ApiKey = apiKey; - ApiKeySecret = apiKeySecret; - } - - /// - /// The base URL of the turnierplan.NET instance that should be queried. - /// - public string ApplicationUrl { get; } - - /// - /// The API key ID as displayed in the turnierplan.NET portal. - /// - public string ApiKey { get; } - - /// - /// The API key secret as displayed in the turnierplan.NET portal. - /// - public string ApiKeySecret { get; } - - /// - /// The value that should be included in the "User-Agent" HTTP header. The default is - /// to Turnierplan.Adapter and this property may not be empty or white-space. - /// - public string UserAgent { get; init; } = "Turnierplan.Adapter"; - - /// - /// If set to true, the does not verify the ID - /// strings before sending the HTTP requests. - /// - public bool DisableIdVerification { get; init; } = false; - - /// - /// If set to true, the does not verify the server version against - /// the used version of Turnierplan.Adapter. - /// - public bool DisableVersionVerification { get; init; } = false; -} From 876468a64d28f6047741928fc784858cb8193d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 20 Sep 2025 10:02:16 +0200 Subject: [PATCH 2/9] Revert "delete existing files" This reverts commit f184224cf9e85b4835da3146d5871d47a9b33c39. --- .../Enums/MatchOutcomeType.cs | 27 +++ src/Turnierplan.Adapter/Enums/MatchState.cs | 22 ++ src/Turnierplan.Adapter/Enums/MatchType.cs | 32 +++ src/Turnierplan.Adapter/Enums/Visibility.cs | 19 ++ src/Turnierplan.Adapter/Models/Group.cs | 34 +++ .../Models/GroupParticipant.cs | 23 ++ src/Turnierplan.Adapter/Models/Match.cs | 82 ++++++++ .../Models/MatchTeamInfo.cs | 23 ++ src/Turnierplan.Adapter/Models/Ranking.cs | 27 +++ src/Turnierplan.Adapter/Models/Team.cs | 23 ++ .../Models/TeamGroupStatistics.cs | 53 +++++ .../Models/TeamSelector.cs | 17 ++ src/Turnierplan.Adapter/Models/Tournament.cs | 59 ++++++ .../Models/TournamentHeader.cs | 34 +++ src/Turnierplan.Adapter/TurnierplanClient.cs | 197 ++++++++++++++++++ .../TurnierplanClientException.cs | 12 ++ .../TurnierplanClientOptions.cs | 53 +++++ 17 files changed, 737 insertions(+) create mode 100644 src/Turnierplan.Adapter/Enums/MatchOutcomeType.cs create mode 100644 src/Turnierplan.Adapter/Enums/MatchState.cs create mode 100644 src/Turnierplan.Adapter/Enums/MatchType.cs create mode 100644 src/Turnierplan.Adapter/Enums/Visibility.cs create mode 100644 src/Turnierplan.Adapter/Models/Group.cs create mode 100644 src/Turnierplan.Adapter/Models/GroupParticipant.cs create mode 100644 src/Turnierplan.Adapter/Models/Match.cs create mode 100644 src/Turnierplan.Adapter/Models/MatchTeamInfo.cs create mode 100644 src/Turnierplan.Adapter/Models/Ranking.cs create mode 100644 src/Turnierplan.Adapter/Models/Team.cs create mode 100644 src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs create mode 100644 src/Turnierplan.Adapter/Models/TeamSelector.cs create mode 100644 src/Turnierplan.Adapter/Models/Tournament.cs create mode 100644 src/Turnierplan.Adapter/Models/TournamentHeader.cs create mode 100644 src/Turnierplan.Adapter/TurnierplanClient.cs create mode 100644 src/Turnierplan.Adapter/TurnierplanClientException.cs create mode 100644 src/Turnierplan.Adapter/TurnierplanClientOptions.cs diff --git a/src/Turnierplan.Adapter/Enums/MatchOutcomeType.cs b/src/Turnierplan.Adapter/Enums/MatchOutcomeType.cs new file mode 100644 index 00000000..8b37ec90 --- /dev/null +++ b/src/Turnierplan.Adapter/Enums/MatchOutcomeType.cs @@ -0,0 +1,27 @@ +namespace Turnierplan.Adapter.Enums; + +/// +/// The match outcome type gives an indication of how a match ended. +/// +public enum MatchOutcomeType +{ + /// + /// Indicates that the match ended after regular time without penalty shootout. + /// + Standard, + + /// + /// Indicates that the match ended after over time but without penalty shootout. + /// + AfterOvertime, + + /// + /// Indicates that the match ended after penalty shootout. + /// + AfterPenalties, + + /// + /// Indicates that the match was not completed normally. Rather, the score is the result of some extraordinary circumstance. + /// + SpecialScoring +} diff --git a/src/Turnierplan.Adapter/Enums/MatchState.cs b/src/Turnierplan.Adapter/Enums/MatchState.cs new file mode 100644 index 00000000..edddabaf --- /dev/null +++ b/src/Turnierplan.Adapter/Enums/MatchState.cs @@ -0,0 +1,22 @@ +namespace Turnierplan.Adapter.Enums; + +/// +/// Represents the state of a match. +/// +public enum MatchState +{ + /// + /// The match has not started yet. + /// + NotStarted, + + /// + /// The match is currently being played, i.e. a "LIVE" outcome was published. + /// + CurrentlyPlaying, + + /// + /// The match is finished, i.e. a "final" result was published. + /// + Finished +} diff --git a/src/Turnierplan.Adapter/Enums/MatchType.cs b/src/Turnierplan.Adapter/Enums/MatchType.cs new file mode 100644 index 00000000..4dbeb36b --- /dev/null +++ b/src/Turnierplan.Adapter/Enums/MatchType.cs @@ -0,0 +1,32 @@ +namespace Turnierplan.Adapter.Enums; + +/// +/// Represents the type of match. +/// +public enum MatchType +{ + /// + /// A group match between two group participants. + /// + GroupMatch, + + /// + /// A knockout (KO) match which is specifically not the final match, such as semi-final or quarter-final. + /// + NonFinalKnockout, + + /// + /// A playoff match which is specifically not the 3rd position playoff. + /// + AdditionalPlayoff, + + /// + /// The playoff match for 3rd place. + /// + ThirdPlacePlayoff, + + /// + /// The final match or in other words the playoff match for 1st place. + /// + Final +} diff --git a/src/Turnierplan.Adapter/Enums/Visibility.cs b/src/Turnierplan.Adapter/Enums/Visibility.cs new file mode 100644 index 00000000..eae3d0e1 --- /dev/null +++ b/src/Turnierplan.Adapter/Enums/Visibility.cs @@ -0,0 +1,19 @@ +using Turnierplan.Adapter.Models; + +namespace Turnierplan.Adapter.Enums; + +/// +/// Represents the visibility of a . +/// +public enum Visibility +{ + /// + /// The tournament is only accessible by an authenticated user or API key. + /// + Private = 1, + + /// + /// The tournament can be viewed by who has the correct link with the tournament ID. + /// + Public = 2 +} diff --git a/src/Turnierplan.Adapter/Models/Group.cs b/src/Turnierplan.Adapter/Models/Group.cs new file mode 100644 index 00000000..cd1a7812 --- /dev/null +++ b/src/Turnierplan.Adapter/Models/Group.cs @@ -0,0 +1,34 @@ +namespace Turnierplan.Adapter.Models; + +/// +/// Represents a single group in the context of a . +/// +public sealed record Group +{ + /// + /// The numerical ID of this group which is unique in the scope of a single tournament. + /// + public required int Id { get; init; } + + /// + /// The alphabetical ID of this group. This is always an upper-case character between A and Z. + /// + public required char AlphabeticalId { get; init; } + + /// + /// The display name of this group which is either a custom display name configured for this group + /// or the default group name containing the . + /// + /// + public required string DisplayName { get; init; } + + /// + /// Whether this group has a custom display name. + /// + public required bool HasCustomDisplayName { get; init; } + + /// + /// An array of all participants assigned to this group. + /// + public required GroupParticipant[] Participants { get; init; } +} diff --git a/src/Turnierplan.Adapter/Models/GroupParticipant.cs b/src/Turnierplan.Adapter/Models/GroupParticipant.cs new file mode 100644 index 00000000..08e18427 --- /dev/null +++ b/src/Turnierplan.Adapter/Models/GroupParticipant.cs @@ -0,0 +1,23 @@ +namespace Turnierplan.Adapter.Models; + +/// +/// Represents the relationship between a team which is assigned to a group. +/// +public sealed record GroupParticipant +{ + /// + /// The numerical ID of the team. + /// + public required int TeamId { get; init; } + + /// + /// The assigned priority of this team which is used when other means of comparing + /// team results inside a group result in a draw. + /// + public required int Priority { get; init; } + + /// + /// The computed statistics resulting from this team's group matches. + /// + public required TeamGroupStatistics Statistics { get; init; } +} diff --git a/src/Turnierplan.Adapter/Models/Match.cs b/src/Turnierplan.Adapter/Models/Match.cs new file mode 100644 index 00000000..66ded3da --- /dev/null +++ b/src/Turnierplan.Adapter/Models/Match.cs @@ -0,0 +1,82 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using Turnierplan.Adapter.Enums; +using MatchType = Turnierplan.Adapter.Enums.MatchType; + +namespace Turnierplan.Adapter.Models; + +/// +/// Represents a single match in the context of a . A match has two participants +/// which are computed by evaluating the team selectors. Once the match is finished, each participant is +/// assigned a score. +/// +public sealed record Match +{ + /// + /// The numerical ID of this match which is unique in the scope of a single tournament. + /// + public required int Id { get; init; } + + /// + /// Ths index of this match. Upon match plan generation, all matches are assigned sequential indices + /// starting at 1. + /// + public required int Index { get; init; } + + /// + /// The index of the court where this match is played, starting at 0. + /// + public required short Court { get; init; } + + /// + /// The kickoff time of this match, or null if no kickoff time was defined. + /// + public DateTime? Kickoff { get; init; } + + /// + /// The type of this match. + /// + public required MatchType Type { get; init; } + + /// + /// The type of this match as a human-readable, localized value. + /// + /// Group Match or Semi-Final + public required string FormattedType { get; init; } + + /// + /// The of the if this match's outcome counts towards + /// the group statistics or null if this match is a deciding/ranking match. + /// + /// + public int? GroupId { get; init; } + + /// + /// true if the outcome of this match contributes to some group's statistics or false + /// if this match is a deciding/ranking match. + /// + /// + [JsonIgnore] + [MemberNotNullWhen(true, nameof(GroupId))] + public bool IsGroupMatch => GroupId is not null; + + /// + /// The "home" team of this match. + /// + public required MatchTeamInfo TeamA { get; init; } + + /// + /// The "aways" team of this match. + /// + public required MatchTeamInfo TeamB { get; init; } + + /// + /// The state of this match, indicating whether the team's scores and are set. + /// + public required MatchState State { get; init; } + + /// + /// The of this match or null if this match is not finished yet. + /// + public MatchOutcomeType? OutcomeType { get; init; } +} diff --git a/src/Turnierplan.Adapter/Models/MatchTeamInfo.cs b/src/Turnierplan.Adapter/Models/MatchTeamInfo.cs new file mode 100644 index 00000000..ce115971 --- /dev/null +++ b/src/Turnierplan.Adapter/Models/MatchTeamInfo.cs @@ -0,0 +1,23 @@ +namespace Turnierplan.Adapter.Models; + +/// +/// Represents the "home" or "away" team of a match. Depending on the state of the tournament, the specific +/// can be available or not. However, the is always available. +/// +public sealed record MatchTeamInfo +{ + /// + /// The team selector for this team. + /// + public required TeamSelector TeamSelector { get; init; } + + /// + /// The of the or null if the team can currently not be evaluated. + /// + public int? TeamId { get; init; } + + /// + /// The score of team participating team or null if this match is not finished yet. + /// + public int? Score { get; init; } +} diff --git a/src/Turnierplan.Adapter/Models/Ranking.cs b/src/Turnierplan.Adapter/Models/Ranking.cs new file mode 100644 index 00000000..3040e1f9 --- /dev/null +++ b/src/Turnierplan.Adapter/Models/Ranking.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Turnierplan.Adapter.Models; + +/// +/// Represents a single ranking in the context of a . There are +/// equally many rankings in a tournament as there are teams. +/// +public sealed record Ranking +{ + /// + /// The placement rank, starting at 1 for the first place and counting up. + /// + public required int PlacementRank { get; init; } + + /// + /// Whether this ranking is currently defined or not. + /// + [MemberNotNullWhen(true, nameof(TeamId))] + public required bool IsDefined { get; init; } + + /// + /// The of the which occupies this ranking or null if + /// this ranking is currently not defined. + /// + public int? TeamId { get; init; } +} diff --git a/src/Turnierplan.Adapter/Models/Team.cs b/src/Turnierplan.Adapter/Models/Team.cs new file mode 100644 index 00000000..c9fdfc7e --- /dev/null +++ b/src/Turnierplan.Adapter/Models/Team.cs @@ -0,0 +1,23 @@ +namespace Turnierplan.Adapter.Models; + +/// +/// Represents a single team in the context of a . +/// +public sealed record Team +{ + /// + /// The numerical ID of this team which is unique in the scope of a single tournament. + /// + public required int Id { get; init; } + + /// + /// The name of this team. + /// + public required string Name { get; init; } + + /// + /// Whether this team plays "out of competition". If it is true, this team will always + /// be regarded worse compared to any other team when sorting teams in a group, for example. + /// + public required bool OutOfCompetition { get; init; } +} diff --git a/src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs b/src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs new file mode 100644 index 00000000..ac9b9459 --- /dev/null +++ b/src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs @@ -0,0 +1,53 @@ +namespace Turnierplan.Adapter.Models; + +/// +/// Value object which holds a single s statistics in the context +/// of the to which this team is assigned to. +/// +public sealed record TeamGroupStatistics +{ + /// + /// The position of the team in the group, starting at 1 for the best team and counting up. + /// + public required int Position { get; init; } + + /// + /// The accumulated achieved score of the respective team in all group matches. + /// + public required int ScoreFor { get; init; } + + /// + /// The accumulated achieved score of the respective team's opponents in all group matches. + /// + public required int ScoreAgainst { get; init; } + + /// + /// The difference between and , calculated as ScoreFor - ScoreAgainst. + /// + public required int ScoreDifference { get; init; } + + /// + /// The number of finished group matches that the respective team has played. + /// + public required int MatchesPlayed { get; init; } + + /// + /// The number of finished group matches where the respective team has won. + /// + public required int MatchesWon { get; init; } + + /// + /// The number of finished group matches of the respective team which have ended in a draw. + /// + public required int MatchesDrawn { get; init; } + + /// + /// The number of finished group matches where the respective team has lost. + /// + public required int MatchesLost { get; init; } + + /// + /// The number of points awarded for the respective team's group match outcomes. + /// + public required int Points { get; init; } +} diff --git a/src/Turnierplan.Adapter/Models/TeamSelector.cs b/src/Turnierplan.Adapter/Models/TeamSelector.cs new file mode 100644 index 00000000..2aa49d91 --- /dev/null +++ b/src/Turnierplan.Adapter/Models/TeamSelector.cs @@ -0,0 +1,17 @@ +namespace Turnierplan.Adapter.Models; + +/// +/// Represents a team selector which can be used to uniquely select a specific team in the context of a . +/// +public sealed record TeamSelector +{ + /// + /// The internal representation of the team selector. + /// + public required string Key { get; init; } + + /// + /// The team selector as a human-readable, localized value. + /// + public required string Localized { get; init; } +} diff --git a/src/Turnierplan.Adapter/Models/Tournament.cs b/src/Turnierplan.Adapter/Models/Tournament.cs new file mode 100644 index 00000000..8a7f203f --- /dev/null +++ b/src/Turnierplan.Adapter/Models/Tournament.cs @@ -0,0 +1,59 @@ +using Turnierplan.Adapter.Enums; + +namespace Turnierplan.Adapter.Models; + +/// +/// Represents a single tournament consisting of s, es and s +/// +public sealed record Tournament +{ + /// + /// The ID of the tournament which is an 11-character sequence of [A-Za-z0-9_-]. + /// + public required string Id { get; init; } + + /// + /// The name of the tournament. + /// + public required string Name { get; init; } + + /// + /// The name of the organization which owns the tournament. + /// + public required string OrganizationName { get; init; } + + /// + /// The name of the folder to which this tournament is assigned or null if not applicable. + /// + public required string? FolderName { get; init; } + + /// + /// The visibility of the tournament. + /// + public required Visibility Visibility { get; init; } + + /// + /// The number of page views i.e. how often this tournament was viewed via its public URL. + /// + public required int PublicPageViews { get; init; } + + /// + /// The teams of this tournament. + /// + public required Team[] Teams { get; init; } + + /// + /// The groups of this tournament. + /// + public required Group[] Groups { get; init; } + + /// + /// The matches of this tournament. + /// + public required Match[] Matches { get; init; } + + /// + /// The rankings of this tournament. + /// + public required Ranking[] Rankings { get; init; } +} diff --git a/src/Turnierplan.Adapter/Models/TournamentHeader.cs b/src/Turnierplan.Adapter/Models/TournamentHeader.cs new file mode 100644 index 00000000..1435177a --- /dev/null +++ b/src/Turnierplan.Adapter/Models/TournamentHeader.cs @@ -0,0 +1,34 @@ +using Turnierplan.Adapter.Enums; + +namespace Turnierplan.Adapter.Models; + +/// +/// Represents a single tournament but does not include any specific data for brevity. +/// +public sealed record TournamentHeader +{ + /// + /// The ID of the tournament which is an 11-character sequence of [A-Za-z0-9_-]. + /// + public required string Id { get; init; } + + /// + /// The name of the tournament. + /// + public required string Name { get; init; } + + /// + /// The name of the organization which owns the tournament. + /// + public required string OrganizationName { get; init; } + + /// + /// The name of the folder to which this tournament is assigned or null if not applicable. + /// + public required string? FolderName { get; init; } + + /// + /// The visibility of the tournament. + /// + public required Visibility Visibility { get; init; } +} diff --git a/src/Turnierplan.Adapter/TurnierplanClient.cs b/src/Turnierplan.Adapter/TurnierplanClient.cs new file mode 100644 index 00000000..2603dd47 --- /dev/null +++ b/src/Turnierplan.Adapter/TurnierplanClient.cs @@ -0,0 +1,197 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Net.Http.Headers; +using Turnierplan.Adapter.Models; + +namespace Turnierplan.Adapter; + +/// +/// Provides an abstraction for accessing the turnierplan.NET public API in the context of a .NET application. +/// +public sealed partial class TurnierplanClient : IDisposable +{ + private const string TurnierplanVersionHeaderName = "x-turnierplan-version"; + + private static readonly string __turnierplanAdapterVersion = + typeof(TurnierplanClient).Assembly.GetName().Version?.ToString() + ?? throw new InvalidOperationException("Could not determine Turnierplan.Adapter version from assembly name."); + + private static readonly JsonSerializerOptions __serializerOptions = new() + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() } + }; + + private readonly HttpClient _httpClient; + private readonly bool _disposeHttpClient; + private readonly bool _disableIdVerification; + private readonly bool _disableVersionVerification; + + /// + /// Initializes a new instance of the class, using an + /// existing and the specified options. Any modifications to the + /// specified instance are not recognized after + /// this constructor has exited. + /// + /// The to use. + /// The options to use. + /// The specified is not disposed by this class. + public TurnierplanClient(HttpClient httpClient, TurnierplanClientOptions options) + { + ArgumentException.ThrowIfNullOrWhiteSpace(options.ApiKey); + ArgumentException.ThrowIfNullOrWhiteSpace(options.ApiKeySecret); + ArgumentException.ThrowIfNullOrWhiteSpace(options.UserAgent); + ArgumentException.ThrowIfNullOrWhiteSpace(options.ApplicationUrl); + + _httpClient = httpClient; + _httpClient.BaseAddress = new Uri(options.ApplicationUrl); + + _httpClient.DefaultRequestHeaders.Add("X-Api-Key", options.ApiKey); + _httpClient.DefaultRequestHeaders.Add("X-Api-Key-Secret", options.ApiKeySecret); + _httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, options.UserAgent); + + _disableIdVerification = options.DisableIdVerification; + _disableVersionVerification = options.DisableVersionVerification; + } + + /// + /// Initializes a new instance of the class and the + /// specified options. This constructor will create a new + /// instance which will be disposed automatically by this class. Any modifications to the + /// specified instance are not recognized after + /// this constructor has exited. + /// + /// The options to use. + public TurnierplanClient(TurnierplanClientOptions options) + : this(new HttpClient(), options) + { + _disposeHttpClient = true; + } + + /// + /// Fetches a single tournament from the API and returns the deserialized . + /// + /// The ID of the tournament to request. + /// An instance of the class which contains the data returned by the API. + /// + /// + /// Thrown if the API returns a non-200 status code or if the response body can not be deserialized. + /// Thrown if is false and the specified is not a valid ID. + /// Thrown if is false and the version of the server does not match the version of the Turnierplan.Adapter library. + /// + /// + /// To be considered valid, the ID must match the following pattern: [A-Za-z0-9_-]{11} + public async Task GetTournament(string tournamentId) + { + VerifyId(tournamentId); + + var request = new HttpRequestMessage(HttpMethod.Get, $"/api/tournaments/{tournamentId}"); + var response = await _httpClient.SendAsync(request); + + VerifyServerVersion(response); + + return await Deserialize(response); + } + + /// + /// Fetches a list of all tournaments in a specific folder from the API and returns the deserialized list with + /// 0..n entries of the type . + /// + /// The ID of the folder to request. + /// A list of instances which contains the data returned by the API. + /// + /// + /// Thrown if the API returns a non-200 status code or if the response body can not be deserialized. + /// Thrown if is false and the specified is not a valid ID. + /// Thrown if is false and the version of the server does not match the version of the Turnierplan.Adapter library. + /// + /// + /// To be considered valid, the ID must match the following pattern: [A-Za-z0-9_-]{11} + public async Task> GetTournaments(string folderId) + { + VerifyId(folderId); + + var query = new QueryBuilder { { "folderId", folderId } }; + var request = new HttpRequestMessage(HttpMethod.Get, $"/api/tournaments{query}"); + var response = await _httpClient.SendAsync(request); + + VerifyServerVersion(response); + + return await Deserialize>(response); + } + + /// + /// If this instance was created using a pre-existing instance, this method does nothing. + /// If this instance was created without specifying a pre-existing instance, this method + /// will dispose the internally created . + /// + public void Dispose() + { + if (_disposeHttpClient) + { + _httpClient.Dispose(); + } + } + + private void VerifyId(string id) + { + if (_disableIdVerification) + { + return; + } + + if (!ValidIdRegex().IsMatch(id)) + { + throw new TurnierplanClientException("The specified ID does not match the expected format. Specify a correct ID or disable this check using 'DisableIdVerification'."); + } + } + + private void VerifyServerVersion(HttpResponseMessage response) + { + if (_disableVersionVerification) + { + return; + } + + if (!response.Headers.TryGetValues(TurnierplanVersionHeaderName, out var headerValue)) + { + throw new TurnierplanClientException($"Could not get '{TurnierplanVersionHeaderName}' header from response. Fix the issue or disable the version check using 'DisableServerVersionVerification'."); + } + + var serverVersion = headerValue.FirstOrDefault(); + + if (string.IsNullOrWhiteSpace(serverVersion)) + { + throw new TurnierplanClientException($"Server returned an empty '{TurnierplanVersionHeaderName}' header. Fix the issue or disable the version check using 'DisableServerVersionVerification'."); + } + + if (!serverVersion.Equals(__turnierplanAdapterVersion)) + { + throw new TurnierplanClientException($"Server version '{serverVersion}' does not match the Turnierplan.Adapter version '{__turnierplanAdapterVersion}'. Fix the issue or disable the version check using 'DisableServerVersionVerification'."); + } + } + + private static async Task Deserialize(HttpResponseMessage response) + { + if (response.StatusCode != HttpStatusCode.OK) + { + throw new TurnierplanClientException($"API returned unexpected status code: {response.StatusCode}"); + } + + var data = await response.Content.ReadFromJsonAsync(__serializerOptions); + + if (data is null) + { + throw new TurnierplanClientException($"Failed to deserialize API response of type '{typeof(T).Name}'."); + } + + return data; + } + + [GeneratedRegex("^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-]{11}$")] + private static partial Regex ValidIdRegex(); +} diff --git a/src/Turnierplan.Adapter/TurnierplanClientException.cs b/src/Turnierplan.Adapter/TurnierplanClientException.cs new file mode 100644 index 00000000..b8ff0cbc --- /dev/null +++ b/src/Turnierplan.Adapter/TurnierplanClientException.cs @@ -0,0 +1,12 @@ +namespace Turnierplan.Adapter; + +/// +/// Represents an error that occurred in the API abstraction layer. +/// +public sealed class TurnierplanClientException : Exception +{ + internal TurnierplanClientException(string? message) + : base(message) + { + } +} diff --git a/src/Turnierplan.Adapter/TurnierplanClientOptions.cs b/src/Turnierplan.Adapter/TurnierplanClientOptions.cs new file mode 100644 index 00000000..2908e50b --- /dev/null +++ b/src/Turnierplan.Adapter/TurnierplanClientOptions.cs @@ -0,0 +1,53 @@ +namespace Turnierplan.Adapter; + +/// +/// Represents the options necessary for constructing an instance of . +/// +public sealed record TurnierplanClientOptions +{ + /// + /// Initializes a new instance of the class with the two required parameters representing the API key to use. + /// + /// The base URL of the turnierplan.NET instance that should be queried. + /// The API key ID as displayed in the turnierplan.NET Portal. + /// The API key secret as displayed in the turnierplan.NET Portal. + public TurnierplanClientOptions(string applicationUrl, string apiKey, string apiKeySecret) + { + ApplicationUrl = applicationUrl; + ApiKey = apiKey; + ApiKeySecret = apiKeySecret; + } + + /// + /// The base URL of the turnierplan.NET instance that should be queried. + /// + public string ApplicationUrl { get; } + + /// + /// The API key ID as displayed in the turnierplan.NET portal. + /// + public string ApiKey { get; } + + /// + /// The API key secret as displayed in the turnierplan.NET portal. + /// + public string ApiKeySecret { get; } + + /// + /// The value that should be included in the "User-Agent" HTTP header. The default is + /// to Turnierplan.Adapter and this property may not be empty or white-space. + /// + public string UserAgent { get; init; } = "Turnierplan.Adapter"; + + /// + /// If set to true, the does not verify the ID + /// strings before sending the HTTP requests. + /// + public bool DisableIdVerification { get; init; } = false; + + /// + /// If set to true, the does not verify the server version against + /// the used version of Turnierplan.Adapter. + /// + public bool DisableVersionVerification { get; init; } = false; +} From 3197b7352818605fec65b37ddc200ee50e3c53a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 20 Sep 2025 10:24:01 +0200 Subject: [PATCH 3/9] overhauled client library --- README.md | 24 +---- src/Turnierplan.Adapter/README.md | 27 ++++++ .../Turnierplan.Adapter.csproj | 2 +- src/Turnierplan.Adapter/TurnierplanClient.cs | 87 +++++-------------- .../TurnierplanClientException.cs | 2 +- .../TurnierplanClientOptions.cs | 30 ++----- 6 files changed, 61 insertions(+), 111 deletions(-) create mode 100644 src/Turnierplan.Adapter/README.md diff --git a/README.md b/README.md index de96ba4f..ca979e5c 100644 --- a/README.md +++ b/README.md @@ -191,26 +191,6 @@ Below are some screenshots of the application: ## Turnierplan.Adapter -If you want to use the **turnierplan.NET** API programatically in a .NET environment, you can use the `Turnierplan.Adapter` [package](https://www.nuget.org/packages/Turnierplan.Adapter) which contains all model classes and an abstraction layer to easily query the API endpoints. +If you want to use the **turnierplan.NET** API programatically in a .NET environment, you can use the `Turnierplan.Adapter` [NuGet package](https://www.nuget.org/packages/Turnierplan.Adapter) which contains all model classes and an abstraction layer to easily query the API endpoints. -Add the package reference to your project: - -```csproj - - - -``` - -In your program, instantiate the `TurnierplanClient` class providing your API key. Finally, you can query the API! - -```cs -var config = new TurnierplanClientOptions("http://localhost:45000", "", "") -{ - UserAgent = "" -}; - -using var client = new TurnierplanClient(config); - -var x = await client.GetTournaments(""); -var y = await client.GetTournament(""); -``` +Please refer to the [package readme](src/Turnierplan.Adapter/README.md) for details and usage examples. diff --git a/src/Turnierplan.Adapter/README.md b/src/Turnierplan.Adapter/README.md new file mode 100644 index 00000000..fabc41d7 --- /dev/null +++ b/src/Turnierplan.Adapter/README.md @@ -0,0 +1,27 @@ +# turnierplan.NET - client library + +*.NET client library for the [turnierplan.NET](https://github.com/turnierplan-NET/turnierplan.NET) API* + +## Introduction + +This NuGet package can be used to query the API endpoints provided by the [turnierplan.NET](https://github.com/turnierplan-NET/turnierplan.NET) application. + +## Usage + +Instantiate the `TurnierplanClientOptions` and provide the base url as well as the credentials: + +```cs +var config = new TurnierplanClientOptions("http://localhost:45000", "", ""); + +using var client = new TurnierplanClient(config); + +var x = await client.GetTournaments(""); +var y = await client.GetTournament(""); +``` + +## Supported Endpoints + +Currently, the `Turnierplan.Adapter` packages supports the following endpoints: + +- `GET /api/tournaments/{id}` - get a single tournament including all details +- `GET /api/tournaments?folderId={id}` - gets all tournaments in a folder diff --git a/src/Turnierplan.Adapter/Turnierplan.Adapter.csproj b/src/Turnierplan.Adapter/Turnierplan.Adapter.csproj index 79170c1e..21072cc2 100644 --- a/src/Turnierplan.Adapter/Turnierplan.Adapter.csproj +++ b/src/Turnierplan.Adapter/Turnierplan.Adapter.csproj @@ -22,6 +22,6 @@ - + diff --git a/src/Turnierplan.Adapter/TurnierplanClient.cs b/src/Turnierplan.Adapter/TurnierplanClient.cs index 2603dd47..9091fd7b 100644 --- a/src/Turnierplan.Adapter/TurnierplanClient.cs +++ b/src/Turnierplan.Adapter/TurnierplanClient.cs @@ -2,18 +2,18 @@ using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; -using System.Text.RegularExpressions; using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Net.Http.Headers; using Turnierplan.Adapter.Models; namespace Turnierplan.Adapter; /// -/// Provides an abstraction for accessing the turnierplan.NET public API in the context of a .NET application. +/// Provides an abstraction for accessing the turnierplan.NET API in the context of a .NET application. /// -public sealed partial class TurnierplanClient : IDisposable +public sealed class TurnierplanClient : IDisposable { + private const string ApiKeyIdHeaderName = "x-api-key"; + private const string ApiKeySecretHeaderName = "x-api-key-secret"; private const string TurnierplanVersionHeaderName = "x-turnierplan-version"; private static readonly string __turnierplanAdapterVersion = @@ -28,42 +28,32 @@ public sealed partial class TurnierplanClient : IDisposable private readonly HttpClient _httpClient; private readonly bool _disposeHttpClient; - private readonly bool _disableIdVerification; - private readonly bool _disableVersionVerification; /// - /// Initializes a new instance of the class, using an - /// existing and the specified options. Any modifications to the - /// specified instance are not recognized after - /// this constructor has exited. + /// Initializes a new instance of the class, using an existing + /// and the specified options. /// /// The to use. /// The options to use. - /// The specified is not disposed by this class. + /// + /// The specified is not disposed when this is disposed. + /// public TurnierplanClient(HttpClient httpClient, TurnierplanClientOptions options) { ArgumentException.ThrowIfNullOrWhiteSpace(options.ApiKey); ArgumentException.ThrowIfNullOrWhiteSpace(options.ApiKeySecret); - ArgumentException.ThrowIfNullOrWhiteSpace(options.UserAgent); ArgumentException.ThrowIfNullOrWhiteSpace(options.ApplicationUrl); _httpClient = httpClient; _httpClient.BaseAddress = new Uri(options.ApplicationUrl); - _httpClient.DefaultRequestHeaders.Add("X-Api-Key", options.ApiKey); - _httpClient.DefaultRequestHeaders.Add("X-Api-Key-Secret", options.ApiKeySecret); - _httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, options.UserAgent); - - _disableIdVerification = options.DisableIdVerification; - _disableVersionVerification = options.DisableVersionVerification; + _httpClient.DefaultRequestHeaders.Add(ApiKeyIdHeaderName, options.ApiKey); + _httpClient.DefaultRequestHeaders.Add(ApiKeySecretHeaderName, options.ApiKeySecret); } /// - /// Initializes a new instance of the class and the - /// specified options. This constructor will create a new - /// instance which will be disposed automatically by this class. Any modifications to the - /// specified instance are not recognized after - /// this constructor has exited. + /// Initializes a new instance of the class using the specified options. This + /// constructor will create a new instance which will be disposed automatically by this class. /// /// The options to use. public TurnierplanClient(TurnierplanClientOptions options) @@ -80,21 +70,17 @@ public TurnierplanClient(TurnierplanClientOptions options) /// /// /// Thrown if the API returns a non-200 status code or if the response body can not be deserialized. - /// Thrown if is false and the specified is not a valid ID. - /// Thrown if is false and the version of the server does not match the version of the Turnierplan.Adapter library. + /// Thrown if the version of the server does not match the version of the Turnierplan.Adapter library. /// /// - /// To be considered valid, the ID must match the following pattern: [A-Za-z0-9_-]{11} public async Task GetTournament(string tournamentId) { - VerifyId(tournamentId); - var request = new HttpRequestMessage(HttpMethod.Get, $"/api/tournaments/{tournamentId}"); - var response = await _httpClient.SendAsync(request); + var response = await _httpClient.SendAsync(request).ConfigureAwait(false); VerifyServerVersion(response); - return await Deserialize(response); + return await Deserialize(response).ConfigureAwait(false); } /// @@ -106,22 +92,18 @@ public async Task GetTournament(string tournamentId) /// /// /// Thrown if the API returns a non-200 status code or if the response body can not be deserialized. - /// Thrown if is false and the specified is not a valid ID. - /// Thrown if is false and the version of the server does not match the version of the Turnierplan.Adapter library. + /// Thrown if the version of the server does not match the version of the Turnierplan.Adapter library. /// /// - /// To be considered valid, the ID must match the following pattern: [A-Za-z0-9_-]{11} public async Task> GetTournaments(string folderId) { - VerifyId(folderId); - var query = new QueryBuilder { { "folderId", folderId } }; var request = new HttpRequestMessage(HttpMethod.Get, $"/api/tournaments{query}"); - var response = await _httpClient.SendAsync(request); + var response = await _httpClient.SendAsync(request).ConfigureAwait(false); VerifyServerVersion(response); - return await Deserialize>(response); + return await Deserialize>(response).ConfigureAwait(false); } /// @@ -137,41 +119,23 @@ public void Dispose() } } - private void VerifyId(string id) + private static void VerifyServerVersion(HttpResponseMessage response) { - if (_disableIdVerification) - { - return; - } - - if (!ValidIdRegex().IsMatch(id)) - { - throw new TurnierplanClientException("The specified ID does not match the expected format. Specify a correct ID or disable this check using 'DisableIdVerification'."); - } - } - - private void VerifyServerVersion(HttpResponseMessage response) - { - if (_disableVersionVerification) - { - return; - } - if (!response.Headers.TryGetValues(TurnierplanVersionHeaderName, out var headerValue)) { - throw new TurnierplanClientException($"Could not get '{TurnierplanVersionHeaderName}' header from response. Fix the issue or disable the version check using 'DisableServerVersionVerification'."); + throw new TurnierplanClientException($"Could not get '{TurnierplanVersionHeaderName}' header from response."); } var serverVersion = headerValue.FirstOrDefault(); if (string.IsNullOrWhiteSpace(serverVersion)) { - throw new TurnierplanClientException($"Server returned an empty '{TurnierplanVersionHeaderName}' header. Fix the issue or disable the version check using 'DisableServerVersionVerification'."); + throw new TurnierplanClientException($"Server returned an empty '{TurnierplanVersionHeaderName}' header."); } if (!serverVersion.Equals(__turnierplanAdapterVersion)) { - throw new TurnierplanClientException($"Server version '{serverVersion}' does not match the Turnierplan.Adapter version '{__turnierplanAdapterVersion}'. Fix the issue or disable the version check using 'DisableServerVersionVerification'."); + throw new TurnierplanClientException($"Server version '{serverVersion}' does not match the Turnierplan.Adapter version '{__turnierplanAdapterVersion}'."); } } @@ -182,7 +146,7 @@ private static async Task Deserialize(HttpResponseMessage response) throw new TurnierplanClientException($"API returned unexpected status code: {response.StatusCode}"); } - var data = await response.Content.ReadFromJsonAsync(__serializerOptions); + var data = await response.Content.ReadFromJsonAsync(__serializerOptions).ConfigureAwait(false); if (data is null) { @@ -191,7 +155,4 @@ private static async Task Deserialize(HttpResponseMessage response) return data; } - - [GeneratedRegex("^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-]{11}$")] - private static partial Regex ValidIdRegex(); } diff --git a/src/Turnierplan.Adapter/TurnierplanClientException.cs b/src/Turnierplan.Adapter/TurnierplanClientException.cs index b8ff0cbc..63918028 100644 --- a/src/Turnierplan.Adapter/TurnierplanClientException.cs +++ b/src/Turnierplan.Adapter/TurnierplanClientException.cs @@ -1,7 +1,7 @@ namespace Turnierplan.Adapter; /// -/// Represents an error that occurred in the API abstraction layer. +/// An error that occurred in the . /// public sealed class TurnierplanClientException : Exception { diff --git a/src/Turnierplan.Adapter/TurnierplanClientOptions.cs b/src/Turnierplan.Adapter/TurnierplanClientOptions.cs index 2908e50b..740f399f 100644 --- a/src/Turnierplan.Adapter/TurnierplanClientOptions.cs +++ b/src/Turnierplan.Adapter/TurnierplanClientOptions.cs @@ -1,16 +1,16 @@ namespace Turnierplan.Adapter; /// -/// Represents the options necessary for constructing an instance of . +/// The options required for constructing an instance of . /// public sealed record TurnierplanClientOptions { /// - /// Initializes a new instance of the class with the two required parameters representing the API key to use. + /// Initializes a new instance of the class with the specified options. /// /// The base URL of the turnierplan.NET instance that should be queried. - /// The API key ID as displayed in the turnierplan.NET Portal. - /// The API key secret as displayed in the turnierplan.NET Portal. + /// The id of the API key. + /// The secret of the API key. public TurnierplanClientOptions(string applicationUrl, string apiKey, string apiKeySecret) { ApplicationUrl = applicationUrl; @@ -24,30 +24,12 @@ public TurnierplanClientOptions(string applicationUrl, string apiKey, string api public string ApplicationUrl { get; } /// - /// The API key ID as displayed in the turnierplan.NET portal. + /// The id of the API key. /// public string ApiKey { get; } /// - /// The API key secret as displayed in the turnierplan.NET portal. + /// The secret of the API key. /// public string ApiKeySecret { get; } - - /// - /// The value that should be included in the "User-Agent" HTTP header. The default is - /// to Turnierplan.Adapter and this property may not be empty or white-space. - /// - public string UserAgent { get; init; } = "Turnierplan.Adapter"; - - /// - /// If set to true, the does not verify the ID - /// strings before sending the HTTP requests. - /// - public bool DisableIdVerification { get; init; } = false; - - /// - /// If set to true, the does not verify the server version against - /// the used version of Turnierplan.Adapter. - /// - public bool DisableVersionVerification { get; init; } = false; } From 84c723fd48c9109e7be8fddc57b2962b943eebee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 20 Sep 2025 10:35:11 +0200 Subject: [PATCH 4/9] some model updates --- src/Turnierplan.Adapter/Models/Group.cs | 2 +- src/Turnierplan.Adapter/Models/Match.cs | 6 +++--- src/Turnierplan.Adapter/Models/Ranking.cs | 3 +-- src/Turnierplan.Adapter/Models/Team.cs | 2 +- src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs | 3 +-- src/Turnierplan.Adapter/Models/Tournament.cs | 9 +++++++-- src/Turnierplan.Adapter/Models/TournamentHeader.cs | 4 ++-- 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Turnierplan.Adapter/Models/Group.cs b/src/Turnierplan.Adapter/Models/Group.cs index cd1a7812..ea6be499 100644 --- a/src/Turnierplan.Adapter/Models/Group.cs +++ b/src/Turnierplan.Adapter/Models/Group.cs @@ -1,7 +1,7 @@ namespace Turnierplan.Adapter.Models; /// -/// Represents a single group in the context of a . +/// A group in the context of a . /// public sealed record Group { diff --git a/src/Turnierplan.Adapter/Models/Match.cs b/src/Turnierplan.Adapter/Models/Match.cs index 66ded3da..b9a0e6f9 100644 --- a/src/Turnierplan.Adapter/Models/Match.cs +++ b/src/Turnierplan.Adapter/Models/Match.cs @@ -6,9 +6,8 @@ namespace Turnierplan.Adapter.Models; /// -/// Represents a single match in the context of a . A match has two participants -/// which are computed by evaluating the team selectors. Once the match is finished, each participant is -/// assigned a score. +/// A match in the context of a . A match has two participants which are computed +/// by evaluating the team selectors. Once the match is finished, each participant is assigned a score. /// public sealed record Match { @@ -56,6 +55,7 @@ public sealed record Match /// if this match is a deciding/ranking match. /// /// + /// This value is generated client-side because it is not part of the API definition. [JsonIgnore] [MemberNotNullWhen(true, nameof(GroupId))] public bool IsGroupMatch => GroupId is not null; diff --git a/src/Turnierplan.Adapter/Models/Ranking.cs b/src/Turnierplan.Adapter/Models/Ranking.cs index 3040e1f9..3a0dc44e 100644 --- a/src/Turnierplan.Adapter/Models/Ranking.cs +++ b/src/Turnierplan.Adapter/Models/Ranking.cs @@ -3,8 +3,7 @@ namespace Turnierplan.Adapter.Models; /// -/// Represents a single ranking in the context of a . There are -/// equally many rankings in a tournament as there are teams. +/// A ranking in the context of a . There are equally many rankings in a tournament as there are teams. /// public sealed record Ranking { diff --git a/src/Turnierplan.Adapter/Models/Team.cs b/src/Turnierplan.Adapter/Models/Team.cs index c9fdfc7e..107e788d 100644 --- a/src/Turnierplan.Adapter/Models/Team.cs +++ b/src/Turnierplan.Adapter/Models/Team.cs @@ -1,7 +1,7 @@ namespace Turnierplan.Adapter.Models; /// -/// Represents a single team in the context of a . +/// A team in the context of a . /// public sealed record Team { diff --git a/src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs b/src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs index ac9b9459..3bd16e95 100644 --- a/src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs +++ b/src/Turnierplan.Adapter/Models/TeamGroupStatistics.cs @@ -1,8 +1,7 @@ namespace Turnierplan.Adapter.Models; /// -/// Value object which holds a single s statistics in the context -/// of the to which this team is assigned to. +/// Represents a team's game statistics in the context of its group. /// public sealed record TeamGroupStatistics { diff --git a/src/Turnierplan.Adapter/Models/Tournament.cs b/src/Turnierplan.Adapter/Models/Tournament.cs index 8a7f203f..d4e9531d 100644 --- a/src/Turnierplan.Adapter/Models/Tournament.cs +++ b/src/Turnierplan.Adapter/Models/Tournament.cs @@ -3,12 +3,12 @@ namespace Turnierplan.Adapter.Models; /// -/// Represents a single tournament consisting of s, es and s +/// A tournament including all details such as teams, groups and matches. /// public sealed record Tournament { /// - /// The ID of the tournament which is an 11-character sequence of [A-Za-z0-9_-]. + /// The id of the tournament. /// public required string Id { get; init; } @@ -27,6 +27,11 @@ public sealed record Tournament /// public required string? FolderName { get; init; } + /// + /// The name of the venue assigned to this tournament or null if not applicable. + /// + public required string? VenueName { get; init; } + /// /// The visibility of the tournament. /// diff --git a/src/Turnierplan.Adapter/Models/TournamentHeader.cs b/src/Turnierplan.Adapter/Models/TournamentHeader.cs index 1435177a..a20b5a7f 100644 --- a/src/Turnierplan.Adapter/Models/TournamentHeader.cs +++ b/src/Turnierplan.Adapter/Models/TournamentHeader.cs @@ -3,12 +3,12 @@ namespace Turnierplan.Adapter.Models; /// -/// Represents a single tournament but does not include any specific data for brevity. +/// Information about a tournament, excluding any game-specific information for brevity. /// public sealed record TournamentHeader { /// - /// The ID of the tournament which is an 11-character sequence of [A-Za-z0-9_-]. + /// The id of the tournament. /// public required string Id { get; init; } From 50626ea3e512ae0676efa2c33ffe59e7234918e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 20 Sep 2025 11:46:57 +0200 Subject: [PATCH 5/9] inmemory + functional test --- README.md | 10 +- .../GlobalUsings.cs | 2 + ...Turnierplan.Adapter.Test.Functional.csproj | 33 ++ .../TurnierplanAdapterTest.cs | 369 ++++++++++++++++++ src/Turnierplan.Adapter/Models/Tournament.cs | 4 +- .../Models/TournamentHeader.cs | 2 +- .../Extensions/WebApplicationExtensions.cs | 6 +- src/Turnierplan.App/Program.cs | 3 + .../Extensions/ConfigurationExtensions.cs | 5 + .../Extensions/ServiceCollectionExtensions.cs | 24 +- src/Turnierplan.Dal/Turnierplan.Dal.csproj | 1 + src/turnierplan.NET.sln | 7 + 12 files changed, 454 insertions(+), 12 deletions(-) create mode 100644 src/Turnierplan.Adapter.Test.Functional/GlobalUsings.cs create mode 100644 src/Turnierplan.Adapter.Test.Functional/Turnierplan.Adapter.Test.Functional.csproj create mode 100644 src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs diff --git a/README.md b/README.md index ca979e5c..e4df759a 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,16 @@ This readme describes how to deploy the application using the pre-built containe **turnierplan.NET** comes as a pre-built container image which can be deployed with minimal configuration. The image is available on GitHub: [ghcr.io/turnierplan-net/turnierplan](https://github.com/turnierplan-NET/turnierplan.NET/pkgs/container/turnierplan) -In the simplest case, run the container directly using the following command. Make sure to substitute the correct PostgreSQL database connection string: +In the simplest case, you can configure the container to use an in-memory data store. Note that this in-memory store is only meant for quick testing and is *not stable* for production! ```shell -docker run -p 80:8080 -e Turnierplan__ApplicationUrl="http://localhost" -e Database__ConnectionString="" ghcr.io/turnierplan-net/turnierplan:latest +docker run -p 80:8080 -e Turnierplan__ApplicationUrl="http://localhost" -e Database__InMemory="true" ghcr.io/turnierplan-net/turnierplan:latest +``` + +A PostgreSQL database can be configured by specifying the `Database__ConnectionString` environment variable: + +```shell +docker run -p 80:8080 -e Turnierplan__ApplicationUrl="http://localhost" -e Database__ConnectionString="" ghcr.io/turnierplan-net/turnierplan:latest ``` The credentials of the initial admin user are displayed in the container logs. diff --git a/src/Turnierplan.Adapter.Test.Functional/GlobalUsings.cs b/src/Turnierplan.Adapter.Test.Functional/GlobalUsings.cs new file mode 100644 index 00000000..5a482fe0 --- /dev/null +++ b/src/Turnierplan.Adapter.Test.Functional/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using FluentAssertions; +global using Xunit; diff --git a/src/Turnierplan.Adapter.Test.Functional/Turnierplan.Adapter.Test.Functional.csproj b/src/Turnierplan.Adapter.Test.Functional/Turnierplan.Adapter.Test.Functional.csproj new file mode 100644 index 00000000..d5e05c36 --- /dev/null +++ b/src/Turnierplan.Adapter.Test.Functional/Turnierplan.Adapter.Test.Functional.csproj @@ -0,0 +1,33 @@ + + + + + net9.0 + enable + enable + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs b/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs new file mode 100644 index 00000000..0b5aefd2 --- /dev/null +++ b/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs @@ -0,0 +1,369 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Turnierplan.Adapter.Enums; +using Turnierplan.Adapter.Models; +using Turnierplan.Core.ApiKey; +using Turnierplan.Core.Extensions; +using Turnierplan.Core.Folder; +using Turnierplan.Core.Organization; +using Turnierplan.Core.RoleAssignment; +using Turnierplan.Core.Tournament; +using Turnierplan.Dal; +using Group = Turnierplan.Adapter.Models.Group; +using GroupParticipant = Turnierplan.Adapter.Models.GroupParticipant; +using Match = Turnierplan.Adapter.Models.Match; +using MatchOutcomeType = Turnierplan.Core.Tournament.MatchOutcomeType; +using MatchType = Turnierplan.Adapter.Enums.MatchType; +using Team = Turnierplan.Adapter.Models.Team; +using TeamGroupStatistics = Turnierplan.Adapter.Models.TeamGroupStatistics; +using Tournament = Turnierplan.Core.Tournament.Tournament; +using Visibility = Turnierplan.Core.Tournament.Visibility; + +namespace Turnierplan.Adapter.Test.Functional; + +public sealed class TurnierplanAdapterTest +{ + [Fact] + public async Task Turnierplan_Client_Works_As_Expected_With_Test_Server() + { + var server = new TestServer(); + SeedingResult seedingResult; + + await using (var scope = server.Services.CreateAsyncScope()) + { + seedingResult = await SeedDatabaseAsync( + scope.ServiceProvider.GetRequiredService(), + scope.ServiceProvider.GetRequiredService>()); + } + + var options = new TurnierplanClientOptions( + server.ClientOptions.BaseAddress.ToString(), + seedingResult.ApiKeyId, + seedingResult.ApiKeySecret); + + var client = new TurnierplanClient(server.CreateClient(), options); + + var tournaments = await client.GetTournaments(seedingResult.FolderId); + + tournaments.Should().BeEquivalentTo([ + new TournamentHeader + { + Id = seedingResult.Tournament1Id, + Name = "T1", + OrganizationName = "TestOrg", + FolderName = "TestFolder", + Visibility = Enums.Visibility.Private + }, + new TournamentHeader + { + Id = seedingResult.Tournament2Id, + Name = "T2", + OrganizationName = "TestOrg", + FolderName = "TestFolder", + Visibility = Enums.Visibility.Public + } + ]); + + var tournament1 = await client.GetTournament(seedingResult.Tournament1Id); + var tournament2 = await client.GetTournament(seedingResult.Tournament2Id); + + tournament1.Should().BeEquivalentTo(new Turnierplan.Adapter.Models.Tournament + { + Id = seedingResult.Tournament1Id, + Name = "T1", + OrganizationName = "TestOrg", + FolderName = "TestFolder", + VenueName = null, + Visibility = Enums.Visibility.Private, + PublicPageViews = 0, + Teams = [], + Groups = [], + Matches = [], + Rankings = [] + }); + + tournament2.Should().BeEquivalentTo(new Turnierplan.Adapter.Models.Tournament + { + Id = seedingResult.Tournament2Id, + Name = "T2", + OrganizationName = "TestOrg", + FolderName = "TestFolder", + VenueName = null, + Visibility = Enums.Visibility.Public, + PublicPageViews = 3, + Teams = [ + new Team + { + Id = 1, + Name = "Team 1", + OutOfCompetition = false + }, + new Team + { + Id = 2, + Name = "Team 2", + OutOfCompetition = false + }, + new Team + { + Id = 3, + Name = "Team 3", + OutOfCompetition = false + } + ], + Groups = [ + new Group + { + Id = 4, + AlphabeticalId = 'A', + DisplayName = "Gruppe A", + HasCustomDisplayName = false, + Participants = new[] + { + new GroupParticipant + { + TeamId = 1, + Priority = 0, + Statistics = new TeamGroupStatistics + { + Position = 3, + ScoreFor = 2, + ScoreAgainst = 3, + ScoreDifference = -1, + MatchesPlayed = 1, + MatchesWon = 0, + MatchesDrawn = 0, + MatchesLost = 1, + Points = 0 + } + }, + new GroupParticipant + { + TeamId = 2, + Priority = 0, + Statistics = new TeamGroupStatistics + { + Position = 1, + ScoreFor = 3, + ScoreAgainst = 2, + ScoreDifference = 1, + MatchesPlayed = 1, + MatchesWon = 1, + MatchesDrawn = 0, + MatchesLost = 0, + Points = 3 + } + }, + new GroupParticipant + { + TeamId = 3, + Priority = 2, + Statistics = new TeamGroupStatistics + { + Position = 2, + ScoreFor = 0, + ScoreAgainst = 0, + ScoreDifference = 0, + MatchesPlayed = 0, + MatchesWon = 0, + MatchesDrawn = 0, + MatchesLost = 0, + Points = 0 + } + } + } + } + ], + Matches = [ + new Match + { + Id = 5, + Index = 1, + Court = 0, + Kickoff = null, + Type = MatchType.GroupMatch, + FormattedType = "Gruppenspiel", + GroupId = 4, + TeamA = new MatchTeamInfo + { + TeamSelector = new TeamSelector + { + Key = "G4/0", + Localized = "Mannschaft 1, Gruppe A" + }, + TeamId = 1, + Score = 2 + }, + TeamB = new MatchTeamInfo + { + TeamSelector = new TeamSelector + { + Key = "G4/1", + Localized = "Mannschaft 2, Gruppe A" + }, + TeamId = 2, + Score = 3 + }, + State = MatchState.Finished, + OutcomeType = Enums.MatchOutcomeType.Standard + }, + new Match + { + Id = 6, + Index = 2, + Court = 0, + Kickoff = null, + Type = MatchType.GroupMatch, + FormattedType = "Gruppenspiel", + GroupId = 4, + TeamA = new MatchTeamInfo + { + TeamSelector = new TeamSelector + { + Key = "G4/1", + Localized = "Mannschaft 2, Gruppe A" + }, + TeamId = 2, + Score = 1 + }, + TeamB = new MatchTeamInfo + { + TeamSelector = new TeamSelector + { + Key = "G4/2", + Localized = "Mannschaft 3, Gruppe A" + }, + TeamId = 3, + Score = 1 + }, + State = MatchState.CurrentlyPlaying, + OutcomeType = Enums.MatchOutcomeType.AfterOvertime + }, + new Match + { + Id = 7, + Index = 3, + Court = 0, + Kickoff = null, + Type = MatchType.GroupMatch, + FormattedType = "Gruppenspiel", + GroupId = 4, + TeamA = new MatchTeamInfo + { + TeamSelector = new TeamSelector + { + Key = "G4/2", + Localized = "Mannschaft 3, Gruppe A" + }, + TeamId = 3, + Score = null + }, + TeamB = new MatchTeamInfo + { + TeamSelector = new TeamSelector + { + Key = "G4/0", + Localized = "Mannschaft 1, Gruppe A" + }, + TeamId = 1, + Score = null + }, + State = MatchState.NotStarted, + OutcomeType = null + } + ], + Rankings = [ + new Ranking + { + PlacementRank = 1, + IsDefined = false, + TeamId = null + }, + new Ranking + { + PlacementRank = 2, + IsDefined = false, + TeamId = null + }, + new Ranking + { + PlacementRank = 3, + IsDefined = false, + TeamId = null + } + ] + }); + } + + private async Task SeedDatabaseAsync(TurnierplanContext context, IPasswordHasher secretHasher) + { + var organization = new Organization("TestOrg"); + + var folder = new Folder(organization, "TestFolder"); + + var tournament1 = new Tournament(organization, "T1", Visibility.Private); + var tournament2 = new Tournament(organization, "T2", Visibility.Public); + + tournament1.SetFolder(folder); + tournament2.SetFolder(folder); + + { + var team1 = tournament2.AddTeam("Team 1"); + var team2 = tournament2.AddTeam("Team 2"); + var team3 = tournament2.AddTeam("Team 3"); + + var group = tournament2.AddGroup('A'); + + tournament2.AddGroupParticipant(group, team1); + tournament2.AddGroupParticipant(group, team2); + tournament2.AddGroupParticipant(group, team3, 2); + + tournament2.GenerateMatchPlan(new MatchPlanConfiguration + { + GroupRoundConfig = new GroupRoundConfig + { + GroupMatchOrder = GroupMatchOrder.Alternating, + GroupPhaseRounds = 1 + }, + FinalsRoundConfig = null, + ScheduleConfig = null + }); + + tournament2.Matches[0].SetOutcome(false, 2, 3, MatchOutcomeType.Standard); + tournament2.Matches[1].SetOutcome(true, 1, 1, MatchOutcomeType.AfterOvertime); + + tournament2.IncrementPublicPageViews(); + tournament2.IncrementPublicPageViews(); + tournament2.IncrementPublicPageViews(); + } + + var apiKey = new ApiKey(organization, "Test", null, DateTime.UtcNow + TimeSpan.FromDays(1)); + apiKey.AssignNewSecret(plainText => secretHasher.HashPassword(apiKey, plainText), out var secretPlainText); + + organization.AddRoleAssignment(Role.Owner, apiKey.AsPrincipal()); + + context.Organizations.Add(organization); + await context.SaveChangesAsync(); + + return new SeedingResult(folder.PublicId.ToString(), tournament1.PublicId.ToString(), tournament2.PublicId.ToString(), apiKey.PublicId.ToString(), secretPlainText); + } + + private sealed record SeedingResult(string FolderId, string Tournament1Id, string Tournament2Id, string ApiKeyId, string ApiKeySecret); + + private sealed class TestServer : WebApplicationFactory + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureAppConfiguration(config => + { + config.AddInMemoryCollection([ + new KeyValuePair("Database:InMemory", "true") + ]); + }); + } + } + +} diff --git a/src/Turnierplan.Adapter/Models/Tournament.cs b/src/Turnierplan.Adapter/Models/Tournament.cs index d4e9531d..02048a8c 100644 --- a/src/Turnierplan.Adapter/Models/Tournament.cs +++ b/src/Turnierplan.Adapter/Models/Tournament.cs @@ -25,12 +25,12 @@ public sealed record Tournament /// /// The name of the folder to which this tournament is assigned or null if not applicable. /// - public required string? FolderName { get; init; } + public string? FolderName { get; init; } /// /// The name of the venue assigned to this tournament or null if not applicable. /// - public required string? VenueName { get; init; } + public string? VenueName { get; init; } /// /// The visibility of the tournament. diff --git a/src/Turnierplan.Adapter/Models/TournamentHeader.cs b/src/Turnierplan.Adapter/Models/TournamentHeader.cs index a20b5a7f..69c53ff9 100644 --- a/src/Turnierplan.Adapter/Models/TournamentHeader.cs +++ b/src/Turnierplan.Adapter/Models/TournamentHeader.cs @@ -25,7 +25,7 @@ public sealed record TournamentHeader /// /// The name of the folder to which this tournament is assigned or null if not applicable. /// - public required string? FolderName { get; init; } + public string? FolderName { get; init; } /// /// The visibility of the tournament. diff --git a/src/Turnierplan.App/Extensions/WebApplicationExtensions.cs b/src/Turnierplan.App/Extensions/WebApplicationExtensions.cs index 84c57cca..6377dbdf 100644 --- a/src/Turnierplan.App/Extensions/WebApplicationExtensions.cs +++ b/src/Turnierplan.App/Extensions/WebApplicationExtensions.cs @@ -22,7 +22,11 @@ public static async Task InitializeDatabaseAsync(this WebApplication app) var logger = scope.ServiceProvider.GetRequiredService>(); var context = scope.ServiceProvider.GetRequiredService(); - await context.Database.MigrateAsync(); + if (context.Database.IsNpgsql()) + { + // If the database is in-memory, no migration is necessary + await context.Database.MigrateAsync(); + } var userCount = await context.Users.CountAsync(); diff --git a/src/Turnierplan.App/Program.cs b/src/Turnierplan.App/Program.cs index c6427c0e..29715691 100644 --- a/src/Turnierplan.App/Program.cs +++ b/src/Turnierplan.App/Program.cs @@ -114,3 +114,6 @@ await app.InitializeDatabaseAsync(); await app.RunAsync(); + +// Public class definition is required for functional tests +public sealed partial class Program; diff --git a/src/Turnierplan.Dal/Extensions/ConfigurationExtensions.cs b/src/Turnierplan.Dal/Extensions/ConfigurationExtensions.cs index df01ac67..1f255dd6 100644 --- a/src/Turnierplan.Dal/Extensions/ConfigurationExtensions.cs +++ b/src/Turnierplan.Dal/Extensions/ConfigurationExtensions.cs @@ -4,6 +4,11 @@ namespace Turnierplan.Dal.Extensions; public static class ConfigurationExtensions { + public static bool UseInMemoryDatabase(this IConfiguration configuration) + { + return configuration.GetSection("Database").GetValue("InMemory", false); + } + public static string? GetDatabaseConnectionString(this IConfiguration configuration) { return configuration.GetSection("Database").GetValue("ConnectionString"); diff --git a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs index b5fdaabd..0d729930 100644 --- a/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs +++ b/src/Turnierplan.Dal/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Azure.Monitor.OpenTelemetry.Exporter; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -23,6 +24,8 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddTurnierplanDataAccessLayer(this IServiceCollection services, IConfiguration configuration) { + string? inMemoryDatabaseName = null; + services.AddDbContext((sp, options) => { if (sp.GetRequiredService().IsDevelopment()) @@ -30,13 +33,22 @@ public static IServiceCollection AddTurnierplanDataAccessLayer(this IServiceColl options.EnableSensitiveDataLogging(); } - var connectionString = configuration.GetDatabaseConnectionString(); - - options.UseNpgsql(connectionString, npgsqlOptions => + if (configuration.UseInMemoryDatabase()) + { + inMemoryDatabaseName ??= Guid.NewGuid().ToString(); + options.UseInMemoryDatabase(inMemoryDatabaseName); + options.ConfigureWarnings(x => x.Log(InMemoryEventId.TransactionIgnoredWarning)); + } + else { - npgsqlOptions.MigrationsAssembly(typeof(TurnierplanContext).Assembly.GetName().Name); - npgsqlOptions.MigrationsHistoryTable("__EFMigrationHistory", TurnierplanContext.Schema); - }); + var connectionString = configuration.GetDatabaseConnectionString(); + + options.UseNpgsql(connectionString, npgsqlOptions => + { + npgsqlOptions.MigrationsAssembly(typeof(TurnierplanContext).Assembly.GetName().Name); + npgsqlOptions.MigrationsHistoryTable("__EFMigrationHistory", TurnierplanContext.Schema); + }); + } }); var applicationInsightsConnectionString = configuration.GetSection("ApplicationInsights").GetValue("ConnectionString"); diff --git a/src/Turnierplan.Dal/Turnierplan.Dal.csproj b/src/Turnierplan.Dal/Turnierplan.Dal.csproj index 1b604378..77249aa2 100644 --- a/src/Turnierplan.Dal/Turnierplan.Dal.csproj +++ b/src/Turnierplan.Dal/Turnierplan.Dal.csproj @@ -13,6 +13,7 @@ + diff --git a/src/turnierplan.NET.sln b/src/turnierplan.NET.sln index 0717214f..a6b1c963 100644 --- a/src/turnierplan.NET.sln +++ b/src/turnierplan.NET.sln @@ -32,6 +32,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Files", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{56957FEA-CDDB-4D12-B25E-0836A4E62A95}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Turnierplan.Adapter.Test.Functional", "Turnierplan.Adapter.Test.Functional\Turnierplan.Adapter.Test.Functional.csproj", "{BE025CDE-2BE5-4138-A03E-0670314A624E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +88,10 @@ Global {A59A3A12-63CE-4BEC-B411-C15709391D2E}.Debug|Any CPU.Build.0 = Debug|Any CPU {A59A3A12-63CE-4BEC-B411-C15709391D2E}.Release|Any CPU.ActiveCfg = Release|Any CPU {A59A3A12-63CE-4BEC-B411-C15709391D2E}.Release|Any CPU.Build.0 = Release|Any CPU + {BE025CDE-2BE5-4138-A03E-0670314A624E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE025CDE-2BE5-4138-A03E-0670314A624E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE025CDE-2BE5-4138-A03E-0670314A624E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE025CDE-2BE5-4138-A03E-0670314A624E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {82CD2098-2466-43C5-9F6A-9FA0E3D641A0} = {56957FEA-CDDB-4D12-B25E-0836A4E62A95} @@ -93,5 +99,6 @@ Global {2E974732-60EC-4AA3-9DB0-3AF56ED34BE8} = {56957FEA-CDDB-4D12-B25E-0836A4E62A95} {E470B250-3682-4462-ADF6-52310773524E} = {56957FEA-CDDB-4D12-B25E-0836A4E62A95} {53A03C1E-3671-4C09-B2A4-DBFB1F4A2604} = {56957FEA-CDDB-4D12-B25E-0836A4E62A95} + {BE025CDE-2BE5-4138-A03E-0670314A624E} = {56957FEA-CDDB-4D12-B25E-0836A4E62A95} EndGlobalSection EndGlobal From 961b3d112c927b4261d262384cbb5781047351f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 20 Sep 2025 11:59:08 +0200 Subject: [PATCH 6/9] test --- .../TurnierplanAdapterTest.cs | 53 ++++++++++++++++++- src/Turnierplan.Adapter/TurnierplanClient.cs | 6 +-- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs b/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs index 0b5aefd2..e3155300 100644 --- a/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs +++ b/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Hosting; +using System.Net; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; @@ -298,6 +299,42 @@ public async Task Turnierplan_Client_Works_As_Expected_With_Test_Server() }); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Turnierplan_Client_Throws_Exception_When_Version_Does_Not_Match(bool sendHeader) + { + using var httpClient = new HttpClient(new MockHttpMessageHandler(_ => + { + var response = new HttpResponseMessage(); + response.StatusCode = HttpStatusCode.Unauthorized; + + if (sendHeader) + { + response.Headers.Add("x-turnierplan-version", "2024.0.0"); // old version that does not exist + } + + return response; + })); + + var options = new TurnierplanClientOptions("http://localhost", "_", "_"); + var client = new TurnierplanClient(httpClient, options); + + var action = async () => + { + _ = await client.GetTournament("x"); + }; + + var actualVersion = typeof(TurnierplanClient).Assembly.GetName().Version!.ToString(); + var expectedMessage = sendHeader + ? $"Server version '2024.0.0' does not match the Turnierplan.Adapter version '{actualVersion}'." + : "Could not get 'X-Turnierplan-Version' header from response."; + + await action.Should() + .ThrowAsync() + .WithMessage(expectedMessage); + } + private async Task SeedDatabaseAsync(TurnierplanContext context, IPasswordHasher secretHasher) { var organization = new Organization("TestOrg"); @@ -366,4 +403,18 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) } } + private sealed class MockHttpMessageHandler : HttpMessageHandler + { + private readonly Func _handler; + + public MockHttpMessageHandler(Func handler) + { + _handler = handler; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(_handler(request)); + } + } } diff --git a/src/Turnierplan.Adapter/TurnierplanClient.cs b/src/Turnierplan.Adapter/TurnierplanClient.cs index 9091fd7b..73ee067a 100644 --- a/src/Turnierplan.Adapter/TurnierplanClient.cs +++ b/src/Turnierplan.Adapter/TurnierplanClient.cs @@ -12,9 +12,9 @@ namespace Turnierplan.Adapter; /// public sealed class TurnierplanClient : IDisposable { - private const string ApiKeyIdHeaderName = "x-api-key"; - private const string ApiKeySecretHeaderName = "x-api-key-secret"; - private const string TurnierplanVersionHeaderName = "x-turnierplan-version"; + private const string ApiKeyIdHeaderName = "X-Api-Key"; + private const string ApiKeySecretHeaderName = "X-Api-Key-Secret"; + private const string TurnierplanVersionHeaderName = "X-Turnierplan-Version"; private static readonly string __turnierplanAdapterVersion = typeof(TurnierplanClient).Assembly.GetName().Version?.ToString() From 47fb8e86621532ad74fd219e16c0dfb98485243b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 20 Sep 2025 12:00:19 +0200 Subject: [PATCH 7/9] Add 2nd ctor --- src/Turnierplan.Adapter/TurnierplanClient.cs | 3 +-- .../TurnierplanClientOptions.cs | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Turnierplan.Adapter/TurnierplanClient.cs b/src/Turnierplan.Adapter/TurnierplanClient.cs index 73ee067a..5017c30b 100644 --- a/src/Turnierplan.Adapter/TurnierplanClient.cs +++ b/src/Turnierplan.Adapter/TurnierplanClient.cs @@ -42,10 +42,9 @@ public TurnierplanClient(HttpClient httpClient, TurnierplanClientOptions options { ArgumentException.ThrowIfNullOrWhiteSpace(options.ApiKey); ArgumentException.ThrowIfNullOrWhiteSpace(options.ApiKeySecret); - ArgumentException.ThrowIfNullOrWhiteSpace(options.ApplicationUrl); _httpClient = httpClient; - _httpClient.BaseAddress = new Uri(options.ApplicationUrl); + _httpClient.BaseAddress = options.ApplicationUri; _httpClient.DefaultRequestHeaders.Add(ApiKeyIdHeaderName, options.ApiKey); _httpClient.DefaultRequestHeaders.Add(ApiKeySecretHeaderName, options.ApiKeySecret); diff --git a/src/Turnierplan.Adapter/TurnierplanClientOptions.cs b/src/Turnierplan.Adapter/TurnierplanClientOptions.cs index 740f399f..4b2fdd8d 100644 --- a/src/Turnierplan.Adapter/TurnierplanClientOptions.cs +++ b/src/Turnierplan.Adapter/TurnierplanClientOptions.cs @@ -8,20 +8,31 @@ public sealed record TurnierplanClientOptions /// /// Initializes a new instance of the class with the specified options. /// - /// The base URL of the turnierplan.NET instance that should be queried. + /// The base URI of the turnierplan.NET instance that should be queried. /// The id of the API key. /// The secret of the API key. - public TurnierplanClientOptions(string applicationUrl, string apiKey, string apiKeySecret) + public TurnierplanClientOptions(Uri applicationUri, string apiKey, string apiKeySecret) { - ApplicationUrl = applicationUrl; + ApplicationUri = applicationUri; ApiKey = apiKey; ApiKeySecret = apiKeySecret; } /// - /// The base URL of the turnierplan.NET instance that should be queried. + /// Initializes a new instance of the class with the specified options. + /// + /// The base URI of the turnierplan.NET instance that should be queried. + /// The id of the API key. + /// The secret of the API key. + public TurnierplanClientOptions(string applicationUri, string apiKey, string apiKeySecret) + : this(new Uri(applicationUri), apiKey, apiKeySecret) + { + } + + /// + /// The base URI of the turnierplan.NET instance that should be queried. /// - public string ApplicationUrl { get; } + public Uri ApplicationUri { get; } /// /// The id of the API key. From 6c7ce4141d4af950cda7fa8204ad94522db09b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 20 Sep 2025 12:00:50 +0200 Subject: [PATCH 8/9] mixup --- .../TurnierplanAdapterTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs b/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs index e3155300..a440bc16 100644 --- a/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs +++ b/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs @@ -317,7 +317,7 @@ public async Task Turnierplan_Client_Throws_Exception_When_Version_Does_Not_Matc return response; })); - var options = new TurnierplanClientOptions("http://localhost", "_", "_"); + var options = new TurnierplanClientOptions(new Uri("http://localhost"), "_", "_"); var client = new TurnierplanClient(httpClient, options); var action = async () => From 59852c4845a7fdbf264deb556b7a23f20a11b4e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sat, 20 Sep 2025 12:01:54 +0200 Subject: [PATCH 9/9] sq --- .../TurnierplanAdapterTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs b/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs index a440bc16..da14a4f1 100644 --- a/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs +++ b/src/Turnierplan.Adapter.Test.Functional/TurnierplanAdapterTest.cs @@ -335,7 +335,7 @@ await action.Should() .WithMessage(expectedMessage); } - private async Task SeedDatabaseAsync(TurnierplanContext context, IPasswordHasher secretHasher) + private static async Task SeedDatabaseAsync(TurnierplanContext context, IPasswordHasher secretHasher) { var organization = new Organization("TestOrg");