diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..307b712 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-reportgenerator-globaltool": { + "version": "5.5.10", + "commands": [ + "reportgenerator" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.github/workflows/dotnet.yaml b/.github/workflows/dotnet.yaml index c332d31..eb31b4c 100644 --- a/.github/workflows/dotnet.yaml +++ b/.github/workflows/dotnet.yaml @@ -22,4 +22,26 @@ jobs: - name: Lint run: ./scripts/lint.sh - name: Test - run: dotnet test --no-build --verbosity normal + run: dotnet test --no-build --coverage --coverage-output-format cobertura --coverage-output coverage.cobertura.xml --verbosity normal + - name: Generate coverage report + run: dotnet reportgenerator "-reports:**/TestResults/coverage.cobertura.xml" "-targetdir:coverage-report" "-reporttypes:Html;Badges;TextSummary" + - name: Upload Pages artifact + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' + uses: actions/upload-pages-artifact@v3 + with: + path: ./coverage-report + + deploy: + needs: build + if: github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 3e759b7..25f5224 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,12 @@ bld/ [Oo]bj/ [Ll]og/ +# Test / coverage outputs +[Tt]est[Rr]esults/ +coverage-report/ +*.cobertura.xml +*.coverage + # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot diff --git a/Directory.Packages.props b/Directory.Packages.props index cdaa82f..3d74bec 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/README.md b/README.md index 747d91f..8b1a96c 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![Nuget](https://img.shields.io/nuget/v/SharpChatwork.svg?style=flat-square)](https://www.nuget.org/packages/SharpChatwork/) -|Branch|Status| -|------|------| -|master|[![master](https://github.com/Egliss/SharpChatwork/actions/workflows/dotnet.yaml/badge.svg?branch=master)](https://github.com/Egliss/SharpChatwork/actions/workflows/dotnet.yaml)| -|develop|[![develop](https://github.com/Egliss/SharpChatwork/actions/workflows/dotnet.yaml/badge.svg?branch=develop)](https://github.com/Egliss/SharpChatwork/actions/workflows/dotnet.yaml)| +|Branch|Status|Coverage| +|------|------|--------| +|master|[![master](https://github.com/Egliss/SharpChatwork/actions/workflows/dotnet.yaml/badge.svg?branch=master)](https://github.com/Egliss/SharpChatwork/actions/workflows/dotnet.yaml)|[![coverage](https://egliss.github.io/SharpChatwork/badge_linecoverage.svg)](https://egliss.github.io/SharpChatwork/)| +|develop|[![develop](https://github.com/Egliss/SharpChatwork/actions/workflows/dotnet.yaml/badge.svg?branch=develop)](https://github.com/Egliss/SharpChatwork/actions/workflows/dotnet.yaml)|[![coverage](https://egliss.github.io/SharpChatwork/badge_linecoverage.svg)](https://egliss.github.io/SharpChatwork/)| ## API Support Status diff --git a/SharpChatwork.Test/SharpChatwork.Test.csproj b/SharpChatwork.Test/SharpChatwork.Test.csproj index b6f859a..29b1a31 100644 --- a/SharpChatwork.Test/SharpChatwork.Test.csproj +++ b/SharpChatwork.Test/SharpChatwork.Test.csproj @@ -5,21 +5,17 @@ enable enable + Exe false true + $(NoWarn);CA1707;CA2012 - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + + + diff --git a/SharpChatwork.Test/src/Client/AccessTokenClientTests.cs b/SharpChatwork.Test/src/Client/AccessTokenClientTests.cs new file mode 100644 index 0000000..9674bd5 --- /dev/null +++ b/SharpChatwork.Test/src/Client/AccessTokenClientTests.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using SharpChatwork.Client.Exceptions; +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Client; + +public class AccessTokenClientTests +{ + [Test] + public async Task QueryAsync_sends_x_chatworktoken_header_and_deserializes_response() + { + var (client, handler) = MockedAccessTokenClient.Create("my-token"); + handler.Expect(HttpMethod.Get, EndPoints.Me.ToString()) + .WithHeaders("X-ChatWorkToken", "my-token") + .Respond("application/json", ApiFixtures.MeGet); + + var user = await client.me.GetUserAsync(); + + await Assert.That(user.account_id).IsEqualTo(123); + await Assert.That(user.name).IsEqualTo("John Smith"); + handler.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task QueryAsync_throws_ChatworkClientException_on_non_success_status() + { + var (client, handler) = MockedAccessTokenClient.Create(); + handler.When(EndPoints.Me.ToString()) + .Respond(HttpStatusCode.InternalServerError, "application/json", ApiFixtures.ErrorResponse); + + await Assert.That(async () => await client.me.GetUserAsync()) + .ThrowsExactly(); + } + + [Test] + public async Task QueryAsync_throws_JsonException_on_malformed_response() + { + var (client, handler) = MockedAccessTokenClient.Create(); + handler.When(EndPoints.Me.ToString()) + .Respond("application/json", "{not-json"); + + await Assert.That(async () => await client.me.GetUserAsync()) + .Throws(); + } + + [Test] + public async Task QueryAsync_uses_specified_http_method_for_post_request() + { + var (client, handler) = MockedAccessTokenClient.Create(); + handler.Expect(HttpMethod.Post, EndPoints.RoomMessages(42).ToString()) + .Respond("application/json", ApiFixtures.MessageIdResult); + + var result = await client.room.message.SendAsync(42, "hi", isSelfUnread: false); + + await Assert.That(result.message_id).IsEqualTo("9876"); + handler.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task ClientName_returns_class_name() + { + var (client, _) = MockedAccessTokenClient.Create(); + await Assert.That(client.clientName).IsEqualTo(nameof(AccessToken.AccessTokenClient)); + } +} diff --git a/SharpChatwork.Test/src/Client/OAuth2ClientTests.cs b/SharpChatwork.Test/src/Client/OAuth2ClientTests.cs new file mode 100644 index 0000000..f5bd841 --- /dev/null +++ b/SharpChatwork.Test/src/Client/OAuth2ClientTests.cs @@ -0,0 +1,51 @@ +using System.Text; +using SharpChatwork.OAuth2; +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Client; + +public class OAuth2ClientTests +{ + [Test] + public async Task UpdateTokenAsync_uses_basic_auth_with_client_and_secret() + { + var (client, handler) = MockedOAuth2Client.Create("ck", "sk"); + var expected = "Basic " + Convert.ToBase64String(Encoding.ASCII.GetBytes("ck:sk")); + handler.Expect(HttpMethod.Post, EndPoints.Token.ToString()) + .WithHeaders("Authorization", expected) + .Respond("application/json", ApiFixtures.OAuth2TokenResult); + + var result = await client.UpdateTokenAsync(OAuth2TokenQuery.GrantType.RefreshToken); + + await Assert.That(result.access_token).IsEqualTo("abc-access"); + await Assert.That(result.refresh_token).IsEqualTo("abc-refresh"); + await Assert.That(result.expires_in).IsEqualTo(3600L); + await Assert.That(result.isError).IsFalse(); + handler.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task QueryAsync_sends_Bearer_authorization_with_access_token() + { + var (client, handler) = MockedOAuth2Client.Create(); + handler.When(HttpMethod.Post, EndPoints.Token.ToString()) + .Respond("application/json", ApiFixtures.OAuth2TokenResult); + await client.UpdateTokenAsync(OAuth2TokenQuery.GrantType.RefreshToken); + + handler.Expect(HttpMethod.Get, EndPoints.Me.ToString()) + .WithHeaders("Authorization", "Bearer abc-access") + .Respond("application/json", ApiFixtures.MeGet); + + var user = await client.me.GetUserAsync(); + + await Assert.That(user.account_id).IsEqualTo(123); + handler.VerifyNoOutstandingExpectation(); + } + + [Test] + public async Task ClientName_returns_class_name() + { + var (client, _) = MockedOAuth2Client.Create(); + await Assert.That(client.clientName).IsEqualTo(nameof(OAuth2Client)); + } +} diff --git a/SharpChatwork.Test/src/Helpers/ApiFixtures.cs b/SharpChatwork.Test/src/Helpers/ApiFixtures.cs new file mode 100644 index 0000000..4f83518 --- /dev/null +++ b/SharpChatwork.Test/src/Helpers/ApiFixtures.cs @@ -0,0 +1,296 @@ +namespace SharpChatwork.Test.Helpers; + +internal static class ApiFixtures +{ + public const string MeGet = """ + { + "account_id": 123, + "room_id": 322, + "name": "John Smith", + "chatwork_id": "tarochatworkid", + "organization_id": 101, + "organization_name": "Hello Company", + "department": "Marketing", + "title": "CMO", + "url": "http://mycompany.example.com", + "introduction": "Self Introduction", + "mail": "taro@example.com", + "tel_organization": "XXX-XXXX-XXXX", + "tel_extension": "YYY-YYYY-YYYY", + "tel_mobile": "ZZZ-ZZZZ-ZZZZ", + "skype": "myskype_id", + "facebook": "myfacebook_id", + "twitter": "mytwitter_id", + "avatar_image_url": "https://example.com/abc.png", + "login_mail": "account@example.com" + } + """; + + public const string MyStatusGet = """ + { + "unread_room_num": 2, + "mention_room_num": 1, + "mytask_room_num": 3, + "unread_num": 12, + "mention_num": 1, + "mytask_num": 8 + } + """; + + public const string MyTasksGet = """ + [ + { + "task_id": 3, + "room": { "room_id": 5, "name": "Group Chat Name", "icon_path": "https://example.com/ico.png" }, + "assigned_by_account": { "account_id": 78, "name": "Anna", "avatar_image_url": "https://example.com/anna.png" }, + "message_id": "13", + "body": "buy milk", + "limit_time": 1384354799, + "status": "open", + "limit_type": "date" + } + ] + """; + + public const string ContactsGet = """ + [ + { + "account_id": 123, + "room_id": 322, + "name": "John Smith", + "chatwork_id": "tarochatworkid", + "organization_id": 101, + "organization_name": "Hello Company", + "department": "Marketing", + "avatar_image_url": "https://example.com/abc.png" + } + ] + """; + + public const string RoomsGet = """ + [ + { + "room_id": 123, + "name": "Group Chat Name", + "type": "group", + "role": "admin", + "sticky": false, + "unread_num": 10, + "mention_num": 1, + "mytask_num": 0, + "message_num": 122, + "file_num": 10, + "task_num": 17, + "icon_path": "https://example.com/ico.png", + "last_update_time": 1298905200 + } + ] + """; + + public const string RoomGet = """ + { + "room_id": 123, + "name": "Group Chat Name", + "type": "group", + "role": "admin", + "sticky": false, + "unread_num": 10, + "mention_num": 1, + "mytask_num": 0, + "message_num": 122, + "file_num": 10, + "task_num": 17, + "icon_path": "https://example.com/ico.png", + "last_update_time": 1298905200 + } + """; + + public const string RoomIdResult = """ + { + "room_id": "1234" + } + """; + + public const string RoomMembersGet = """ + [ + { + "account_id": 123, + "role": "admin", + "name": "John Smith", + "chatwork_id": "tarochatworkid", + "organization_id": 101, + "organization_name": "Hello Company", + "department": "Marketing", + "avatar_image_url": "https://example.com/abc.png" + } + ] + """; + + public const string RoomMessagesGet = """ + [ + { + "message_id": "5", + "account": { + "account_id": 123, + "name": "Bob", + "avatar_image_url": "https://example.com/bob.png" + }, + "body": "Hello Chatwork!", + "send_time": 1384242850, + "update_time": 0 + } + ] + """; + + public const string RoomMessageGet = """ + { + "message_id": "5", + "account": { + "account_id": 123, + "name": "Bob", + "avatar_image_url": "https://example.com/bob.png" + }, + "body": "Hello Chatwork!", + "send_time": 1384242850, + "update_time": 0 + } + """; + + public const string MessageIdResult = """ + { + "message_id": "9876" + } + """; + + public const string MessageReadUnreadResult = """ + { + "unread_num": 3, + "mention_num": 1 + } + """; + + public const string RoomTasksGet = """ + [ + { + "task_id": 3, + "room": { "room_id": 5, "name": "Group Chat", "icon_path": "https://example.com/ico.png" }, + "assigned_by_account": { "account_id": 78, "name": "Anna", "avatar_image_url": "https://example.com/anna.png" }, + "message_id": "13", + "body": "buy milk", + "limit_time": 1384354799, + "status": "open", + "limit_type": "date" + } + ] + """; + + public const string RoomTaskGet = """ + { + "task_id": 3, + "room": { "room_id": 5, "name": "Group Chat", "icon_path": "https://example.com/ico.png" }, + "assigned_by_account": { "account_id": 78, "name": "Anna", "avatar_image_url": "https://example.com/anna.png" }, + "message_id": "13", + "body": "buy milk", + "limit_time": 1384354799, + "status": "open", + "limit_type": "date" + } + """; + + public const string TaskIdResult = """ + { + "task_id": "1234" + } + """; + + public const string RoomFilesGet = """ + [ + { + "file_id": 3, + "account": { + "account_id": 123, + "name": "Bob", + "avatar_image_url": "https://example.com/bob.png" + }, + "message_id": "22", + "filename": "README.md", + "filesize": 21, + "upload_time": 1384164237 + } + ] + """; + + public const string RoomFileGet = """ + { + "file_id": 3, + "account": { + "account_id": 123, + "name": "Bob", + "avatar_image_url": "https://example.com/bob.png" + }, + "message_id": "22", + "filename": "README.md", + "filesize": 21, + "upload_time": 1384164237 + } + """; + + public const string FileIdResult = """ + { + "file_id": "1234" + } + """; + + public const string InviteLinkGet = """ + { + "public": true, + "url": "https://example.com/abc123", + "need_acceptance": true, + "description": "Welcome" + } + """; + + public const string IncomingRequestsGet = """ + [ + { + "request_id": 1, + "account_id": 2, + "message": "Please add me!", + "name": "Mike", + "chatwork_id": "mike-id", + "organization_id": 101, + "organization_name": "Hello Company", + "department": "Engineering", + "avatar_image_url": "https://example.com/mike.png" + } + ] + """; + + public const string IncomingRequestPut = """ + { + "request_id": 1, + "account_id": 2, + "message": "Please add me!", + "name": "Mike", + "chatwork_id": "mike-id", + "organization_id": 101, + "organization_name": "Hello Company", + "department": "Engineering", + "avatar_image_url": "https://example.com/mike.png" + } + """; + + public const string OAuth2TokenResult = """ + { + "access_token": "abc-access", + "refresh_token": "abc-refresh", + "token_type": "bearer", + "expires_in": 3600, + "scope": "rooms.all:read_write" + } + """; + + public const string ErrorResponse = """ + { "errors": ["Invalid API token"] } + """; +} diff --git a/SharpChatwork.Test/src/Helpers/MockedAccessTokenClient.cs b/SharpChatwork.Test/src/Helpers/MockedAccessTokenClient.cs new file mode 100644 index 0000000..b9cd435 --- /dev/null +++ b/SharpChatwork.Test/src/Helpers/MockedAccessTokenClient.cs @@ -0,0 +1,15 @@ +using SharpChatwork.AccessToken; + +namespace SharpChatwork.Test.Helpers; + +internal static class MockedAccessTokenClient +{ + public const string DefaultToken = "test-token"; + + public static (AccessTokenClient Client, MockHttpMessageHandler Handler) Create(string token = DefaultToken) + { + var handler = new MockHttpMessageHandler(); + var http = new HttpClient(handler); + return (new AccessTokenClient(token, http), handler); + } +} diff --git a/SharpChatwork.Test/src/Helpers/MockedOAuth2Client.cs b/SharpChatwork.Test/src/Helpers/MockedOAuth2Client.cs new file mode 100644 index 0000000..f9ff3ee --- /dev/null +++ b/SharpChatwork.Test/src/Helpers/MockedOAuth2Client.cs @@ -0,0 +1,18 @@ +using SharpChatwork.OAuth2; + +namespace SharpChatwork.Test.Helpers; + +internal static class MockedOAuth2Client +{ + public const string DefaultClientKey = "client-key"; + public const string DefaultSecretKey = "secret-key"; + + public static (OAuth2Client Client, MockHttpMessageHandler Handler) Create( + string clientKey = DefaultClientKey, + string secretKey = DefaultSecretKey) + { + var handler = new MockHttpMessageHandler(); + var http = new HttpClient(handler); + return (new OAuth2Client(clientKey, secretKey, http), handler); + } +} diff --git a/SharpChatwork.Test/src/Helpers/StubChatworkClient.cs b/SharpChatwork.Test/src/Helpers/StubChatworkClient.cs new file mode 100644 index 0000000..c2a9132 --- /dev/null +++ b/SharpChatwork.Test/src/Helpers/StubChatworkClient.cs @@ -0,0 +1,64 @@ +namespace SharpChatwork.Test.Helpers; + +internal static class StubChatworkClient +{ + public static ChatworkClient WithJsonResponse(string json, int statusCode = 200) + { + var stub = Substitute.For(); + stub.QueryAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns(_ => new ValueTask(new ResponseWrapper + { + statusCode = statusCode, + content = json, + headers = new Dictionary>(), + })); + return stub; + } + + public static async Task> ReadFormValuesAsync(HttpContent? content) + { + if(content is null) + { + return new Dictionary(); + } + var raw = await content.ReadAsStringAsync(); + var dict = new Dictionary(); + foreach(var pair in raw.Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var eq = pair.IndexOf('='); + if(eq < 0) + { + dict[Uri.UnescapeDataString(pair)] = string.Empty; + continue; + } + var key = Uri.UnescapeDataString(pair[..eq].Replace('+', ' ')); + var value = Uri.UnescapeDataString(pair[(eq + 1)..].Replace('+', ' ')); + dict[key] = value; + } + return dict; + } + + public static HttpContent? LastQueryAsyncContent(ChatworkClient stub) + { + var call = stub.ReceivedCalls() + .Last(c => string.Equals(c.GetMethodInfo().Name, nameof(ChatworkClient.QueryAsync), StringComparison.Ordinal) + && c.GetArguments().Length == 4 + && c.GetArguments()[2] is null or HttpContent); + return (HttpContent?)call.GetArguments()[2]; + } + + public static async Task AssertFormDataAsync(ChatworkClient stub, params (string Key, string Value)[] expected) + { + var content = LastQueryAsyncContent(stub); + var form = await ReadFormValuesAsync(content); + foreach(var (key, value) in expected) + { + var actual = form.TryGetValue(key, out var found) ? found : null; + await Assert.That(actual).IsEqualTo(value).Because($"form key '{key}' should equal '{value}'"); + } + } +} diff --git a/SharpChatwork.Test/src/Query/ContactQueryTests.cs b/SharpChatwork.Test/src/Query/ContactQueryTests.cs new file mode 100644 index 0000000..47f9c93 --- /dev/null +++ b/SharpChatwork.Test/src/Query/ContactQueryTests.cs @@ -0,0 +1,25 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query; + +public class ContactQueryTests +{ + [Test] + public async Task GetAllAsync_calls_contacts_endpoint_with_GET() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.ContactsGet); + var query = new ContactQuery(stub); + + var contacts = (await query.GetAllAsync()).ToList(); + + await Assert.That(contacts.Count).IsEqualTo(1); + await Assert.That(contacts[0].account_id).IsEqualTo(123); + await Assert.That(contacts[0].chatwork_id).IsEqualTo("tarochatworkid"); + await Assert.That(contacts[0].organization_name).IsEqualTo("Hello Company"); + await stub.Received(1).QueryAsync( + EndPoints.Contacts, + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } +} diff --git a/SharpChatwork.Test/src/Query/IncomingRequestQueryTests.cs b/SharpChatwork.Test/src/Query/IncomingRequestQueryTests.cs new file mode 100644 index 0000000..80e85e3 --- /dev/null +++ b/SharpChatwork.Test/src/Query/IncomingRequestQueryTests.cs @@ -0,0 +1,58 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query; + +public class IncomingRequestQueryTests +{ + [Test] + public async Task GetAllAsync_calls_incoming_requests_endpoint_with_GET() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.IncomingRequestsGet); + var query = new IncomingRequestQuery(stub); + + var requests = (await query.GetAllAsync()).ToList(); + + await Assert.That(requests.Count).IsEqualTo(1); + await Assert.That(requests[0].request_id).IsEqualTo(1); + await Assert.That(requests[0].account_id).IsEqualTo(2); + await Assert.That(requests[0].message).IsEqualTo("Please add me!"); + await Assert.That(requests[0].name).IsEqualTo("Mike"); + await Assert.That(requests[0].chatwork_id).IsEqualTo("mike-id"); + await stub.Received(1).QueryAsync( + EndPoints.IncomingRequests, + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task AcceptAsync_puts_to_incoming_requests_of_id() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.IncomingRequestPut); + var query = new IncomingRequestQuery(stub); + + var result = await query.AcceptAsync(7); + + await Assert.That(result.request_id).IsEqualTo(1); + await stub.Received(1).QueryAsync( + EndPoints.IncomingRequestsOf(7), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task CancelAsync_deletes_incoming_requests_of_id() + { + var stub = StubChatworkClient.WithJsonResponse("{}"); + var query = new IncomingRequestQuery(stub); + + await query.CancelAsync(9); + + await stub.Received(1).QueryAsync( + EndPoints.IncomingRequestsOf(9), + HttpMethod.Delete, + Arg.Any(), + Arg.Any()); + } +} diff --git a/SharpChatwork.Test/src/Query/MeQueryTests.cs b/SharpChatwork.Test/src/Query/MeQueryTests.cs new file mode 100644 index 0000000..eed9b1b --- /dev/null +++ b/SharpChatwork.Test/src/Query/MeQueryTests.cs @@ -0,0 +1,59 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query; + +public class MeQueryTests +{ + [Test] + public async Task GetUserAsync_calls_me_endpoint_with_GET() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MeGet); + var query = new MeQuery(stub); + + var user = await query.GetUserAsync(); + + await Assert.That(user.account_id).IsEqualTo(123); + await Assert.That(user.chatwork_id).IsEqualTo("tarochatworkid"); + await Assert.That(user.organization_name).IsEqualTo("Hello Company"); + await Assert.That(user.login_mail).IsEqualTo("account@example.com"); + await stub.Received(1).QueryAsync( + EndPoints.Me, + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task GetMyStatusAsync_calls_my_status_endpoint_with_GET() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MyStatusGet); + var query = new MeQuery(stub); + + var status = await query.GetMyStatusAsync(); + + await Assert.That(status.unread_room_num).IsEqualTo(2); + await Assert.That(status.mytask_num).IsEqualTo(8); + await stub.Received(1).QueryAsync( + EndPoints.MyStatus, + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task GetMyTasksAsync_calls_my_tasks_endpoint_with_GET() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MyTasksGet); + var query = new MeQuery(stub); + + var tasks = (await query.GetMyTasksAsync()).ToList(); + + await Assert.That(tasks.Count).IsEqualTo(1); + await Assert.That(tasks[0].task_id).IsEqualTo(3); + await stub.Received(1).QueryAsync( + EndPoints.MyTasks, + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } +} diff --git a/SharpChatwork.Test/src/Query/RoomFileQueryTests.cs b/SharpChatwork.Test/src/Query/RoomFileQueryTests.cs new file mode 100644 index 0000000..59e976c --- /dev/null +++ b/SharpChatwork.Test/src/Query/RoomFileQueryTests.cs @@ -0,0 +1,59 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query; + +public class RoomFileQueryTests +{ + [Test] + public async Task GetAllAsync_calls_files_endpoint_with_account_id_query_param() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomFilesGet); + var query = new RoomFileQuery(stub); + + var files = (await query.GetAllAsync(42, accountId: 9)).ToList(); + + await Assert.That(files.Count).IsEqualTo(1); + await Assert.That(files[0].file_id).IsEqualTo(3); + await Assert.That(files[0].filename).IsEqualTo("README.md"); + await Assert.That(files[0].filesize).IsEqualTo(21); + await Assert.That(files[0].message_id).IsEqualTo("22"); + await stub.Received(1).QueryAsync( + Arg.Is(u => u.OriginalString.EndsWith("/rooms/42/files?account_id=9", StringComparison.Ordinal)), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task GetAsync_calls_files_of_id_endpoint_with_create_download_url_query() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomFileGet); + var query = new RoomFileQuery(stub); + + var file = await query.GetAsync(42, 3, createDownloadLink: true); + + await Assert.That(file.file_id).IsEqualTo(3); + await stub.Received(1).QueryAsync( + Arg.Is(u => u.OriginalString.EndsWith("/rooms/42/files/3?create_download_url=1", StringComparison.Ordinal)), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task UploadAsync_with_stream_posts_multipart_to_files_endpoint() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.FileIdResult); + var query = new RoomFileQuery(stub); + using var stream = new System.IO.MemoryStream(new byte[] { 1, 2, 3 }); + + var result = await query.UploadAsync(42, stream, "test.txt", "hello"); + + await Assert.That(result.id).IsEqualTo("1234"); + await stub.Received(1).QueryAsync( + EndPoints.RoomFiles(42), + HttpMethod.Post, + Arg.Is(c => c is System.Net.Http.MultipartFormDataContent), + Arg.Any()); + } +} diff --git a/SharpChatwork.Test/src/Query/RoomInviteQueryTests.cs b/SharpChatwork.Test/src/Query/RoomInviteQueryTests.cs new file mode 100644 index 0000000..8f4c3a6 --- /dev/null +++ b/SharpChatwork.Test/src/Query/RoomInviteQueryTests.cs @@ -0,0 +1,76 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query; + +public class RoomInviteQueryTests +{ + [Test] + public async Task GetAsync_gets_room_link() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.InviteLinkGet); + var query = new RoomInviteQuery(stub); + + var link = await query.GetAsync(42); + + await Assert.That(link.url).IsEqualTo("https://example.com/abc123"); + await stub.Received(1).QueryAsync( + EndPoints.RoomLink(42), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task CreateAsync_posts_form_data_to_room_link_endpoint() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.InviteLinkGet); + var query = new RoomInviteQuery(stub); + + var link = await query.CreateAsync(42, "code123", "Welcome", requireAcceptance: true); + + await Assert.That(link.need_acceptance).IsTrue(); + await stub.Received(1).QueryAsync( + EndPoints.RoomLink(42), + HttpMethod.Post, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, + ("code", "code123"), + ("description", "Welcome"), + ("need_acceptance", "1")); + } + + [Test] + public async Task UpdateAsync_puts_form_data_to_room_link_endpoint() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.InviteLinkGet); + var query = new RoomInviteQuery(stub); + + await query.UpdateAsync(42, "code456", "Hi", requireAcceptance: false); + + await stub.Received(1).QueryAsync( + EndPoints.RoomLink(42), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, + ("code", "code456"), + ("description", "Hi"), + ("need_acceptance", "0")); + } + + [Test] + public async Task DestroyAsync_calls_room_link_with_DELETE() + { + var stub = StubChatworkClient.WithJsonResponse("{}"); + var query = new RoomInviteQuery(stub); + + await query.DestroyAsync(42); + + await stub.Received(1).QueryAsync( + EndPoints.RoomLink(42), + HttpMethod.Delete, + Arg.Any(), + Arg.Any()); + } +} diff --git a/SharpChatwork.Test/src/Query/RoomMemberQueryTests.cs b/SharpChatwork.Test/src/Query/RoomMemberQueryTests.cs new file mode 100644 index 0000000..d2bfca5 --- /dev/null +++ b/SharpChatwork.Test/src/Query/RoomMemberQueryTests.cs @@ -0,0 +1,46 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query; + +public class RoomMemberQueryTests +{ + [Test] + public async Task GetAllAsync_calls_room_member_endpoint_with_GET() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomMembersGet); + var query = new RoomMemberQuery(stub); + + var members = (await query.GetAllAsync(123)).ToList(); + + await Assert.That(members.Count).IsEqualTo(1); + await Assert.That(members[0].account_id).IsEqualTo(123); + await stub.Received(1).QueryAsync( + EndPoints.RoomMember(123), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task UpdateAsync_puts_form_data_to_room_member_endpoint() + { + var stub = StubChatworkClient.WithJsonResponse("""{"admin":[10],"member":[20,21],"readonly":[30]}"""); + var query = new RoomMemberQuery(stub); + + var result = await query.UpdateAsync(42, [10L], [20L, 21L], [30L]); + + await Assert.That(result.admin).Contains(10L); + await Assert.That(result.member).Contains(20L); + await Assert.That(result.member).Contains(21L); + await Assert.That(result.@readonly).Contains(30L); + await stub.Received(1).QueryAsync( + EndPoints.RoomMember(42), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, + ("members_admin_ids", "10"), + ("members_member_ids", "20,21"), + ("members_readonly_ids", "30")); + } +} diff --git a/SharpChatwork.Test/src/Query/RoomMessageQueryTests.cs b/SharpChatwork.Test/src/Query/RoomMessageQueryTests.cs new file mode 100644 index 0000000..9bf7779 --- /dev/null +++ b/SharpChatwork.Test/src/Query/RoomMessageQueryTests.cs @@ -0,0 +1,126 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query; + +public class RoomMessageQueryTests +{ + [Test] + public async Task GetAllAsync_calls_messages_endpoint_with_force_query_param() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomMessagesGet); + var query = new RoomMessageQuery(stub); + + var messages = (await query.GetAllAsync(42, isForceMode: true)).ToList(); + + await Assert.That(messages.Count).IsEqualTo(1); + await Assert.That(messages[0].body).IsEqualTo("Hello Chatwork!"); + await stub.Received(1).QueryAsync( + Arg.Is(u => u.OriginalString.EndsWith("/rooms/42/messages?force=1", StringComparison.Ordinal)), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task GetAsync_calls_messages_of_id_with_GET() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomMessageGet); + var query = new RoomMessageQuery(stub); + + var message = await query.GetAsync(42, 5); + + await Assert.That(message.message_id).IsEqualTo("5"); + await Assert.That(message.body).IsEqualTo("Hello Chatwork!"); + await Assert.That(message.send_time).IsEqualTo(1384242850); + await Assert.That(message.account.account_id).IsEqualTo(123); + await Assert.That(message.account.name).IsEqualTo("Bob"); + await stub.Received(1).QueryAsync( + EndPoints.RoomMessagesOf(42, 5), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task SendAsync_posts_body_and_self_unread_form_data() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MessageIdResult); + var query = new RoomMessageQuery(stub); + + var result = await query.SendAsync(42, "hello", isSelfUnread: true); + + await Assert.That(result.message_id).IsEqualTo("9876"); + await stub.Received(1).QueryAsync( + EndPoints.RoomMessages(42), + HttpMethod.Post, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("body", "hello"), ("self_unread", "1")); + } + + [Test] + public async Task UpdateAsync_puts_to_messages_of_id_with_body_form() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MessageIdResult); + var query = new RoomMessageQuery(stub); + + var result = await query.UpdateAsync(42, 5, "updated"); + + await Assert.That(result.message_id).IsEqualTo("9876"); + await stub.Received(1).QueryAsync( + EndPoints.RoomMessagesOf(42, 5), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("body", "updated")); + } + + [Test] + public async Task RemoveAsync_calls_messages_of_id_with_DELETE() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MessageIdResult); + var query = new RoomMessageQuery(stub); + + await query.RemoveAsync(42, 5); + + await stub.Received(1).QueryAsync( + EndPoints.RoomMessagesOf(42, 5), + HttpMethod.Delete, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task ReadAsync_puts_to_messages_read_with_message_id_form() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MessageReadUnreadResult); + var query = new RoomMessageQuery(stub); + + var result = await query.ReadAsync(42, 5); + + await Assert.That(result.unread_num).IsEqualTo(3); + await stub.Received(1).QueryAsync( + EndPoints.RoomMessagesRead(42), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("message_id", "5")); + } + + [Test] + public async Task UnReadAsync_puts_to_messages_unread_with_message_id_form() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MessageReadUnreadResult); + var query = new RoomMessageQuery(stub); + + var result = await query.UnReadAsync(42, 5); + + await Assert.That(result.mention_num).IsEqualTo(1); + await stub.Received(1).QueryAsync( + EndPoints.RoomMessagesUnread(42), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("message_id", "5")); + } +} diff --git a/SharpChatwork.Test/src/Query/RoomQueryTests.cs b/SharpChatwork.Test/src/Query/RoomQueryTests.cs new file mode 100644 index 0000000..f46b99d --- /dev/null +++ b/SharpChatwork.Test/src/Query/RoomQueryTests.cs @@ -0,0 +1,126 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query; + +public class RoomQueryTests +{ + [Test] + public async Task GetAllAsync_calls_rooms_endpoint_with_GET() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomsGet); + var query = new RoomQuery(stub); + + var rooms = (await query.GetAllAsync()).ToList(); + + await Assert.That(rooms.Count).IsEqualTo(1); + await Assert.That(rooms[0].room_id).IsEqualTo(123); + await stub.Received(1).QueryAsync( + EndPoints.Rooms, + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task GetAsync_calls_room_of_id_with_GET() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomGet); + var query = new RoomQuery(stub); + + var room = await query.GetAsync(123); + + await Assert.That(room.room_id).IsEqualTo(123); + await Assert.That(room.name).IsEqualTo("Group Chat Name"); + await Assert.That(room.type).IsEqualTo("group"); + await Assert.That(room.role).IsEqualTo("admin"); + await Assert.That(room.sticky).IsFalse(); + await Assert.That(room.message_num).IsEqualTo(122); + await Assert.That(room.last_update_time).IsEqualTo(1298905200); + await stub.Received(1).QueryAsync( + EndPoints.RoomOf(123), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task CreateAsync_posts_to_rooms_endpoint() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomIdResult); + var query = new RoomQuery(stub); + + var result = await query.CreateAsync(); + + await Assert.That(result.room_id).IsEqualTo("1234"); + await stub.Received(1).QueryAsync( + EndPoints.Rooms, + HttpMethod.Post, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task UpdateAsync_puts_form_data_to_room_of_id() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomIdResult); + var query = new RoomQuery(stub); + + var result = await query.UpdateAsync(123, "new room", "desc", RoomIconPreset.Project); + + await Assert.That(result.room_id).IsEqualTo("1234"); + await stub.Received(1).QueryAsync( + EndPoints.RoomOf(123), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, + ("name", "new room"), + ("description", "desc"), + ("icon_preset", "project")); + } + + [Test] + public async Task LeaveAsync_deletes_room_of_id_with_action_type_leave() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomIdResult); + var query = new RoomQuery(stub); + + await query.LeaveAsync(123); + + await stub.Received(1).QueryAsync( + EndPoints.RoomOf(123), + HttpMethod.Delete, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("action_type", "leave")); + } + + [Test] + public async Task DeleteAsync_deletes_room_of_id_with_action_type_delete() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomIdResult); + var query = new RoomQuery(stub); + + await query.DeleteAsync(123); + + await stub.Received(1).QueryAsync( + EndPoints.RoomOf(123), + HttpMethod.Delete, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("action_type", "delete")); + } + + [Test] + public async Task SubQueries_are_instantiated_with_correct_types() + { + var stub = StubChatworkClient.WithJsonResponse("{}"); + var query = new RoomQuery(stub); + + await Assert.That(query.message).IsTypeOf(); + await Assert.That(query.member).IsTypeOf(); + await Assert.That(query.invite).IsTypeOf(); + await Assert.That(query.file).IsTypeOf(); + await Assert.That(query.task).IsTypeOf(); + } +} diff --git a/SharpChatwork.Test/src/Query/RoomTaskQueryTests.cs b/SharpChatwork.Test/src/Query/RoomTaskQueryTests.cs new file mode 100644 index 0000000..420b12d --- /dev/null +++ b/SharpChatwork.Test/src/Query/RoomTaskQueryTests.cs @@ -0,0 +1,84 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query; + +public class RoomTaskQueryTests +{ + [Test] + public async Task GetAllAsync_sends_account_id_and_status_as_query_string() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomTasksGet); + var query = new RoomTaskQuery(stub); + + var tasks = (await query.GetAllAsync(42, accountId: 1, autherId: 2, isDone: false)).ToList(); + + await Assert.That(tasks.Count).IsEqualTo(1); + await stub.Received(1).QueryAsync( + Arg.Is(u => + u.OriginalString.Contains("/rooms/42/tasks?", StringComparison.Ordinal) + && u.OriginalString.Contains("account_id=1", StringComparison.Ordinal) + && u.OriginalString.Contains("assigned_by_account_id=2", StringComparison.Ordinal) + && u.OriginalString.Contains("status=open", StringComparison.Ordinal)), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task GetAllAsync_uses_status_done_when_isDone_true() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomTasksGet); + var query = new RoomTaskQuery(stub); + + await query.GetAllAsync(42, 1, 2, isDone: true); + + await stub.Received(1).QueryAsync( + Arg.Is(u => u.OriginalString.Contains("status=done", StringComparison.Ordinal)), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task GetAsync_calls_room_task_of_id_with_GET() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomTaskGet); + var query = new RoomTaskQuery(stub); + + var task = await query.GetAsync(42, 3); + + await Assert.That(task.task_id).IsEqualTo(3); + await stub.Received(1).QueryAsync( + EndPoints.RoomTasksOf(42, 3), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task UpdateAsync_puts_to_task_status_with_body_form() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.TaskIdResult); + var query = new RoomTaskQuery(stub); + + var result = await query.UpdateAsync(42, 3, TaskStateType.Done); + + await Assert.That(result.task_id).IsEqualTo("1234"); + await stub.Received(1).QueryAsync( + EndPoints.RoomTasksOfStatus(42, 3), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("body", "done")); + } + + [Test] + public async Task CreateAsync_throws_NotImplementedException() + { + var stub = StubChatworkClient.WithJsonResponse("{}"); + var query = new RoomTaskQuery(stub); + + await Assert.That(async () => await query.CreateAsync(1, "body", 0)) + .Throws(); + } +} diff --git a/SharpChatwork.Test/src/Query/Spec/DtoSpecTests.cs b/SharpChatwork.Test/src/Query/Spec/DtoSpecTests.cs new file mode 100644 index 0000000..27baf0a --- /dev/null +++ b/SharpChatwork.Test/src/Query/Spec/DtoSpecTests.cs @@ -0,0 +1,25 @@ +namespace SharpChatwork.Test.Query.Spec; + +public class DtoSpecTests +{ + [Test] + public async Task Room_should_expose_description_per_spec() + { + var prop = typeof(Room).GetProperty("description"); + await Assert.That(prop).IsNotNull(); + } + + [Test] + public async Task UserTask_should_expose_account_per_spec() + { + var prop = typeof(UserTask).GetProperty("account"); + await Assert.That(prop).IsNotNull(); + } + + [Test] + public async Task UserFile_should_expose_download_url_per_spec() + { + var prop = typeof(UserFile).GetProperty("download_url"); + await Assert.That(prop).IsNotNull(); + } +} diff --git a/SharpChatwork.Test/src/Query/Spec/IncomingRequestQuerySpecTests.cs b/SharpChatwork.Test/src/Query/Spec/IncomingRequestQuerySpecTests.cs new file mode 100644 index 0000000..99120b9 --- /dev/null +++ b/SharpChatwork.Test/src/Query/Spec/IncomingRequestQuerySpecTests.cs @@ -0,0 +1,21 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query.Spec; + +public class IncomingRequestQuerySpecTests +{ + [Test] + public async Task AcceptAsync_should_use_PUT_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.IncomingRequestPut); + var query = new IncomingRequestQuery(stub); + + await query.AcceptAsync(7); + + await stub.Received(1).QueryAsync( + EndPoints.IncomingRequestsOf(7), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + } +} diff --git a/SharpChatwork.Test/src/Query/Spec/README.md b/SharpChatwork.Test/src/Query/Spec/README.md new file mode 100644 index 0000000..6325aa3 --- /dev/null +++ b/SharpChatwork.Test/src/Query/Spec/README.md @@ -0,0 +1,12 @@ +# Spec Conformance Tests + +This folder contains tests that verify the **specification** as documented in `docs/apis/` — +not the current implementation. Each test name and `[Skip]` reason cites the doc that defines +the expected behavior. + +Tests marked `[Skip("impl-bug: ...")]` describe a known divergence between the implementation +and the documented Chatwork API. They will start passing automatically once the underlying +implementation bug is fixed; this is intentional. + +Existing tests in `Query/*.cs` lock the **current** behavior of the implementation, so a fix +to one of these bugs will require updating the matching regression test. diff --git a/SharpChatwork.Test/src/Query/Spec/RoomFileQuerySpecTests.cs b/SharpChatwork.Test/src/Query/Spec/RoomFileQuerySpecTests.cs new file mode 100644 index 0000000..a80259b --- /dev/null +++ b/SharpChatwork.Test/src/Query/Spec/RoomFileQuerySpecTests.cs @@ -0,0 +1,30 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query.Spec; + +public class RoomFileQuerySpecTests +{ + [Test] + public async Task EndPoints_RoomFilesOf_should_not_contain_extra_whitespace_per_spec() + { + var uri = EndPoints.RoomFilesOf(42, 7).ToString(); + + await Assert.That(uri).IsEqualTo("https://api.chatwork.com/v2/rooms/42/files/7"); + } + + [Test] + public async Task GetAsync_should_target_file_of_id_path_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomFileGet); + var query = new RoomFileQuery(stub); + + await query.GetAsync(42, 7, createDownloadLink: true); + + await stub.Received(1).QueryAsync( + Arg.Is(u => u.OriginalString.StartsWith( + "https://api.chatwork.com/v2/rooms/42/files/7", StringComparison.Ordinal)), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } +} diff --git a/SharpChatwork.Test/src/Query/Spec/RoomInviteQuerySpecTests.cs b/SharpChatwork.Test/src/Query/Spec/RoomInviteQuerySpecTests.cs new file mode 100644 index 0000000..d999488 --- /dev/null +++ b/SharpChatwork.Test/src/Query/Spec/RoomInviteQuerySpecTests.cs @@ -0,0 +1,51 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query.Spec; + +public class RoomInviteQuerySpecTests +{ + [Test] + public async Task GetAsync_should_use_GET_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.InviteLinkGet); + var query = new RoomInviteQuery(stub); + + await query.GetAsync(42); + + await stub.Received(1).QueryAsync( + EndPoints.RoomLink(42), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task CreateAsync_should_target_link_endpoint_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.InviteLinkGet); + var query = new RoomInviteQuery(stub); + + await query.CreateAsync(42, "code123", "Welcome", requireAcceptance: true); + + await stub.Received(1).QueryAsync( + EndPoints.RoomLink(42), + HttpMethod.Post, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task UpdateAsync_should_target_link_endpoint_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.InviteLinkGet); + var query = new RoomInviteQuery(stub); + + await query.UpdateAsync(42, "code456", "Hi", requireAcceptance: false); + + await stub.Received(1).QueryAsync( + EndPoints.RoomLink(42), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + } +} diff --git a/SharpChatwork.Test/src/Query/Spec/RoomMemberQuerySpecTests.cs b/SharpChatwork.Test/src/Query/Spec/RoomMemberQuerySpecTests.cs new file mode 100644 index 0000000..9df0da5 --- /dev/null +++ b/SharpChatwork.Test/src/Query/Spec/RoomMemberQuerySpecTests.cs @@ -0,0 +1,25 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query.Spec; + +public class RoomMemberQuerySpecTests +{ + [Test] + public async Task UpdateAsync_should_PUT_members_with_form_data_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse("""{"admin":[1],"member":[2],"readonly":[3]}"""); + var query = new RoomMemberQuery(stub); + + await query.UpdateAsync(42, [1L], [2L], [3L]); + + await stub.Received(1).QueryAsync( + EndPoints.RoomMember(42), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, + ("members_admin_ids", "1"), + ("members_member_ids", "2"), + ("members_readonly_ids", "3")); + } +} diff --git a/SharpChatwork.Test/src/Query/Spec/RoomMessageQuerySpecTests.cs b/SharpChatwork.Test/src/Query/Spec/RoomMessageQuerySpecTests.cs new file mode 100644 index 0000000..5ab2579 --- /dev/null +++ b/SharpChatwork.Test/src/Query/Spec/RoomMessageQuerySpecTests.cs @@ -0,0 +1,54 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query.Spec; + +public class RoomMessageQuerySpecTests +{ + [Test] + public async Task ReadAsync_should_PUT_messages_read_with_message_id_body_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MessageReadUnreadResult); + var query = new RoomMessageQuery(stub); + + await query.ReadAsync(42, 5); + + await stub.Received(1).QueryAsync( + Arg.Is(u => u.OriginalString == "https://api.chatwork.com/v2/rooms/42/messages/read"), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("message_id", "5")); + } + + [Test] + public async Task UnReadAsync_should_PUT_messages_unread_with_message_id_body_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MessageReadUnreadResult); + var query = new RoomMessageQuery(stub); + + await query.UnReadAsync(42, 5); + + await stub.Received(1).QueryAsync( + Arg.Is(u => u.OriginalString == "https://api.chatwork.com/v2/rooms/42/messages/unread"), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("message_id", "5")); + } + + [Test] + public async Task UpdateAsync_should_PUT_messages_of_id_with_body_in_form_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.MessageIdResult); + var query = new RoomMessageQuery(stub); + + await query.UpdateAsync(42, 5, "updated"); + + await stub.Received(1).QueryAsync( + EndPoints.RoomMessagesOf(42, 5), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("body", "updated")); + } +} diff --git a/SharpChatwork.Test/src/Query/Spec/RoomQuerySpecTests.cs b/SharpChatwork.Test/src/Query/Spec/RoomQuerySpecTests.cs new file mode 100644 index 0000000..80ca8ce --- /dev/null +++ b/SharpChatwork.Test/src/Query/Spec/RoomQuerySpecTests.cs @@ -0,0 +1,89 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query.Spec; + +public class RoomQuerySpecTests +{ + [Test] + public async Task EndPoints_Rooms_should_be_lowercase_per_spec() + { + await Assert.That(EndPoints.Rooms.ToString()) + .IsEqualTo("https://api.chatwork.com/v2/rooms"); + } + + [Test] + public async Task CreateAsync_should_use_POST_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomIdResult); + var query = new RoomQuery(stub); + + await query.CreateAsync(); + + await stub.Received(1).QueryAsync( + EndPoints.Rooms, + HttpMethod.Post, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task UpdateAsync_should_use_PUT_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomIdResult); + var query = new RoomQuery(stub); + + await query.UpdateAsync(123, "new room", "desc", RoomIconPreset.Project); + + await stub.Received(1).QueryAsync( + EndPoints.RoomOf(123), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task UpdateAsync_should_send_distinct_description_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomIdResult); + var query = new RoomQuery(stub); + + await query.UpdateAsync(123, "new room", "a separate description", RoomIconPreset.Project); + + await StubChatworkClient.AssertFormDataAsync(stub, + ("name", "new room"), + ("description", "a separate description"), + ("icon_preset", "project")); + } + + [Test] + public async Task DeleteAsync_should_DELETE_room_of_id_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomIdResult); + var query = new RoomQuery(stub); + + await query.DeleteAsync(123); + + await stub.Received(1).QueryAsync( + EndPoints.RoomOf(123), + HttpMethod.Delete, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("action_type", "delete")); + } + + [Test] + public async Task LeaveAsync_should_DELETE_room_of_id_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomIdResult); + var query = new RoomQuery(stub); + + await query.LeaveAsync(123); + + await stub.Received(1).QueryAsync( + EndPoints.RoomOf(123), + HttpMethod.Delete, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("action_type", "leave")); + } +} diff --git a/SharpChatwork.Test/src/Query/Spec/RoomTaskQuerySpecTests.cs b/SharpChatwork.Test/src/Query/Spec/RoomTaskQuerySpecTests.cs new file mode 100644 index 0000000..f3f1775 --- /dev/null +++ b/SharpChatwork.Test/src/Query/Spec/RoomTaskQuerySpecTests.cs @@ -0,0 +1,56 @@ +using SharpChatwork.Test.Helpers; + +namespace SharpChatwork.Test.Query.Spec; + +public class RoomTaskQuerySpecTests +{ + [Test] + public async Task GetAllAsync_should_send_filters_as_query_string_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.RoomTasksGet); + var query = new RoomTaskQuery(stub); + + await query.GetAllAsync(42, accountId: 1, autherId: 2, isDone: false); + + await stub.Received(1).QueryAsync( + Arg.Is(u => + u.OriginalString.Contains("account_id=1", StringComparison.Ordinal) + && u.OriginalString.Contains("assigned_by_account_id=2", StringComparison.Ordinal) + && u.OriginalString.Contains("status=open", StringComparison.Ordinal)), + HttpMethod.Get, + Arg.Any(), + Arg.Any()); + } + + [Test] + public async Task UpdateAsync_should_PUT_tasks_of_id_status_with_body_in_form_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse(ApiFixtures.TaskIdResult); + var query = new RoomTaskQuery(stub); + + await query.UpdateAsync(42, 3, TaskStateType.Done); + + await stub.Received(1).QueryAsync( + Arg.Is(u => u.OriginalString == "https://api.chatwork.com/v2/rooms/42/tasks/3/status"), + HttpMethod.Put, + Arg.Any(), + Arg.Any()); + await StubChatworkClient.AssertFormDataAsync(stub, ("body", "done")); + } + + [Test] + [Skip("interface-gap: IRoomTaskQuery.CreateAsync(roomId, taskText, limit) is missing required to_ids/limit_type params per spec docs/apis/rooms/tasks/post.md. Signature extension needed before this can be implemented.")] + public async Task CreateAsync_should_POST_tasks_with_required_body_per_spec() + { + var stub = StubChatworkClient.WithJsonResponse("""{"task_ids":[123,124]}"""); + var query = new RoomTaskQuery(stub); + + await query.CreateAsync(42, "buy milk", 1384354799); + + await stub.Received(1).QueryAsync( + EndPoints.RoomTasks(42), + HttpMethod.Post, + Arg.Any(), + Arg.Any()); + } +} diff --git a/SharpChatwork.Test/src/UnitTest1.cs b/SharpChatwork.Test/src/UnitTest1.cs deleted file mode 100644 index ee7cf5d..0000000 --- a/SharpChatwork.Test/src/UnitTest1.cs +++ /dev/null @@ -1,11 +0,0 @@ -using SharpChatwork.Query; - -namespace SharpChatwork.Test; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - } -} diff --git a/SharpChatwork.Test/src/Usings.cs b/SharpChatwork.Test/src/Usings.cs index c802f44..9e394e6 100644 --- a/SharpChatwork.Test/src/Usings.cs +++ b/SharpChatwork.Test/src/Usings.cs @@ -1 +1,16 @@ -global using Xunit; +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Net; +global using System.Net.Http; +global using System.Threading; +global using System.Threading.Tasks; +global using NSubstitute; +global using RichardSzalay.MockHttp; +global using SharpChatwork; +global using SharpChatwork.Query; +global using SharpChatwork.Query.Types; +global using TUnit.Assertions; +global using TUnit.Assertions.Conditions; +global using TUnit.Assertions.Extensions; +global using TUnit.Core; diff --git a/SharpChatwork/src/Client/EndPoints.cs b/SharpChatwork/src/Client/EndPoints.cs index ecc663b..0543fe3 100644 --- a/SharpChatwork/src/Client/EndPoints.cs +++ b/SharpChatwork/src/Client/EndPoints.cs @@ -9,7 +9,7 @@ public class EndPoints public static readonly Uri Me = new Uri(@"https://api.chatwork.com/v2/me"); public static readonly Uri MyStatus = new Uri(@"https://api.chatwork.com/v2/my/status"); public static readonly Uri MyTasks = new Uri(@"https://api.chatwork.com/v2/my/tasks"); - public static readonly Uri Rooms = new Uri(@"https://api.chatwork.com/v2/Rooms"); + public static readonly Uri Rooms = new Uri(@"https://api.chatwork.com/v2/rooms"); public static readonly Uri Contacts = new Uri(@"https://api.chatwork.com/v2/contacts"); public static readonly Uri IncomingRequests = new Uri(@"https://api.chatwork.com/v2/incoming_requests"); @@ -60,7 +60,12 @@ public static Uri RoomFiles(long roomId) public static Uri RoomFilesOf(long roomId, long fileId) { - return new Uri($@"https://api.chatwork.com/v 2/rooms/{roomId}/files/{fileId}"); + return new Uri($@"https://api.chatwork.com/v2/rooms/{roomId}/files/{fileId}"); + } + + public static Uri RoomTasksOfStatus(long roomId, long taskId) + { + return new Uri($@"https://api.chatwork.com/v2/rooms/{roomId}/tasks/{taskId}/status"); } public static Uri RoomLink(long roomId) diff --git a/SharpChatwork/src/Client/Query/IncomingRequestQuery.cs b/SharpChatwork/src/Client/Query/IncomingRequestQuery.cs index 71a98f2..6cc6a68 100644 --- a/SharpChatwork/src/Client/Query/IncomingRequestQuery.cs +++ b/SharpChatwork/src/Client/Query/IncomingRequestQuery.cs @@ -10,7 +10,7 @@ internal sealed class IncomingRequestQuery(IChatworkClient client) : ClientQuery { public async ValueTask AcceptAsync(long requestId, CancellationToken token = default) { - return await this.chatworkClient.QueryAsync(EndPoints.IncomingRequestsOf(requestId), HttpMethod.Post, new Dictionary(), token); + return await this.chatworkClient.QueryAsync(EndPoints.IncomingRequestsOf(requestId), HttpMethod.Put, new Dictionary(), token); } public async ValueTask CancelAsync(long requestId, CancellationToken token = default) diff --git a/SharpChatwork/src/Client/Query/RoomFileQuery.cs b/SharpChatwork/src/Client/Query/RoomFileQuery.cs index f74e5bc..b59f338 100644 --- a/SharpChatwork/src/Client/Query/RoomFileQuery.cs +++ b/SharpChatwork/src/Client/Query/RoomFileQuery.cs @@ -20,7 +20,7 @@ public async ValueTask> GetAllAsync(long roomId, long acco public async ValueTask GetAsync(long roomId, long fileId, bool createDownloadLink, CancellationToken token = default) { - var uri = $"{EndPoints.RoomFiles(roomId)}?create_download_url={UrlArgEncoder.BoolToInt(createDownloadLink)}"; + var uri = $"{EndPoints.RoomFilesOf(roomId, fileId)}?create_download_url={UrlArgEncoder.BoolToInt(createDownloadLink)}"; return await this.chatworkClient.QueryAsync(new Uri(uri), HttpMethod.Get, new Dictionary(), token); } public async ValueTask UploadAsync(long roomId, Stream stream, string filePath, string message, CancellationToken token = default) diff --git a/SharpChatwork/src/Client/Query/RoomInviteQuery.cs b/SharpChatwork/src/Client/Query/RoomInviteQuery.cs index 7e00f5f..eecfc09 100644 --- a/SharpChatwork/src/Client/Query/RoomInviteQuery.cs +++ b/SharpChatwork/src/Client/Query/RoomInviteQuery.cs @@ -17,7 +17,7 @@ public async ValueTask CreateAsync(long roomId, string uniqueName, s {"description", description}, {"need_acceptance", UrlArgEncoder.BoolToInt(requireAcceptance).ToString(CultureInfo.InvariantCulture)}, }; - return await this.chatworkClient.QueryAsync(EndPoints.RoomTasks(roomId), HttpMethod.Post, data, token); + return await this.chatworkClient.QueryAsync(EndPoints.RoomLink(roomId), HttpMethod.Post, data, token); } public async ValueTask DestroyAsync(long roomId, CancellationToken token = default) @@ -27,7 +27,7 @@ public async ValueTask DestroyAsync(long roomId, CancellationToken token = defau public async ValueTask GetAsync(long roomId, CancellationToken token = default) { - return await this.chatworkClient.QueryAsync(EndPoints.RoomLink(roomId), HttpMethod.Post, new Dictionary(), token); + return await this.chatworkClient.QueryAsync(EndPoints.RoomLink(roomId), HttpMethod.Get, new Dictionary(), token); } public async ValueTask UpdateAsync(long roomId, string uniqueName, string description, bool requireAcceptance, CancellationToken token = default) @@ -38,6 +38,6 @@ public async ValueTask UpdateAsync(long roomId, string uniqueName, s {"description", description}, {"need_acceptance", UrlArgEncoder.BoolToInt(requireAcceptance).ToString(CultureInfo.InvariantCulture)}, }; - return await this.chatworkClient.QueryAsync(EndPoints.RoomTasks(roomId), HttpMethod.Put, data, token); + return await this.chatworkClient.QueryAsync(EndPoints.RoomLink(roomId), HttpMethod.Put, data, token); } } diff --git a/SharpChatwork/src/Client/Query/RoomMemberQuery.cs b/SharpChatwork/src/Client/Query/RoomMemberQuery.cs index 12a4161..e03fe75 100644 --- a/SharpChatwork/src/Client/Query/RoomMemberQuery.cs +++ b/SharpChatwork/src/Client/Query/RoomMemberQuery.cs @@ -1,5 +1,6 @@ -using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -14,18 +15,19 @@ public async ValueTask> GetAllAsync(long roomId, CancellationT return await this.chatworkClient.QueryAsync>(EndPoints.RoomMember(roomId), HttpMethod.Get, new Dictionary(), token); } - public ValueTask UpdateAsync(long roomId, IEnumerable adminsMembers, IEnumerable normalMembers, IEnumerable readonlyMembers, CancellationToken token = default) + public async ValueTask UpdateAsync(long roomId, IEnumerable adminsMembers, IEnumerable normalMembers, IEnumerable readonlyMembers, CancellationToken token = default) { - throw new NotImplementedException(); - /* - var data = new Dictionary() + var data = new Dictionary { - // TODO convert - // {"members_admin_ids",adminsMembers}, - // {"members_member_ids",roomName }, - // {"members_readonly_ids",preset.ToAliasOrDefault() } + {"members_admin_ids", JoinIds(adminsMembers)}, + {"members_member_ids", JoinIds(normalMembers)}, + {"members_readonly_ids", JoinIds(readonlyMembers)}, }; - return await this.chatworkClient.QueryAsync(EndPoints.RoomMember(roomId), HttpMethod.Get, data); - */ + return await this.chatworkClient.QueryAsync(EndPoints.RoomMember(roomId), HttpMethod.Put, data, token); + } + + private static string JoinIds(IEnumerable ids) + { + return string.Join(",", ids.Select(id => id.ToString(CultureInfo.InvariantCulture))); } } diff --git a/SharpChatwork/src/Client/Query/RoomMessageQuery.cs b/SharpChatwork/src/Client/Query/RoomMessageQuery.cs index f5e7985..b174648 100644 --- a/SharpChatwork/src/Client/Query/RoomMessageQuery.cs +++ b/SharpChatwork/src/Client/Query/RoomMessageQuery.cs @@ -25,8 +25,11 @@ public async ValueTask GetAsync(long roomId, long messageId, Cancel public async ValueTask ReadAsync(long roomId, long messageId, CancellationToken token = default) { - var uri = $"{EndPoints.RoomMessages(roomId)}?message_id={messageId}"; - return await this.chatworkClient.QueryAsync(new Uri(uri), HttpMethod.Post, new Dictionary(), token); + var data = new Dictionary + { + {"message_id", messageId.ToString(CultureInfo.InvariantCulture)}, + }; + return await this.chatworkClient.QueryAsync(EndPoints.RoomMessagesRead(roomId), HttpMethod.Put, data, token); } public async ValueTask RemoveAsync(long roomId, long messageId, CancellationToken token = default) @@ -46,13 +49,19 @@ public async ValueTask SendAsync(long roomId, string message, bool is public async ValueTask UnReadAsync(long roomId, long messageId, CancellationToken token = default) { - var uri = $"{EndPoints.RoomMessages(roomId)}?message_id={messageId}"; - return await this.chatworkClient.QueryAsync(new Uri(uri), HttpMethod.Post, new Dictionary(), token); + var data = new Dictionary + { + {"message_id", messageId.ToString(CultureInfo.InvariantCulture)}, + }; + return await this.chatworkClient.QueryAsync(EndPoints.RoomMessagesUnread(roomId), HttpMethod.Put, data, token); } public async ValueTask UpdateAsync(long roomId, long messageId, string message, CancellationToken token = default) { - var uri = $"{EndPoints.RoomMessagesOf(roomId, messageId)}?body={message}"; - return await this.chatworkClient.QueryAsync(new Uri(uri), HttpMethod.Post, new Dictionary(), token); + var data = new Dictionary + { + {"body", message}, + }; + return await this.chatworkClient.QueryAsync(EndPoints.RoomMessagesOf(roomId, messageId), HttpMethod.Put, data, token); } } diff --git a/SharpChatwork/src/Client/Query/RoomQuery.cs b/SharpChatwork/src/Client/Query/RoomQuery.cs index c397d2c..beb04e9 100644 --- a/SharpChatwork/src/Client/Query/RoomQuery.cs +++ b/SharpChatwork/src/Client/Query/RoomQuery.cs @@ -17,13 +17,16 @@ internal sealed class RoomQuery(IChatworkClient client) : ClientQuery(client), I public async ValueTask CreateAsync(CancellationToken token = default) { - return await this.chatworkClient.QueryAsync(EndPoints.Rooms, HttpMethod.Get, new Dictionary(), token); + return await this.chatworkClient.QueryAsync(EndPoints.Rooms, HttpMethod.Post, new Dictionary(), token); } public async ValueTask DeleteAsync(long roomId, CancellationToken token = default) { - var uri = $"{EndPoints.RoomMessages(roomId)}?action_type=delete"; - await this.chatworkClient.QueryAsync(new Uri(uri), HttpMethod.Post, new Dictionary(), token); + var data = new Dictionary + { + {"action_type", "delete"}, + }; + await this.chatworkClient.QueryAsync(EndPoints.RoomOf(roomId), HttpMethod.Delete, data, token); } public async ValueTask> GetAllAsync(CancellationToken token = default) @@ -38,8 +41,11 @@ public async ValueTask GetAsync(long roomId, CancellationToken token = def public async ValueTask LeaveAsync(long roomId, CancellationToken token = default) { - var uri = $"{EndPoints.RoomMessages(roomId)}?action_type=leave"; - await this.chatworkClient.QueryAsync(new Uri(uri), HttpMethod.Post, new Dictionary(), token); + var data = new Dictionary + { + {"action_type", "leave"}, + }; + await this.chatworkClient.QueryAsync(EndPoints.RoomOf(roomId), HttpMethod.Delete, data, token); } public async ValueTask UpdateAsync( @@ -49,16 +55,10 @@ public async ValueTask UpdateAsync( { var data = new Dictionary { - { - "name", roomName - }, - { - "description", roomName - }, - { - "icon_preset", preset.ToAliasOrDefault() - }, + {"name", roomName}, + {"description", description}, + {"icon_preset", preset.ToAliasOrDefault()}, }; - return await this.chatworkClient.QueryAsync(EndPoints.RoomOf(roomId), HttpMethod.Post, data, token); + return await this.chatworkClient.QueryAsync(EndPoints.RoomOf(roomId), HttpMethod.Put, data, token); } } diff --git a/SharpChatwork/src/Client/Query/RoomTaskQuery.cs b/SharpChatwork/src/Client/Query/RoomTaskQuery.cs index 19fe54a..6147353 100644 --- a/SharpChatwork/src/Client/Query/RoomTaskQuery.cs +++ b/SharpChatwork/src/Client/Query/RoomTaskQuery.cs @@ -30,21 +30,20 @@ public async ValueTask GetAsync(long roomId, long taskId, Cancellation public async ValueTask> GetAllAsync(long roomId, long accountId, long autherId, bool isDone = false, CancellationToken token = default) { - var doneString = "done"; - if(!isDone) - doneString = "open"; - var data = new Dictionary - { - {"account_id", accountId.ToString(CultureInfo.InvariantCulture)}, - {"assigned_by_account_id", autherId.ToString(CultureInfo.InvariantCulture)}, - {"status", doneString}, - }; - return await this.chatworkClient.QueryAsync>(EndPoints.RoomTasks(roomId), HttpMethod.Get, data, token); + var doneString = isDone ? "done" : "open"; + var uri = $"{EndPoints.RoomTasks(roomId)}" + + $"?account_id={accountId.ToString(CultureInfo.InvariantCulture)}" + + $"&assigned_by_account_id={autherId.ToString(CultureInfo.InvariantCulture)}" + + $"&status={doneString}"; + return await this.chatworkClient.QueryAsync>(new Uri(uri), HttpMethod.Get, new Dictionary(), token); } public async ValueTask UpdateAsync(long roomId, long taskId, TaskStateType state, CancellationToken token = default) { - var uri = $"{EndPoints.RoomTasksOf(roomId, taskId)}?body={state.ToAliasOrDefault()}"; - return await this.chatworkClient.QueryAsync(new Uri(uri), HttpMethod.Post, new Dictionary(), token); + var data = new Dictionary + { + {"body", state.ToAliasOrDefault()}, + }; + return await this.chatworkClient.QueryAsync(EndPoints.RoomTasksOfStatus(roomId, taskId), HttpMethod.Put, data, token); } } diff --git a/SharpChatwork/src/Client/QueryResult/Room.cs b/SharpChatwork/src/Client/QueryResult/Room.cs index a3a9ea6..cc5cf60 100644 --- a/SharpChatwork/src/Client/QueryResult/Room.cs +++ b/SharpChatwork/src/Client/QueryResult/Room.cs @@ -18,4 +18,5 @@ public class Room public int task_num { get; set; } = 0; public string icon_path { get; set; } = string.Empty; public int last_update_time { get; set; } = 0; + public string description { get; set; } = string.Empty; } diff --git a/SharpChatwork/src/Client/QueryResult/UserFile.cs b/SharpChatwork/src/Client/QueryResult/UserFile.cs index db124aa..47e33a4 100644 --- a/SharpChatwork/src/Client/QueryResult/UserFile.cs +++ b/SharpChatwork/src/Client/QueryResult/UserFile.cs @@ -10,4 +10,5 @@ public class UserFile public string filename { get; set; } public int filesize { get; set; } public int upload_time { get; set; } + public string download_url { get; set; } } diff --git a/SharpChatwork/src/Client/QueryResult/UserTask.cs b/SharpChatwork/src/Client/QueryResult/UserTask.cs index 37af8ef..ee932f7 100644 --- a/SharpChatwork/src/Client/QueryResult/UserTask.cs +++ b/SharpChatwork/src/Client/QueryResult/UserTask.cs @@ -6,6 +6,7 @@ public class UserTask { public int task_id { get; set; } public Room room { get; set; } + public User account { get; set; } public User assigned_by_account { get; set; } public string message_id { get; set; } public string body { get; set; } diff --git a/docs/NEXT_RELEASE_NOTE.md b/docs/NEXT_RELEASE_NOTE.md index e69de29..658fa7a 100644 --- a/docs/NEXT_RELEASE_NOTE.md +++ b/docs/NEXT_RELEASE_NOTE.md @@ -0,0 +1,34 @@ +## v0.3.0 + +#### EndPoints +- `EndPoints.Rooms` の URL `/Rooms` (大文字) を `/rooms` に修正 +- `EndPoints.RoomFilesOf` の URL に混入していた連続空白 (`v 2`) を `v2` に修正 +- `EndPoints.RoomTasksOfStatus(roomId, taskId)` を新規追加 (PUT `/rooms/{id}/tasks/{tid}/status` 用) + +#### RoomQuery +- `CreateAsync`: GET → POST に修正 +- `UpdateAsync`: POST → PUT に修正、`description` form 値に `roomName` を入れていたバグを修正 +- `DeleteAsync` / `LeaveAsync`: 誤った POST `/rooms/{id}/messages?action_type=...` → 正しい DELETE `/rooms/{id}` + `action_type` form body に修正 + +#### RoomMessageQuery +- `ReadAsync`: POST `/rooms/{id}/messages?message_id=...` → PUT `/rooms/{id}/messages/read` + `message_id` form body +- `UnReadAsync`: POST `/rooms/{id}/messages?message_id=...` → PUT `/rooms/{id}/messages/unread` + `message_id` form body +- `UpdateAsync`: POST `/rooms/{id}/messages/{mid}?body=...` → PUT `/rooms/{id}/messages/{mid}` + `body` form body + +#### RoomTaskQuery +- `GetAllAsync`: フィルタ (`account_id` / `assigned_by_account_id` / `status`) を form body → query string へ移動 +- `UpdateAsync`: POST `/rooms/{id}/tasks/{tid}?body=...` → PUT `/rooms/{id}/tasks/{tid}/status` + `body` form body + +#### RoomFileQuery +- `GetAsync`: URL に `file_id` が含まれていなかったバグを修正 (`EndPoints.RoomFiles(roomId)` → `EndPoints.RoomFilesOf(roomId, fileId)`) + +#### RoomInviteQuery +- `GetAsync`: POST → GET に修正 +- `CreateAsync`: 誤って `EndPoints.RoomTasks(id)` を叩いていた → `EndPoints.RoomLink(id)` に修正 +- `UpdateAsync`: 同上、`EndPoints.RoomTasks(id)` → `EndPoints.RoomLink(id)` + +#### IncomingRequestQuery +- `AcceptAsync`: POST → PUT に修正 + +#### RoomMemberQuery +- `UpdateAsync`: `NotImplementedException` → PUT `/rooms/{id}/members` + `members_admin_ids` / `members_member_ids` / `members_readonly_ids` (long のカンマ区切り) form body を実装 diff --git a/global.json b/global.json index f46882a..fddfa0d 100644 --- a/global.json +++ b/global.json @@ -2,5 +2,8 @@ "sdk": { "version": "10.0.200", "rollForward": "latestFeature" + }, + "test": { + "runner": "Microsoft.Testing.Platform" } } diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100644 index 0000000..9c9734c --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,12 @@ +#!/bin/bash -xe + +cd `dirname $0` && cd ../ + +COVERAGE_FILE="coverage.cobertura.xml" +REPORT_DIR="coverage-report" + +dotnet tool restore +dotnet test --coverage --coverage-output-format cobertura --coverage-output ${COVERAGE_FILE} +dotnet reportgenerator "-reports:**/TestResults/${COVERAGE_FILE}" "-targetdir:${REPORT_DIR}" "-reporttypes:Html;Badges;TextSummary" + +cat ${REPORT_DIR}/Summary.txt