diff --git a/README.md b/README.md index 05a9b5ff..1de1514c 100644 --- a/README.md +++ b/README.md @@ -141,11 +141,11 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( ## ⌛ Progress -![Server & Client - 116 / 318](https://img.shields.io/badge/Server_&_Client-116%20%2F%20318-gold?style=for-the-badge) +![Server & Client - 118 / 318](https://img.shields.io/badge/Server_&_Client-118%20%2F%20318-gold?style=for-the-badge) -![Server - 61 / 225](https://img.shields.io/badge/Server-61%20%2F%20225-red?style=for-the-badge) +![Server - 62 / 225](https://img.shields.io/badge/Server-62%20%2F%20225-red?style=for-the-badge) -![Client - 55 / 93](https://img.shields.io/badge/Client-55%20%2F%2093-gold?style=for-the-badge) +![Client - 56 / 93](https://img.shields.io/badge/Client-56%20%2F%2093-gold?style=for-the-badge) ### 🔑 Key | Icon | Definition | @@ -256,7 +256,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | [Update Phone Verification](https://appwrite.io/docs/references/1.6.x/server-rest/users#updatePhoneVerification) | ❌ | ✅ | ### Teams -![Teams - 16 / 26](https://img.shields.io/badge/Teams-16%20%2F%2026-gold?style=for-the-badge) +![Teams - 18 / 26](https://img.shields.io/badge/Teams-18%20%2F%2026-gold?style=for-the-badge) | Endpoint | Client | Server | |:-:|:-:|:-:| @@ -267,7 +267,7 @@ string emailAddressOrErrorMessage = userResponse.Result.Match( | [Delete Team](https://appwrite.io/docs/references/1.6.x/client-rest/teams#delete) | ✅ | ✅ | | [List Team Memberships](https://appwrite.io/docs/references/1.6.x/client-rest/teams#listMemberships) | ✅ | ✅ | | [Create Team Membership](https://appwrite.io/docs/references/1.6.x/client-rest/teams#createMembership) | ✅ | ✅ | -| [Get Team Membership](https://appwrite.io/docs/references/1.6.x/client-rest/teams#getMembership) | ⬛ | ⬛ | +| [Get Team Membership](https://appwrite.io/docs/references/1.6.x/client-rest/teams#getMembership) | ✅ | ✅ | | [Update Membership](https://appwrite.io/docs/references/1.6.x/client-rest/teams#updateMembership) | ⬛ | ⬛ | | [Delete Team Membership](https://appwrite.io/docs/references/1.6.x/client-rest/teams#deleteMembership) | ✅ | ✅ | | [Update Team Membership Status](https://appwrite.io/docs/references/1.6.x/client-rest/teams#updateMembershipStatus) | ⬛ | ⬛ | diff --git a/src/PinguApps.Appwrite.Client/Clients/ITeamsClient.cs b/src/PinguApps.Appwrite.Client/Clients/ITeamsClient.cs index 858ab4c0..c6d1ccb7 100644 --- a/src/PinguApps.Appwrite.Client/Clients/ITeamsClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/ITeamsClient.cs @@ -79,7 +79,13 @@ public interface ITeamsClient /// The request content /// 204 success code Task DeleteTeamMembership(DeleteTeamMembershipRequest request); - [Obsolete("This method hasn't yet been implemented!")] + + /// + /// Get a team member by the membership unique id. All team members have read access for this resource. + /// Appwrite Docs + /// + /// The request content + /// The membership Task> GetTeamMembership(GetTeamMembershipRequest request); [Obsolete("This method hasn't yet been implemented!")] Task> UpdateMembership(UpdateMembershipRequest request); diff --git a/src/PinguApps.Appwrite.Client/Clients/TeamsClient.cs b/src/PinguApps.Appwrite.Client/Clients/TeamsClient.cs index 1a09c4a5..7e580097 100644 --- a/src/PinguApps.Appwrite.Client/Clients/TeamsClient.cs +++ b/src/PinguApps.Appwrite.Client/Clients/TeamsClient.cs @@ -6,6 +6,7 @@ using PinguApps.Appwrite.Client.Internals; using PinguApps.Appwrite.Client.Utils; using PinguApps.Appwrite.Shared; +using PinguApps.Appwrite.Shared.Enums; using PinguApps.Appwrite.Shared.Requests.Teams; using PinguApps.Appwrite.Shared.Responses; @@ -130,6 +131,7 @@ public async Task> CreateTeamMembership(CreateTeamMem { try { + request.ValidationContext = ValidationContext.Client; request.Validate(true); var result = await _teamsApi.CreateTeamMembership(GetCurrentSessionOrThrow(), request.TeamId, request); @@ -159,9 +161,22 @@ public async Task DeleteTeamMembership(DeleteTeamMembershipReque } } - [ExcludeFromCodeCoverage] /// - public Task> GetTeamMembership(GetTeamMembershipRequest request) => throw new NotImplementedException(); + public async Task> GetTeamMembership(GetTeamMembershipRequest request) + { + try + { + request.Validate(true); + + var result = await _teamsApi.GetTeamMembership(GetCurrentSessionOrThrow(), request.TeamId, request.MembershipId); + + return result.GetApiResponse(); + } + catch (Exception e) + { + return e.GetExceptionResponse(); + } + } [ExcludeFromCodeCoverage] /// diff --git a/src/PinguApps.Appwrite.Playground/App.cs b/src/PinguApps.Appwrite.Playground/App.cs index 7d26ce76..444aa331 100644 --- a/src/PinguApps.Appwrite.Playground/App.cs +++ b/src/PinguApps.Appwrite.Playground/App.cs @@ -19,22 +19,23 @@ public async Task Run(string[] args) { _client.SetSession(_session); - var request = new DeleteTeamMembershipRequest() + var request = new CreateTeamMembershipRequest() { TeamId = "67142b78001c379958cb", - MembershipId = "671448a87af3cd4babc7" + Email = "pingu@example.com", + Name = "Pingu" }; - //var clientResponse = await _client.Teams.CreateTeamMembership(request); + var clientResponse = await _client.Teams.CreateTeamMembership(request); - //Console.WriteLine(clientResponse.Result.Match( - // result => result.ToString(), - // appwriteError => appwriteError.Message, - // internalError => internalError.Message)); + Console.WriteLine(clientResponse.Result.Match( + result => result.ToString(), + appwriteError => appwriteError.Message, + internalError => internalError.Message)); Console.WriteLine("############################################################################"); - var serverResponse = await _server.Teams.DeleteTeamMembership(request); + var serverResponse = await _server.Teams.CreateTeamMembership(request); Console.WriteLine(serverResponse.Result.Match( result => result.ToString(), diff --git a/src/PinguApps.Appwrite.Server/Clients/ITeamsClient.cs b/src/PinguApps.Appwrite.Server/Clients/ITeamsClient.cs index a67fa469..04a8c655 100644 --- a/src/PinguApps.Appwrite.Server/Clients/ITeamsClient.cs +++ b/src/PinguApps.Appwrite.Server/Clients/ITeamsClient.cs @@ -79,7 +79,13 @@ public interface ITeamsClient /// The request content /// 204 success code Task DeleteTeamMembership(DeleteTeamMembershipRequest request); - [Obsolete("This method hasn't yet been implemented!")] + + /// + /// Get a team member by the membership unique id. All team members have read access for this resource. + /// Appwrite Docs + /// + /// The request content + /// The membership Task> GetTeamMembership(GetTeamMembershipRequest request); [Obsolete("This method hasn't yet been implemented!")] Task> UpdateMembership(UpdateMembershipRequest request); diff --git a/src/PinguApps.Appwrite.Server/Clients/TeamsClient.cs b/src/PinguApps.Appwrite.Server/Clients/TeamsClient.cs index 21542ba9..3245a507 100644 --- a/src/PinguApps.Appwrite.Server/Clients/TeamsClient.cs +++ b/src/PinguApps.Appwrite.Server/Clients/TeamsClient.cs @@ -6,6 +6,7 @@ using PinguApps.Appwrite.Server.Internals; using PinguApps.Appwrite.Server.Utils; using PinguApps.Appwrite.Shared; +using PinguApps.Appwrite.Shared.Enums; using PinguApps.Appwrite.Shared.Requests.Teams; using PinguApps.Appwrite.Shared.Responses; @@ -130,6 +131,7 @@ public async Task> CreateTeamMembership(CreateTeamMem { try { + request.ValidationContext = ValidationContext.Server; request.Validate(true); var result = await _teamsApi.CreateTeamMembership(request.TeamId, request); @@ -159,9 +161,22 @@ public async Task DeleteTeamMembership(DeleteTeamMembershipReque } } - [ExcludeFromCodeCoverage] /// - public Task> GetTeamMembership(GetTeamMembershipRequest request) => throw new NotImplementedException(); + public async Task> GetTeamMembership(GetTeamMembershipRequest request) + { + try + { + request.Validate(true); + + var result = await _teamsApi.GetTeamMembership(request.TeamId, request.MembershipId); + + return result.GetApiResponse(); + } + catch (Exception e) + { + return e.GetExceptionResponse(); + } + } [ExcludeFromCodeCoverage] /// diff --git a/src/PinguApps.Appwrite.Shared/Enums/ValidationContext.cs b/src/PinguApps.Appwrite.Shared/Enums/ValidationContext.cs new file mode 100644 index 00000000..bfce1c0a --- /dev/null +++ b/src/PinguApps.Appwrite.Shared/Enums/ValidationContext.cs @@ -0,0 +1,7 @@ +namespace PinguApps.Appwrite.Shared.Enums; +public enum ValidationContext +{ + None, + Client, + Server +} diff --git a/src/PinguApps.Appwrite.Shared/Requests/BaseRequest.cs b/src/PinguApps.Appwrite.Shared/Requests/BaseRequest.cs index d4da9baf..b98db5a7 100644 --- a/src/PinguApps.Appwrite.Shared/Requests/BaseRequest.cs +++ b/src/PinguApps.Appwrite.Shared/Requests/BaseRequest.cs @@ -1,5 +1,8 @@ -using FluentValidation; +using System.Text.Json.Serialization; +using FluentValidation; using FluentValidation.Results; +using PinguApps.Appwrite.Shared.Attributes; +using PinguApps.Appwrite.Shared.Enums; namespace PinguApps.Appwrite.Shared.Requests; @@ -12,6 +15,13 @@ public abstract class BaseRequest where TRequest : class where TValidator : IValidator, new() { + /// + /// A flag to determine whether we are validating from the server or the client. Often not needed. + /// + [JsonIgnore] + [SdkExclude] + public ValidationContext ValidationContext { get; set; } = ValidationContext.None; + /// /// True if the request object passes all validation /// diff --git a/src/PinguApps.Appwrite.Shared/Requests/Teams/Validators/CreateTeamMembershipRequestValidator.cs b/src/PinguApps.Appwrite.Shared/Requests/Teams/Validators/CreateTeamMembershipRequestValidator.cs index f038f16a..a44e2c1f 100644 --- a/src/PinguApps.Appwrite.Shared/Requests/Teams/Validators/CreateTeamMembershipRequestValidator.cs +++ b/src/PinguApps.Appwrite.Shared/Requests/Teams/Validators/CreateTeamMembershipRequestValidator.cs @@ -1,5 +1,6 @@ using System; using FluentValidation; +using PinguApps.Appwrite.Shared.Enums; namespace PinguApps.Appwrite.Shared.Requests.Teams.Validators; public class CreateTeamMembershipRequestValidator : AbstractValidator @@ -36,6 +37,11 @@ public CreateTeamMembershipRequestValidator() .When(x => x.Url is not null) .WithMessage("Invalid URL format."); + RuleFor(x => x.Url) + .NotEmpty() + .When(x => x.ValidationContext == ValidationContext.Client) + .WithMessage("Url is required."); + RuleFor(x => x.Name) .NotEmpty() .WithMessage("Name must either be null or a non empty string.") diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Teams/TeamsClientTests.CreateTeamMembership.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Teams/TeamsClientTests.CreateTeamMembership.cs index 0c45c9a0..c3f20de9 100644 --- a/tests/PinguApps.Appwrite.Client.Tests/Clients/Teams/TeamsClientTests.CreateTeamMembership.cs +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Teams/TeamsClientTests.CreateTeamMembership.cs @@ -16,7 +16,8 @@ public async Task CreateTeamMembership_ShouldReturnSuccess_WhenApiCallSucceeds() { TeamId = IdUtils.GenerateUniqueId(), Email = "test@example.com", - Roles = ["role1", "role2"] + Roles = ["role1", "role2"], + Url = "https://localhost:1234" }; _mockHttp.Expect(HttpMethod.Post, $"{TestConstants.Endpoint}/teams/{request.TeamId}/memberships") @@ -41,7 +42,8 @@ public async Task CreateTeamMembership_ShouldReturnError_WhenSessionIsNull() { TeamId = IdUtils.GenerateUniqueId(), Email = "test@example.com", - Roles = ["role1", "role2"] + Roles = ["role1", "role2"], + Url = "https://localhost:1234" }; // Act @@ -61,7 +63,8 @@ public async Task CreateTeamMembership_ShouldHandleException_WhenApiCallFails() { TeamId = IdUtils.GenerateUniqueId(), Email = "test@example.com", - Roles = ["role1", "role2"] + Roles = ["role1", "role2"], + Url = "https://localhost:1234" }; _mockHttp.Expect(HttpMethod.Post, $"{TestConstants.Endpoint}/teams/{request.TeamId}/memberships") @@ -87,7 +90,8 @@ public async Task CreateTeamMembership_ShouldReturnErrorResponse_WhenExceptionOc { TeamId = IdUtils.GenerateUniqueId(), Email = "test@example.com", - Roles = ["role1", "role2"] + Roles = ["role1", "role2"], + Url = "https://localhost:1234" }; _mockHttp.Expect(HttpMethod.Post, $"{TestConstants.Endpoint}/teams/{request.TeamId}/memberships") diff --git a/tests/PinguApps.Appwrite.Client.Tests/Clients/Teams/TeamsClientTests.GetTeamMembership.cs b/tests/PinguApps.Appwrite.Client.Tests/Clients/Teams/TeamsClientTests.GetTeamMembership.cs new file mode 100644 index 00000000..fbbe082a --- /dev/null +++ b/tests/PinguApps.Appwrite.Client.Tests/Clients/Teams/TeamsClientTests.GetTeamMembership.cs @@ -0,0 +1,101 @@ +using System.Net; +using PinguApps.Appwrite.Client.Clients; +using PinguApps.Appwrite.Shared.Requests.Teams; +using PinguApps.Appwrite.Shared.Tests; +using PinguApps.Appwrite.Shared.Utils; +using RichardSzalay.MockHttp; + +namespace PinguApps.Appwrite.Client.Tests.Clients.Teams; +public partial class TeamsClientTests +{ + [Fact] + public async Task GetTeamMembership_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + var request = new GetTeamMembershipRequest + { + TeamId = IdUtils.GenerateUniqueId(), + MembershipId = IdUtils.GenerateUniqueId() + }; + + _mockHttp.Expect(HttpMethod.Get, $"{TestConstants.Endpoint}/teams/{request.TeamId}/memberships/{request.MembershipId}") + .ExpectedHeaders(true) + .Respond(TestConstants.AppJson, TestConstants.MembershipResponse); + + _appwriteClient.SetSession(TestConstants.Session); + + // Act + var result = await _appwriteClient.Teams.GetTeamMembership(request); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task GetTeamMembership_ShouldReturnError_WhenSessionIsNull() + { + // Arrange + var request = new GetTeamMembershipRequest + { + TeamId = IdUtils.GenerateUniqueId(), + MembershipId = IdUtils.GenerateUniqueId() + }; + + // Act + var result = await _appwriteClient.Teams.GetTeamMembership(request); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsInternalError); + Assert.Equal(ISessionAware.SessionExceptionMessage, result.Result.AsT2.Message); + } + + [Fact] + public async Task GetTeamMembership_ShouldHandleException_WhenApiCallFails() + { + // Arrange + var request = new GetTeamMembershipRequest + { + TeamId = IdUtils.GenerateUniqueId(), + MembershipId = IdUtils.GenerateUniqueId() + }; + + _mockHttp.Expect(HttpMethod.Get, $"{TestConstants.Endpoint}/teams/{request.TeamId}/memberships/{request.MembershipId}") + .ExpectedHeaders(true) + .Respond(HttpStatusCode.BadRequest, TestConstants.AppJson, TestConstants.AppwriteError); + + _appwriteClient.SetSession(TestConstants.Session); + + // Act + var result = await _appwriteClient.Teams.GetTeamMembership(request); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + } + + [Fact] + public async Task GetTeamMembership_ShouldReturnErrorResponse_WhenExceptionOccurs() + { + // Arrange + var request = new GetTeamMembershipRequest + { + TeamId = IdUtils.GenerateUniqueId(), + MembershipId = IdUtils.GenerateUniqueId() + }; + + _mockHttp.Expect(HttpMethod.Get, $"{TestConstants.Endpoint}/teams/{request.TeamId}/memberships/{request.MembershipId}") + .ExpectedHeaders(true) + .Throw(new HttpRequestException("An error occurred")); + + _appwriteClient.SetSession(TestConstants.Session); + + // Act + var result = await _appwriteClient.Teams.GetTeamMembership(request); + + // Assert + Assert.False(result.Success); + Assert.True(result.IsInternalError); + Assert.Equal("An error occurred", result.Result.AsT2.Message); + } +} diff --git a/tests/PinguApps.Appwrite.Server.Tests/Clients/Teams/TeamsClientTests.GetTeamMembership.cs b/tests/PinguApps.Appwrite.Server.Tests/Clients/Teams/TeamsClientTests.GetTeamMembership.cs new file mode 100644 index 00000000..383936dd --- /dev/null +++ b/tests/PinguApps.Appwrite.Server.Tests/Clients/Teams/TeamsClientTests.GetTeamMembership.cs @@ -0,0 +1,75 @@ +using System.Net; +using PinguApps.Appwrite.Shared.Requests.Teams; +using PinguApps.Appwrite.Shared.Tests; +using PinguApps.Appwrite.Shared.Utils; +using RichardSzalay.MockHttp; + +namespace PinguApps.Appwrite.Server.Tests.Clients.Teams; +public partial class TeamsClientTests +{ + [Fact] + public async Task GetTeamMembership_ShouldReturnSuccess_WhenApiCallSucceeds() + { + // Arrange + var request = new GetTeamMembershipRequest + { + TeamId = IdUtils.GenerateUniqueId(), + MembershipId = IdUtils.GenerateUniqueId() + }; + + _mockHttp.Expect(HttpMethod.Get, $"{TestConstants.Endpoint}/teams/{request.TeamId}/memberships/{request.MembershipId}") + .ExpectedHeaders() + .Respond(TestConstants.AppJson, TestConstants.MembershipResponse); + + // Act + var result = await _appwriteClient.Teams.GetTeamMembership(request); + + // Assert + Assert.True(result.Success); + } + + [Fact] + public async Task GetTeamMembership_ShouldHandleException_WhenApiCallFails() + { + // Arrange + var request = new GetTeamMembershipRequest + { + TeamId = IdUtils.GenerateUniqueId(), + MembershipId = IdUtils.GenerateUniqueId() + }; + + _mockHttp.Expect(HttpMethod.Get, $"{TestConstants.Endpoint}/teams/{request.TeamId}/memberships/{request.MembershipId}") + .ExpectedHeaders() + .Respond(HttpStatusCode.BadRequest, TestConstants.AppJson, TestConstants.AppwriteError); + + // Act + var result = await _appwriteClient.Teams.GetTeamMembership(request); + + // Assert + Assert.True(result.IsError); + Assert.True(result.IsAppwriteError); + } + + [Fact] + public async Task GetTeamMembership_ShouldReturnErrorResponse_WhenExceptionOccurs() + { + // Arrange + var request = new GetTeamMembershipRequest + { + TeamId = IdUtils.GenerateUniqueId(), + MembershipId = IdUtils.GenerateUniqueId() + }; + + _mockHttp.Expect(HttpMethod.Get, $"{TestConstants.Endpoint}/teams/{request.TeamId}/memberships/{request.MembershipId}") + .ExpectedHeaders() + .Throw(new HttpRequestException("An error occurred")); + + // Act + var result = await _appwriteClient.Teams.GetTeamMembership(request); + + // Assert + Assert.False(result.Success); + Assert.True(result.IsInternalError); + Assert.Equal("An error occurred", result.Result.AsT2.Message); + } +} diff --git a/tests/PinguApps.Appwrite.Shared.Tests/Requests/Teams/CreateTeamMembershipRequestTests.cs b/tests/PinguApps.Appwrite.Shared.Tests/Requests/Teams/CreateTeamMembershipRequestTests.cs index b6bfca93..648ec623 100644 --- a/tests/PinguApps.Appwrite.Shared.Tests/Requests/Teams/CreateTeamMembershipRequestTests.cs +++ b/tests/PinguApps.Appwrite.Shared.Tests/Requests/Teams/CreateTeamMembershipRequestTests.cs @@ -1,4 +1,5 @@ using FluentValidation; +using PinguApps.Appwrite.Shared.Enums; using PinguApps.Appwrite.Shared.Requests.Teams; using PinguApps.Appwrite.Shared.Requests.Teams.Validators; using PinguApps.Appwrite.Shared.Utils; @@ -71,7 +72,15 @@ public void Properties_CanBeSet() new() { TeamId = IdUtils.GenerateUniqueId(), - UserId = IdUtils.GenerateUniqueId() + UserId = IdUtils.GenerateUniqueId(), + ValidationContext = ValidationContext.Server + }, + new() + { + TeamId = IdUtils.GenerateUniqueId(), + UserId = IdUtils.GenerateUniqueId(), + Url = "https://example.com", + ValidationContext = ValidationContext.Client } ]; @@ -147,6 +156,12 @@ public void IsValid_WithValidData_ReturnsTrue(CreateTeamMembershipRequest reques new() { TeamId = IdUtils.GenerateUniqueId() + }, + new() + { + TeamId = IdUtils.GenerateUniqueId(), + UserId = IdUtils.GenerateUniqueId(), + ValidationContext = ValidationContext.Client } };