From 4dbd8adbe8442b2bc75e5c7187d526e57de033df Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Thu, 29 Aug 2024 22:25:09 +0700 Subject: [PATCH 01/44] Add one more song to the pool, with 626 tracks in the DB. --- .../Unit/TrackDatabaseServiceTest.cs | 2 +- MSOC.Backend/tracks.db | Bin 143360 -> 147456 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs b/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs index 15f1391..55d285c 100644 --- a/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs @@ -33,6 +33,6 @@ public void AllEntriesExists() var trackDatabase = _fixture.GetService(_testOutputHelper)!; trackDatabase.Database.EnsureCreated(); - Assert.InRange(trackDatabase.Tracks.Count(), 620, 650); + Assert.StrictEqual(626, trackDatabase.Tracks.Count()); } } \ No newline at end of file diff --git a/MSOC.Backend/tracks.db b/MSOC.Backend/tracks.db index 9b85c230c8ddeb751da00e1e79a6e86675045984..52f63696d6ce9bb23716aefb853199aa0083e35d 100644 GIT binary patch delta 563 zcmZp8z|qjaIYFA0m4SglWuk&TkfB%BxiMvdK8HAe6;O~tWipR}4hxXoG_lcbbCbaj zJ~p7JQcM2k2K#ye#-dFDECq~xlUWbkL*BUqS)sR+(yW-5fUnV6ba9^St4 zFk`e2vphq|banwI8|D`bB@-L<6boDOc^HHxsxZC z-x6dpoLsfSXu7>HlgRe-K%N{QFVI|Oz7-7o-TbnAD>e%X%;HllYAJ%)!-&hNbpnhQ u*qy2ib}2)4OCiDttWNz0G{OvFfebHXECS7@6x^a;M)CWHOvwwZdq+y)cu=_VYlV8~`lCIluq_ From 24af638ae2e3f772fe8f5355ce638ec418844d03 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 08:34:24 +0700 Subject: [PATCH 02/44] Add school list CSV to SQLite script for transparency. --- MSOC.Backend/Scripts/upload_school.py | 61 +++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 MSOC.Backend/Scripts/upload_school.py diff --git a/MSOC.Backend/Scripts/upload_school.py b/MSOC.Backend/Scripts/upload_school.py new file mode 100644 index 0000000..d603b85 --- /dev/null +++ b/MSOC.Backend/Scripts/upload_school.py @@ -0,0 +1,61 @@ +import csv +import json +import enum +from http.client import HTTPConnection +from time import sleep + + +class SchoolType(enum.Enum): + HighSchool = 3 + University = 4 + + +def send_request( + name: str, + school_type: SchoolType, +) -> None: + body = { + "name": name, + "type": school_type.value, + } + + headers = {"Content-type": "application/json"} + + http_handler = HTTPConnection("localhost", 5246, timeout=10000) + http_handler.request("POST", "/api/admin/add-school", json.dumps(body), headers) + + print(f"Sent {body} to the database!") + + sleep(1) + + +if __name__ == "__main__": + try: + with ( + open("thpt.csv", "r", encoding="utf-8") as f + ): + csv_file = csv.reader(f) + + # skip header + next(csv_file) + + while fields := next(csv_file): + (_, name) = fields + send_request(name.upper(), SchoolType.HighSchool) + except StopIteration: + pass + + try: + with ( + open("daihoc.csv", "r", encoding="utf-8") as f + ): + csv_file = csv.reader(f) + + # skip header + next(csv_file) + + while fields := next(csv_file): + (_, name) = fields + send_request(name.upper(), SchoolType.University) + except StopIteration: + pass From 5e7f90df59e740124c2e80a5800a9c8feca79915 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 08:34:42 +0700 Subject: [PATCH 03/44] Add school.db database file and test suite. --- .../Unit/SchoolDatabaseServiceTest.cs | 38 ++++++++++++++++++ MSOC.Backend/.gitignore | 7 +++- MSOC.Backend/schools.db | Bin 0 -> 32768 bytes 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs create mode 100644 MSOC.Backend/schools.db diff --git a/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs b/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs new file mode 100644 index 0000000..5686257 --- /dev/null +++ b/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs @@ -0,0 +1,38 @@ +using MSOC.Backend.Service; +using Xunit.Abstractions; +using Xunit.Microsoft.DependencyInjection.Abstracts; + +namespace MSOC.Backend.Tests.Unit; + +public class SchoolDatabaseServiceTest(ITestOutputHelper testOutputHelper, BackendTestBedFixture fixture) + : TestBed(testOutputHelper, fixture) +{ + [Fact] + public void DatabaseInjectionWorks() + { + var schoolDatabase = _fixture.GetService(_testOutputHelper)!; + schoolDatabase.Database.EnsureCreated(); + + Assert.NotNull(schoolDatabase); + } + + [Fact] + public void AllTablesExists() + { + var schoolDatabase = _fixture.GetService(_testOutputHelper)!; + schoolDatabase.Database.EnsureCreated(); + + var _ = schoolDatabase.Schools; + + Assert.True(true); + } + + [Fact] + public void AllEntriesExists() + { + var schoolDatabase = _fixture.GetService(_testOutputHelper)!; + schoolDatabase.Database.EnsureCreated(); + + Assert.StrictEqual(256, schoolDatabase.Schools.Count()); + } +} \ No newline at end of file diff --git a/MSOC.Backend/.gitignore b/MSOC.Backend/.gitignore index 9b800ed..a0e65bf 100644 --- a/MSOC.Backend/.gitignore +++ b/MSOC.Backend/.gitignore @@ -6,9 +6,12 @@ config.json *.db-shm *.db-wal -# We make an exclusion for tracks database. -# Composing this is a real pain in the ass. +# We make an exclusion for tracks and schools database. +# Composing them is a real pain in the ass. !tracks.db +!schools.db # Shhh... Scripts/MSOC raw track list.csv +Scripts/daihoc.csv +Scripts/thpt.csv diff --git a/MSOC.Backend/schools.db b/MSOC.Backend/schools.db new file mode 100644 index 0000000000000000000000000000000000000000..ef9c96f584239de700edf1423778e5c5f2c0a6a4 GIT binary patch literal 32768 zcmeHQdyG`qdA~E~?#%2w9hQAByO*WB!oxfR1nhWr7-q&hk7aje%^KUVfyEFWYhJd& z*ydozvItnjCJ|U6rItmIOTZqO* znVH=?*i`Btmd*-xm-{&9`+nc!yuRNsv@yNw(3rJj{J`GPL)LQRF2n6M?z1ezFc|*& z@VETs#xG|1ANc92`n}$-%vikRm!Zgi7-r}lBm6g!?}wZ4i~iCGXaqC@8Uc-fMnEH= z5zq)|1T+E~fjdWF!1S~(Sl}8tH2T#&V+Z%|!D?>H!Lj{^$M$WT{LhBiU_6?STlr{T zI&MvRcHX|xy<-dU=6t@6d0RU>T_+Z{e#qwY}F$ zW%Kbwe9+3~@>aHxPFt~Db|^m>#dWK9pnr>cX<#swi4Klf{qYeiTFB>8S+t&sXY&hX zYgx$DYvtpc(2#7bd@FzCG2EKmRmeY|Z*qBCSFI|Gzhz5&LuS{b2SyL=8sB#?x$EGe z@dHO3FS=`0b9|?%yY3!>#`-wRxDY)Y|WAtD^{*pvL(8GyS?$UeiZvUqX)-UFJ7{E zl^=S!B0n?mM}KJqGy)m{jetf#BcKt`2xtT}0vZ90fJQ(g@Hs?a7Rxk#+9p5)^m0dj zP1^Y)A4h&2c_NaFbcTN$E`@J~FNR+TkB2kih2d$Tk3xSL`u)%|q5DEzAr|}~_!q(J z!EXi+1vdv{!ARh@fu9HdH1K-h8-ejaKF||z`~SuN*Z#Ns-}WE%KkVP&pW_cV{#)aZ z8?QH>Z+xclk;Z7_?8du%|L*$-U(t8f_mXd~FYEh~Z@Tvr@B7}jz305gyj#8ZdzX4! zJpb+ax#wNab zo;RN~H<_!=9`--%pV<%DTkLn)i)=Sbvz4r!G54?BKXSk6e$oAqd#yX<`q1@Pt~Xpy zySBQLu4S%{Y9lvuW*UoJ`Q$*}+E^$}oQYZa!QyGw#NDPb-@ZP4^F-E4N3#jm%w6i~ zd=i%vRyv349UPpARd_l!WG%8pW6`X2^Gxw9ZZ3G%G)8LfZ7iTM^i#n1Y@#qyns`2I zCDHo}tmhfih~i<`|RMUN>i5Ig;t3R14 zp3Ja?PnpJMS0+nch;LO=lkQfYB(a85_?~AyM;!%1S5_=pJW*g@JYpITOckgMX6eQs zTSYFPo%5t=mnfQdcri;;<*X{H_r&P`;)mS>v`N3 zsanNkY2s?Zij{7>mQAv0hfQOy-3`o;F5S2oW77|r#sd4A-4_tcvW|nc-O!YMHF@hA zjFK$A9A(W1l&yzvUWRn((!{wKo3YCA1pb|mnZ_EspLFpgj9i*HMaC}O zcs}h9rY}gPr)&=tZU3RbhRmkXtC178?1S|Qm3zY9Pvb2 zW?1Jo1@Qo3lCiZJDss3^=U3HDfhF7vL1;m$b1Bv|s)Cw;mv-K&da*+i4s&IIP2W;6 ziilKUs@6voRB)|q67Fh|by)Mms+FqnX44*0t_YPbAr4Zkb+c*%4httJTejsvWpUdZ z`w<(kc+-dyn>tM-Z(feF&P}$Wl^BuDSkWw-{(usGAW5EJ3qPznuJUNBk1Z{j#+dB@ zSs7zLt>=YF(Q!*gwM7AO364~nxEy2c zX%$FF)yH$zkg{fLze1`k7WrO*O}}3On~ha zVBLwSl2_$JHfw`vq@-QSKFow$m7W<8EU=KK3P0Y&Q=&H zp3E{UrUr`YTy3X)k;+>7)SDt&vKXRnTtSCX)uGH{cGd)}H!kK_&wW#a-VQ8EhHu7L z_gAKB9TQPX4_szV_bNw86koQ6kva!h$2|(h0q6xA+QaXyYX#Q4PQ_j#6&2<^3C-Kq zDpwpXev6`P;-rNvQD9A9Ry_!i@Jf_*tWiBAb3h6J!-!-Kq@2~2tYRl_k);x;;_(9O zUZwOA{S20~WIjjZ5o`aF^0U0UheQAoTURPSD@U&!WmZha*v=~bNO-TMn6+FPj|QIp z(!_NP@1=?FrpoD|eVOXClA3LYX<4f5x*jtEOg)hFkvhAVC_RSkff$X6*v})UBw6#_ z3TD;h`U2}*tgum+`zdEog6~+QKvg#>hmrC#7AhUpJppT;Ov~C9DD$b?_9Ut8i|UR& z%#~4Pt@9P0<&0#{&^qTSA!~*}nD-Q0KG!sM)lGk@bK8L^d60Lt&rtxA)rH`4SnQas zl*YJ!5fe+A?_m~prNCN$M@g7X6wg}Zuz7?9>zJipN=x+Zkr3&;tGCWDNjZqM&s36# zDTheV7?sXq9I#Zn5;<2425ec6X*}wx_@YDnlUFT5dKKdn=Dw_@TRDx8dm}jpvQO)b ziavN}sCYbOA!@U%vr8c^M`@8n2*k^*r_&)vAkOm?Z^xn2^bRGsoq$EA?`T&TWdLl9 zvH;D@XjGliro2gJrT%1;!of~=t*uJVWbr)AF)Zhry)CAZu#F$HQ=>&F+A#qHXnw!0 z*)*O4U^HRJjZ;`8;K!RWOdudEC7c@HVp;hw1i&1bBtOO4?ouJ7 z?ql-*T#EHX9880EPFm|}ATBT~ta6X-ZgdY1Aca^7*f7JILn=|A@;?=~Vg-C>Z9&y1 zy|}S}F-okFSZ_etpdyXvQ_LU8kgU_M{3!uJs5g7ez@m+{G%D{T{VqXEvDUGCwycvT z>&Rwuwa1ouDHd*H9BS_b+Kc9_TzVukkczQ6o~mVxa4#CsX(lH|x2xtT}0vZ90fJQ(gpb^jrXaqC@8Uc;K=No~bi*>r? zM=-byTK_+XgYU=(k#{3Mh>k-&q2b%AdG$Nr!C z|IB~Ef6Twtzs`SG<8M&?f2Hx2#>0)7#wCqSzTf!X_x+LYyzjVgr!VJQ?wjWQmG{Tq z>)vmAcY6E0U-0~==bt?9crJJjd)9lJ8vefF`wd@j7(kW(ADG9?d)ZIftL#BG*Zp7a zAGlAu)9$eAnrjSd{2#uy@*NEZM`eAvRIFU80hCbAW7liil|59TnWgd(Dvqxy?U8j- zqGW4ev8Z{Wa@fLc}Ar@}oMLV;fq&6xLa)O|l)?-g0qis>=Y8k~f?hoeCJQ{G245p_7=^!_N{446RpaMsQi z01e>%GFqk{J2Hc=?fT4qAW|K-MmntAJ(EItM>M(v0K- zI4r+kxRPfj2^-d02z$A=FN?}}3_P`FPrR>QHX%MnHI0yo@ zH$^cq>BMGp5Ct6kI-2f_59Y0S3Lk@wZ>*C`(m+Mwv^73I6a$_qGn2P0E~Ru)C-LVlJ}-OfQokTI8x52D^TYb7%9rT8FR5_|)>sUX+W z*~Fh;My>jDIo8U-IFJlJ**y(2SMsclgKwbtSVZ9?3xjZQ7Pr7Skhg5AYB|jyjzKt( zXvPg$N&x0QNWnkOPq%2SlzH{E`GBA4CG_vQ>^R_(JnIs02d}hCvb%^I>}^=r3M?4M8e!upy@Q zAXw6j36a>&!7HfEG6G{7dFFGF3kp;F5?mEp3V#Pw5HXF;z*L|cOa&9G4kRnE3>^HJ z23HZ#og7SqS~O`UW2+7|z&EH(Wpb7vx<(m4bnW7x9Mrml?WDBkIt>xPE~p*v;F;2& zC?R!l&VH*Mlz705Ufe4BI;j9FU$cQhU~B4;9-BE8`c!k5Fl!#$x7 zvDS}=!oi;euLK_nP7nND;Q2t-|B3%y|2O=7{kWJw#MyO#Rs*R8~8EooQCQtLNn|W*80-K>ozr75@sE`T*sfX zxzvuk@%dZVurw3RRISh}ta9^ie$+HZ>xzN>QMoNd&D^*Ocyjy*B=4#J3ikMDIZo$2 z!`LFp(9U6H-`PQh<@`zUc>&IbsXp6TWP$(OAIFY+!raK8fE=^x$x*%e{F%XA!yh+| z!=I(eskTq>!;mNP8D7SxOS}y@?UN=4Jo@<|wA5O+CE9bN&#id9fEBv<5FdP)3{GpD z55H#fgST;ewRd2y;s=%w`4OZyLKl%bPF+Z8T!%3B;k$3+&slN$K?_=^ zj@R<9-R6`^Y^L1ChnMqRw~1-I{avR%SMr^vvA3SG@@&=tq`mx60KU22i&dx5n~Kj7 S)>z+DZnAe?L|(ol7yDn{YVG#` literal 0 HcmV?d00001 From f0cc3edecd4beed4e32de3426122c42b780af554 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 10:39:21 +0700 Subject: [PATCH 04/44] Singleton auto-activation only provide *negligible* performance uplift. So it's fine to remove it. --- MSOC.Backend/MSOC.Backend.csproj | 1 - MSOC.Backend/Program.cs | 5 ----- 2 files changed, 6 deletions(-) diff --git a/MSOC.Backend/MSOC.Backend.csproj b/MSOC.Backend/MSOC.Backend.csproj index b6b153b..c838066 100644 --- a/MSOC.Backend/MSOC.Backend.csproj +++ b/MSOC.Backend/MSOC.Backend.csproj @@ -20,7 +20,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/MSOC.Backend/Program.cs b/MSOC.Backend/Program.cs index 1a7de3a..b5e3bed 100644 --- a/MSOC.Backend/Program.cs +++ b/MSOC.Backend/Program.cs @@ -24,11 +24,6 @@ .AddHttpContextAccessor() .AddSwaggerGen(); -// Enable services at startup time. -builder.Services - .ActivateSingleton() - .ActivateSingleton(); - var app = builder.Build(); // The best testing is to send the request on browser. From 8e2c665a15aa03385149a316124fa37c37be4b0d Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 10:40:29 +0700 Subject: [PATCH 05/44] Add even more tests for TrackController. --- MSOC.Backend.Tests/Api/HealthCheckApiTest.cs | 26 --- MSOC.Backend.Tests/GameApplicationFactory.cs | 44 +++++ MSOC.Backend.Tests/Integration/SmokeTest.cs | 29 ++++ .../Integration/TrackApiTest.cs | 152 ++++++++++++++++++ MSOC.Backend.Tests/MSOC.Backend.Tests.csproj | 1 + ...BedFixture.cs => ServiceTestBedFixture.cs} | 6 +- .../Unit/GameDatabaseServiceTest.cs | 4 +- .../Unit/MaimaiInquiryServiceTest.cs | 4 +- .../Unit/SchoolDatabaseServiceTest.cs | 4 +- .../Unit/TrackDatabaseServiceTest.cs | 4 +- MSOC.Backend/Controller/TrackController.cs | 50 ++++-- 11 files changed, 276 insertions(+), 48 deletions(-) delete mode 100644 MSOC.Backend.Tests/Api/HealthCheckApiTest.cs create mode 100644 MSOC.Backend.Tests/GameApplicationFactory.cs create mode 100644 MSOC.Backend.Tests/Integration/SmokeTest.cs create mode 100644 MSOC.Backend.Tests/Integration/TrackApiTest.cs rename MSOC.Backend.Tests/{BackendTestBedFixture.cs => ServiceTestBedFixture.cs} (90%) diff --git a/MSOC.Backend.Tests/Api/HealthCheckApiTest.cs b/MSOC.Backend.Tests/Api/HealthCheckApiTest.cs deleted file mode 100644 index d041db9..0000000 --- a/MSOC.Backend.Tests/Api/HealthCheckApiTest.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using MSOC.Backend.Controller; - -namespace MSOC.Backend.Tests.Api; - -public class HealthCheckApiTest -{ - /// - /// Smoke test(n): - /// preliminary testing or sanity testing to reveal simple failures severe enough to, - /// for example, reject a prospective software release. - /// - [Fact] - public void ApiSmokeTest() - { - var healthCheckController = new TestController(); - var result = healthCheckController.HealthCheck(); - - Assert.NotNull(result); - Assert.Multiple(() => - { - Assert.IsType(result); - Assert.StrictEqual((result as OkObjectResult)?.Value, "I am OK"); - }); - } -} \ No newline at end of file diff --git a/MSOC.Backend.Tests/GameApplicationFactory.cs b/MSOC.Backend.Tests/GameApplicationFactory.cs new file mode 100644 index 0000000..98261cd --- /dev/null +++ b/MSOC.Backend.Tests/GameApplicationFactory.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MSOC.Backend.Service; + +namespace MSOC.Backend.Tests; + +public class GameApplicationFactory : WebApplicationFactory where TProgram : class +{ + protected override IHost CreateHost(IHostBuilder builder) + { + builder.ConfigureServices(services => + { + var gameDatabase = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); + var trackDatabase = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); + var schoolDatabase = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); + + if (gameDatabase != null) services.Remove(gameDatabase); + if (trackDatabase != null) services.Remove(trackDatabase); + if (schoolDatabase != null) services.Remove(schoolDatabase); + + var path = Directory.CreateDirectory(AppContext.BaseDirectory) + .Parent!.Parent!.Parent!.Parent!.ToString(); + + services.AddDbContext( + o => o.UseSqlite($"Filename={path}/MSOC.Backend/schools.db"), + ServiceLifetime.Transient + ); + + services.AddDbContext( + o => o.UseSqlite($"Filename={path}/MSOC.Backend/tracks.db"), + ServiceLifetime.Transient + ); + + services.AddDbContext( + o => o.UseSqlite("Filename=:memory:"), + ServiceLifetime.Transient + ); + }); + + return base.CreateHost(builder); + } +} \ No newline at end of file diff --git a/MSOC.Backend.Tests/Integration/SmokeTest.cs b/MSOC.Backend.Tests/Integration/SmokeTest.cs new file mode 100644 index 0000000..287ee7f --- /dev/null +++ b/MSOC.Backend.Tests/Integration/SmokeTest.cs @@ -0,0 +1,29 @@ +using System.Net; + +namespace MSOC.Backend.Tests.Integration; + +public class SmokeTest : IClassFixture> +{ + private readonly GameApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public SmokeTest(GameApplicationFactory factory) + { + _factory = factory; + _httpClient = factory.CreateClient(); + } + + /// + /// Smoke test(n): + /// preliminary testing or sanity testing to reveal simple failures severe enough to, + /// for example, reject a prospective software release. + /// + [Fact] + public async Task ApiSmokeTest() + { + var response = await _httpClient.GetAsync("/api/healthcheck"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("I am OK", await response.Content.ReadAsStringAsync()); + } +} \ No newline at end of file diff --git a/MSOC.Backend.Tests/Integration/TrackApiTest.cs b/MSOC.Backend.Tests/Integration/TrackApiTest.cs new file mode 100644 index 0000000..c964e01 --- /dev/null +++ b/MSOC.Backend.Tests/Integration/TrackApiTest.cs @@ -0,0 +1,152 @@ +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using MSOC.Backend.Service; + +namespace MSOC.Backend.Tests.Integration; + +public class TrackApiTest : IClassFixture> +{ + private readonly GameApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public TrackApiTest(GameApplicationFactory factory) + { + _factory = factory; + _httpClient = factory.CreateClient(); + } + + [Theory] + [InlineData(0, 15)] + [InlineData(1, 15.1)] + [InlineData(0, 15.1)] + public async Task TrackSelectInvalidBoundariesFail(double minDiff, double maxDiff) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var trackDatabase = scope.ServiceProvider.GetService()!; + await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.GetAsync($"/api/tracks/select?min_diff={minDiff}&max_diff={maxDiff}"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Equal("[min_diff; max_diff] must be in range of [1.0; 15.0]", content); + } + + [Theory] + [InlineData(15.0, 1.0)] + [InlineData(14.0, 2.0)] + [InlineData(13.0, 3.0)] + [InlineData(12.0, 4.0)] + [InlineData(11.0, 5.0)] + public async Task TrackSelectReversedBoundariesFail(double minDiff, double maxDiff) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var trackDatabase = scope.ServiceProvider.GetService()!; + await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.GetAsync($"/api/tracks/select?min_diff={minDiff}&max_diff={maxDiff}"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Equal("min_diff must be less than or equal to max_diff", content); + } + + [Theory] + [InlineData(1.0, 11.0)] + [InlineData(2.0, 12.0)] + [InlineData(3.0, 13.0)] + [InlineData(4.0, 14.0)] + [InlineData(5.0, 15.0)] + public async Task TrackSelectPass(double minDiff, double maxDiff) + { + using var scope = _factory.Services.CreateScope(); + + var trackDatabase = scope.ServiceProvider.GetService()!; + await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.GetAsync($"/api/tracks/select?min_diff={minDiff}&max_diff={maxDiff}"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotStrictEqual("[]", content); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(627)] + [InlineData(628)] + public async Task TrackMarkFail(int trackId) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var trackDatabase = scope.ServiceProvider.GetService()!; + await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.GetAsync($"/api/tracks/mark?track_id={trackId}&testing=true"); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Equal("ID can only be [1-626]", await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(625)] + [InlineData(626)] + public async Task TrackMarkPass(int trackId) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var trackDatabase = scope.ServiceProvider.GetService()!; + await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.GetAsync($"/api/tracks/mark?track_id={trackId}&testing=true"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotStrictEqual("[]", content); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(627)] + [InlineData(628)] + public async Task TrackGetFail(int trackId) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var trackDatabase = scope.ServiceProvider.GetService()!; + await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.GetAsync($"/api/tracks/get?track_id={trackId}"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Equal("ID can only be [1-626]", content); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(625)] + [InlineData(626)] + public async Task TrackGetPass(int trackId) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var trackDatabase = scope.ServiceProvider.GetService()!; + await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.GetAsync($"/api/tracks/get?track_id={trackId}"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(content); + } +} \ No newline at end of file diff --git a/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj b/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj index e890ec9..3c76328 100644 --- a/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj +++ b/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj @@ -11,6 +11,7 @@ + diff --git a/MSOC.Backend.Tests/BackendTestBedFixture.cs b/MSOC.Backend.Tests/ServiceTestBedFixture.cs similarity index 90% rename from MSOC.Backend.Tests/BackendTestBedFixture.cs rename to MSOC.Backend.Tests/ServiceTestBedFixture.cs index d6c615b..ba297af 100644 --- a/MSOC.Backend.Tests/BackendTestBedFixture.cs +++ b/MSOC.Backend.Tests/ServiceTestBedFixture.cs @@ -7,7 +7,7 @@ namespace MSOC.Backend.Tests; -public class BackendTestBedFixture : TestBedFixture +public class ServiceTestBedFixture : TestBedFixture { protected override void AddServices(IServiceCollection services, IConfiguration? configuration) { @@ -28,7 +28,7 @@ protected override void AddServices(IServiceCollection services, IConfiguration? ); services.AddDbContext( - o => o.UseSqlite($"Filename=:memory:"), + o => o.UseSqlite("Filename=:memory:"), ServiceLifetime.Transient ); } @@ -48,5 +48,5 @@ protected override IEnumerable GetTestAppSettings() protected override ValueTask DisposeAsyncCore() => new(); protected override void AddUserSecrets(IConfigurationBuilder configurationBuilder) - => configurationBuilder.AddUserSecrets(); + => configurationBuilder.AddUserSecrets(); } \ No newline at end of file diff --git a/MSOC.Backend.Tests/Unit/GameDatabaseServiceTest.cs b/MSOC.Backend.Tests/Unit/GameDatabaseServiceTest.cs index 2426386..93a240a 100644 --- a/MSOC.Backend.Tests/Unit/GameDatabaseServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/GameDatabaseServiceTest.cs @@ -4,8 +4,8 @@ namespace MSOC.Backend.Tests.Unit; -public class GameDatabaseServiceTest(ITestOutputHelper testOutputHelper, BackendTestBedFixture fixture) - : TestBed(testOutputHelper, fixture) +public class GameDatabaseServiceTest(ITestOutputHelper testOutputHelper, ServiceTestBedFixture fixture) + : TestBed(testOutputHelper, fixture) { [Fact] public void DatabaseInjectionWorks() diff --git a/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs b/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs index 10e7efa..aaefe78 100644 --- a/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs @@ -5,8 +5,8 @@ namespace MSOC.Backend.Tests.Unit; [CollectionDefinition("Dependency Injection")] -public class MaimaiInquiryServiceTest(ITestOutputHelper testOutputHelper, BackendTestBedFixture fixture) - : TestBed(testOutputHelper, fixture) +public class MaimaiInquiryServiceTest(ITestOutputHelper testOutputHelper, ServiceTestBedFixture fixture) + : TestBed(testOutputHelper, fixture) { [Theory] [InlineData(1234)] diff --git a/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs b/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs index 5686257..6e57286 100644 --- a/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs @@ -4,8 +4,8 @@ namespace MSOC.Backend.Tests.Unit; -public class SchoolDatabaseServiceTest(ITestOutputHelper testOutputHelper, BackendTestBedFixture fixture) - : TestBed(testOutputHelper, fixture) +public class SchoolDatabaseServiceTest(ITestOutputHelper testOutputHelper, ServiceTestBedFixture fixture) + : TestBed(testOutputHelper, fixture) { [Fact] public void DatabaseInjectionWorks() diff --git a/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs b/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs index 55d285c..24f530b 100644 --- a/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs @@ -4,8 +4,8 @@ namespace MSOC.Backend.Tests.Unit; -public class TrackDatabaseServiceTest(ITestOutputHelper testOutputHelper, BackendTestBedFixture fixture) - : TestBed(testOutputHelper, fixture) +public class TrackDatabaseServiceTest(ITestOutputHelper testOutputHelper, ServiceTestBedFixture fixture) + : TestBed(testOutputHelper, fixture) { [Fact] public void DatabaseInjectionWorks() diff --git a/MSOC.Backend/Controller/TrackController.cs b/MSOC.Backend/Controller/TrackController.cs index 8ec3015..4384873 100644 --- a/MSOC.Backend/Controller/TrackController.cs +++ b/MSOC.Backend/Controller/TrackController.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc; -using MSOC.Backend.Database.Models; using MSOC.Backend.Service; namespace MSOC.Backend.Controller; @@ -21,12 +20,15 @@ public IActionResult SelectTracks( [FromQuery(Name = "max_diff")] double maxDiff = 15.0 ) { - if (minDiff > maxDiff) return BadRequest("oi!"); - + if (minDiff > maxDiff) return BadRequest("min_diff must be less than or equal to max_diff"); + if (minDiff < 1 || maxDiff > 15) return BadRequest("[min_diff; max_diff] must be in range of [1.0; 15.0]"); + var foundedTracks = _trackDatabase.Tracks .Where(track => !track.HasBeenPicked) .Where(track => minDiff <= track.Constant && track.Constant <= maxDiff) .ToArray(); + + if (foundedTracks.Length == 0) return NotFound(); Random.Shared.Shuffle(foundedTracks); @@ -34,19 +36,45 @@ public IActionResult SelectTracks( } [HttpGet("mark")] - public IActionResult MarkTrackAsPicked([FromQuery] int trackId) + public IActionResult MarkTrackAsPicked( + [FromQuery(Name = "track_id")] int trackId, + [FromQuery] bool testing = false + ) { + if (trackId is < 1 or > 626) return BadRequest("ID can only be [1-626]"); + var foundedTracks = _trackDatabase.Tracks .Where(track => track.Id == trackId) - .Take(1); + .Take(1) + .ToArray(); + + if (foundedTracks.Length == 0) return NotFound(); - foreach (var track in foundedTracks) - { - track.HasBeenPicked = true; - _trackDatabase.Update(track); - _trackDatabase.SaveChanges(); - } + if (!testing) + foreach (var track in foundedTracks) + { + track.HasBeenPicked = true; + _trackDatabase.Update(track); + _trackDatabase.SaveChanges(); + } return Ok(); } + + [HttpGet("get")] + public IActionResult GetTrack( + [FromQuery(Name = "track_id")] int trackId + ) + { + if (trackId is < 1 or > 626) return BadRequest("ID can only be [1-626]"); + + var foundedTracks = _trackDatabase.Tracks + .Where(track => track.Id == trackId) + .Take(1) + .ToArray(); + + if (foundedTracks.Length == 0) return NotFound(); + + return Ok(foundedTracks[0]); + } } \ No newline at end of file From 65c238ac78d9fbaec1b33092447c268979a21ecd Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 10:43:05 +0700 Subject: [PATCH 06/44] Update dependencies. --- MSOC.Backend.Tests/MSOC.Backend.Tests.csproj | 9 ++++++--- MSOC.Backend/MSOC.Backend.csproj | 14 +++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj b/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj index 3c76328..79507ee 100644 --- a/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj +++ b/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj @@ -10,12 +10,15 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MSOC.Backend/MSOC.Backend.csproj b/MSOC.Backend/MSOC.Backend.csproj index c838066..4e03f95 100644 --- a/MSOC.Backend/MSOC.Backend.csproj +++ b/MSOC.Backend/MSOC.Backend.csproj @@ -8,19 +8,19 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 77fb0eae9d4a537118a120baa0cb7a3d2f729a3b Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 10:45:35 +0700 Subject: [PATCH 07/44] Declutter CI output by reducing the verbosity of `dotnet test`. --- .github/workflows/basic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index 947445f..abba5f6 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -30,4 +30,4 @@ jobs: run: dotnet restore - name: Run tests - run: dotnet test MSOC.Backend.Tests -c Release -v n --nologo --no-restore + run: dotnet test MSOC.Backend.Tests -c Release --nologo --no-restore From fae3de9433d7e0f70167fbea723b9e85bb40e6db Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 11:14:33 +0700 Subject: [PATCH 08/44] Reintroduce Discord ID into Player model. --- MSOC.Backend/Controller/AdminController.cs | 3 +- MSOC.Backend/Controller/PlayerController.cs | 17 ++- .../RequestModel/PlayerRequestModel.cs | 9 +- MSOC.Backend/Database/Models/Player.cs | 7 +- ...830041035_ReintroduceDiscordId.Designer.cs | 142 ++++++++++++++++++ .../20240830041035_ReintroduceDiscordId.cs | 29 ++++ .../GameDatabaseServiceModelSnapshot.cs | 5 +- 7 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 MSOC.Backend/Migrations/20240830041035_ReintroduceDiscordId.Designer.cs create mode 100644 MSOC.Backend/Migrations/20240830041035_ReintroduceDiscordId.cs diff --git a/MSOC.Backend/Controller/AdminController.cs b/MSOC.Backend/Controller/AdminController.cs index 3460e7a..67016db 100644 --- a/MSOC.Backend/Controller/AdminController.cs +++ b/MSOC.Backend/Controller/AdminController.cs @@ -26,7 +26,8 @@ public IActionResult AddPlayer([FromBody] PlayerRequestModel player) { _gameDatabase.Players.Add(new Player { - Id = player.Id, + Id = player.DiscordId, + FriendCode = player.FriendCode, IsLeader = player.IsLeader, Rating = player.Rating, Username = player.Username, diff --git a/MSOC.Backend/Controller/PlayerController.cs b/MSOC.Backend/Controller/PlayerController.cs index 01d2969..54d45ae 100644 --- a/MSOC.Backend/Controller/PlayerController.cs +++ b/MSOC.Backend/Controller/PlayerController.cs @@ -7,22 +7,29 @@ namespace MSOC.Backend.Controller; [Route("api/player")] public class PlayerController : ControllerBase { + private readonly GameDatabaseService _gameDatabase; private readonly MaimaiInquiryService _maimai; - public PlayerController(MaimaiInquiryService maimai) + public PlayerController(MaimaiInquiryService maimai, GameDatabaseService gameDatabase) { _maimai = maimai; + _gameDatabase = gameDatabase; } - [HttpPost("query")] - public async Task QueryUserByFriendCode([FromBody] ulong friendCode) + [HttpGet("query")] + public async Task QueryUserByDiscord( + [FromQuery(Name = "id")] ulong discordId + ) { - var needed = await _maimai.PerformFriendCodeLookupAsync(friendCode); + var player = _gameDatabase.Players.FirstOrDefault(player => player.Id == discordId); + if (player == null) return NotFound(); + + var needed = await _maimai.PerformFriendCodeLookupAsync(player.FriendCode); if (needed.Length == 0) return BadRequest("Unable to retrieve needed information."); var name = needed[0].TextContent; - var rating = needed[1].TextContent; + var rating = needed[1].TextContent; return new JsonResult(new Dictionary { diff --git a/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs b/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs index 26767f0..1c18263 100644 --- a/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs +++ b/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs @@ -5,9 +5,14 @@ namespace MSOC.Backend.Controller.RequestModel; public class PlayerRequestModel { /// - /// maimai Friend code associated with this user. + /// Discord ID associated with this player. /// - public ulong Id { get; set; } + public ulong DiscordId { get; set; } + + /// + /// maimai Friend Code associated with this player. + /// + public ulong FriendCode { get; set; } /// /// maimai username. diff --git a/MSOC.Backend/Database/Models/Player.cs b/MSOC.Backend/Database/Models/Player.cs index ca39242..9a9aea3 100644 --- a/MSOC.Backend/Database/Models/Player.cs +++ b/MSOC.Backend/Database/Models/Player.cs @@ -8,9 +8,14 @@ namespace MSOC.Backend.Database.Models; public class Player { /// - /// maimai Friend code associated with this user. + /// Discord ID associated with this player. /// public ulong Id { get; set; } + + /// + /// maimai Friend code associated with this player. + /// + public ulong FriendCode { get; set; } /// /// maimai username. diff --git a/MSOC.Backend/Migrations/20240830041035_ReintroduceDiscordId.Designer.cs b/MSOC.Backend/Migrations/20240830041035_ReintroduceDiscordId.Designer.cs new file mode 100644 index 0000000..2955e57 --- /dev/null +++ b/MSOC.Backend/Migrations/20240830041035_ReintroduceDiscordId.Designer.cs @@ -0,0 +1,142 @@ +// +using System; +using MSOC.Backend.Service; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MSOC.Backend.Migrations +{ + [DbContext(typeof(GameDatabaseService))] + [Migration("20240830041035_ReintroduceDiscordId")] + partial class ReintroduceDiscordId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FriendCode") + .HasColumnType("INTEGER"); + + b.Property("IsLeader") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("SchoolId") + .HasColumnType("INTEGER"); + + b.Property("TeamId") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TeamId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Score", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateOfAcceptance") + .HasColumnType("TEXT"); + + b.Property("DateOfAdmission") + .HasColumnType("TEXT"); + + b.Property("DxScore1") + .HasColumnType("INTEGER"); + + b.Property("DxScore2") + .HasColumnType("INTEGER"); + + b.Property("IsAccepted") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Sub1") + .HasColumnType("REAL"); + + b.Property("Sub2") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId") + .IsUnique(); + + b.ToTable("Scores"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Team", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Teams"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => + { + b.HasOne("MSOC.Backend.Database.Models.Team", "Team") + .WithMany("Players") + .HasForeignKey("TeamId"); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Score", b => + { + b.HasOne("MSOC.Backend.Database.Models.Player", "Player") + .WithOne("Score") + .HasForeignKey("MSOC.Backend.Database.Models.Score", "PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => + { + b.Navigation("Score"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Team", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MSOC.Backend/Migrations/20240830041035_ReintroduceDiscordId.cs b/MSOC.Backend/Migrations/20240830041035_ReintroduceDiscordId.cs new file mode 100644 index 0000000..ef46e6c --- /dev/null +++ b/MSOC.Backend/Migrations/20240830041035_ReintroduceDiscordId.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MSOC.Backend.Migrations +{ + /// + public partial class ReintroduceDiscordId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FriendCode", + table: "Players", + type: "INTEGER", + nullable: false, + defaultValue: 0ul); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FriendCode", + table: "Players"); + } + } +} diff --git a/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs b/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs index b145218..e79d0d7 100644 --- a/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs +++ b/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs @@ -15,7 +15,7 @@ partial class GameDatabaseServiceModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => { @@ -23,6 +23,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("FriendCode") + .HasColumnType("INTEGER"); + b.Property("IsLeader") .HasColumnType("INTEGER"); From c72a3685182b6dfbe15807a0455e0438384de64f Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 11:45:51 +0700 Subject: [PATCH 09/44] Add maimai avatar URL field. --- .../Unit/MaimaiInquiryServiceTest.cs | 13 +- MSOC.Backend/Database/Models/Player.cs | 6 + ...40830044224_AddMaimaiAvatarUrl.Designer.cs | 147 ++++++++++++++++++ .../20240830044224_AddMaimaiAvatarUrl.cs | 30 ++++ .../GameDatabaseServiceModelSnapshot.cs | 11 +- MSOC.Backend/Service/MaimaiInquiryService.cs | 17 +- 6 files changed, 216 insertions(+), 8 deletions(-) create mode 100644 MSOC.Backend/Migrations/20240830044224_AddMaimaiAvatarUrl.Designer.cs create mode 100644 MSOC.Backend/Migrations/20240830044224_AddMaimaiAvatarUrl.cs diff --git a/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs b/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs index aaefe78..41dd6c7 100644 --- a/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs @@ -1,4 +1,5 @@ -using MSOC.Backend.Service; +using AngleSharp.Html.Dom; +using MSOC.Backend.Service; using Xunit.Abstractions; using Xunit.Microsoft.DependencyInjection.Abstracts; @@ -29,7 +30,10 @@ public async Task ValidFamiliarFriendCodeTest(ulong friendCode) var maimai = _fixture.GetService(_testOutputHelper)!; var result = await maimai.PerformFriendCodeLookupAsync(friendCode); - Assert.StrictEqual(2, result.Length); + Assert.StrictEqual(3, result.Length); + Assert.NotEmpty(result[0].TextContent); + Assert.NotEmpty(result[1].TextContent); + Assert.NotEmpty((result[2] as IHtmlImageElement)!.Source!); } [Theory] @@ -39,6 +43,9 @@ public async Task ValidStrangerFriendCodeTest(ulong friendCode) var maimai = _fixture.GetService(_testOutputHelper)!; var result = await maimai.PerformFriendCodeLookupAsync(friendCode); - Assert.StrictEqual(2, result.Length); + Assert.StrictEqual(3, result.Length); + Assert.NotEmpty(result[0].TextContent); + Assert.NotEmpty(result[1].TextContent); + Assert.NotEmpty((result[2] as IHtmlImageElement)!.Source!); } } \ No newline at end of file diff --git a/MSOC.Backend/Database/Models/Player.cs b/MSOC.Backend/Database/Models/Player.cs index 9a9aea3..5a6fb1c 100644 --- a/MSOC.Backend/Database/Models/Player.cs +++ b/MSOC.Backend/Database/Models/Player.cs @@ -38,6 +38,12 @@ public class Player /// public int SchoolId { get; set; } + /// + /// maimai avatar URL. + /// + [MaxLength(256)] + public string MaimaiAvatarUrl { get; set; } = ""; + /// /// The team this player belongs to. /// diff --git a/MSOC.Backend/Migrations/20240830044224_AddMaimaiAvatarUrl.Designer.cs b/MSOC.Backend/Migrations/20240830044224_AddMaimaiAvatarUrl.Designer.cs new file mode 100644 index 0000000..068f9d2 --- /dev/null +++ b/MSOC.Backend/Migrations/20240830044224_AddMaimaiAvatarUrl.Designer.cs @@ -0,0 +1,147 @@ +// +using System; +using MSOC.Backend.Service; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MSOC.Backend.Migrations +{ + [DbContext(typeof(GameDatabaseService))] + [Migration("20240830044224_AddMaimaiAvatarUrl")] + partial class AddMaimaiAvatarUrl + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FriendCode") + .HasColumnType("INTEGER"); + + b.Property("IsLeader") + .HasColumnType("INTEGER"); + + b.Property("MaimaiAvatarUrl") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("SchoolId") + .HasColumnType("INTEGER"); + + b.Property("TeamId") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TeamId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Score", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateOfAcceptance") + .HasColumnType("TEXT"); + + b.Property("DateOfAdmission") + .HasColumnType("TEXT"); + + b.Property("DxScore1") + .HasColumnType("INTEGER"); + + b.Property("DxScore2") + .HasColumnType("INTEGER"); + + b.Property("IsAccepted") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Sub1") + .HasColumnType("REAL"); + + b.Property("Sub2") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId") + .IsUnique(); + + b.ToTable("Scores"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Team", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Teams"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => + { + b.HasOne("MSOC.Backend.Database.Models.Team", "Team") + .WithMany("Players") + .HasForeignKey("TeamId"); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Score", b => + { + b.HasOne("MSOC.Backend.Database.Models.Player", "Player") + .WithOne("Score") + .HasForeignKey("MSOC.Backend.Database.Models.Score", "PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => + { + b.Navigation("Score"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Team", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MSOC.Backend/Migrations/20240830044224_AddMaimaiAvatarUrl.cs b/MSOC.Backend/Migrations/20240830044224_AddMaimaiAvatarUrl.cs new file mode 100644 index 0000000..43e2bd6 --- /dev/null +++ b/MSOC.Backend/Migrations/20240830044224_AddMaimaiAvatarUrl.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MSOC.Backend.Migrations +{ + /// + public partial class AddMaimaiAvatarUrl : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MaimaiAvatarUrl", + table: "Players", + type: "TEXT", + maxLength: 256, + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MaimaiAvatarUrl", + table: "Players"); + } + } +} diff --git a/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs b/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs index e79d0d7..8c5f4c2 100644 --- a/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs +++ b/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs @@ -29,6 +29,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsLeader") .HasColumnType("INTEGER"); + b.Property("MaimaiAvatarUrl") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + b.Property("Rating") .HasColumnType("INTEGER"); @@ -47,7 +52,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("TeamId"); - b.ToTable("Players"); + b.ToTable("Players", (string)null); }); modelBuilder.Entity("MSOC.Backend.Database.Models.Score", b => @@ -85,7 +90,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("PlayerId") .IsUnique(); - b.ToTable("Scores"); + b.ToTable("Scores", (string)null); }); modelBuilder.Entity("MSOC.Backend.Database.Models.Team", b => @@ -101,7 +106,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Teams"); + b.ToTable("Teams", (string)null); }); modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => diff --git a/MSOC.Backend/Service/MaimaiInquiryService.cs b/MSOC.Backend/Service/MaimaiInquiryService.cs index 88f3ffe..fe00ca0 100644 --- a/MSOC.Backend/Service/MaimaiInquiryService.cs +++ b/MSOC.Backend/Service/MaimaiInquiryService.cs @@ -50,7 +50,20 @@ public async Task PerformFriendCodeLookupAsync(ulong friendCode) var html = await result.Content.ReadAsStreamAsync(); var document = _parser.ParseDocument(html).All; - return document.Where(x => - x is { LocalName: "div", ClassName: "name_block f_l f_16" or "rating_block" }).ToArray(); + try + { + List results = new() + { + document.First(x => x.LocalName == "div" && x.ClassName == "name_block f_l f_16"), + document.First(x => x.LocalName == "div" && x.ClassName == "rating_block"), + document.First(x => x.LocalName == "img" && x.ClassName == "w_112 f_l"), + }; + + return results.ToArray(); + } + catch (InvalidOperationException) + { + return []; + } } } \ No newline at end of file From f07855a4fdd48c0c35d2b425b5714fe3df1ebc1d Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 11:46:49 +0700 Subject: [PATCH 10/44] Change how a player is looked up and saved in the database. --- MSOC.Backend/Controller/AdminController.cs | 21 ++++++++--- MSOC.Backend/Controller/PlayerController.cs | 37 +++++++++---------- .../RequestModel/PlayerRequestModel.cs | 11 ------ 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/MSOC.Backend/Controller/AdminController.cs b/MSOC.Backend/Controller/AdminController.cs index 67016db..2237637 100644 --- a/MSOC.Backend/Controller/AdminController.cs +++ b/MSOC.Backend/Controller/AdminController.cs @@ -1,3 +1,4 @@ +using AngleSharp.Html.Dom; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MSOC.Backend.Controller.RequestModel; @@ -13,27 +14,37 @@ public class AdminController : ControllerBase private readonly GameDatabaseService _gameDatabase; private readonly TrackDatabaseService _trackDatabase; private readonly SchoolDatabaseService _schoolDatabase; + private readonly MaimaiInquiryService _maimai; - public AdminController(GameDatabaseService gameDatabase, TrackDatabaseService trackDatabase, SchoolDatabaseService schoolDatabase) + public AdminController( + GameDatabaseService gameDatabase, + TrackDatabaseService trackDatabase, + SchoolDatabaseService schoolDatabase, + MaimaiInquiryService maimai + ) { _gameDatabase = gameDatabase; _trackDatabase = trackDatabase; _schoolDatabase = schoolDatabase; + _maimai = maimai; } [HttpPost("admin/add-player")] - public IActionResult AddPlayer([FromBody] PlayerRequestModel player) + public async Task AddPlayer([FromBody] PlayerRequestModel player) { + var maiInfo = await _maimai.PerformFriendCodeLookupAsync(player.FriendCode); + _gameDatabase.Players.Add(new Player { Id = player.DiscordId, FriendCode = player.FriendCode, IsLeader = player.IsLeader, - Rating = player.Rating, - Username = player.Username, + Username = maiInfo[0].TextContent, + Rating = Convert.ToInt32(maiInfo[1].TextContent), + MaimaiAvatarUrl = (maiInfo[2] as IHtmlImageElement)!.Source! }); - _gameDatabase.SaveChanges(); + await _gameDatabase.SaveChangesAsync(); return Ok(); } diff --git a/MSOC.Backend/Controller/PlayerController.cs b/MSOC.Backend/Controller/PlayerController.cs index 54d45ae..c5107b7 100644 --- a/MSOC.Backend/Controller/PlayerController.cs +++ b/MSOC.Backend/Controller/PlayerController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using MSOC.Backend.Service; namespace MSOC.Backend.Controller; @@ -8,33 +9,31 @@ namespace MSOC.Backend.Controller; public class PlayerController : ControllerBase { private readonly GameDatabaseService _gameDatabase; - private readonly MaimaiInquiryService _maimai; - public PlayerController(MaimaiInquiryService maimai, GameDatabaseService gameDatabase) + public PlayerController(GameDatabaseService gameDatabase) { - _maimai = maimai; _gameDatabase = gameDatabase; } - [HttpGet("query")] - public async Task QueryUserByDiscord( - [FromQuery(Name = "id")] ulong discordId + [HttpGet("get")] + public IActionResult GetPlayer( + [FromQuery(Name = "id")] ulong lookupKey, + [FromQuery(Name = "type")] string queryType = "friend_code" ) { - var player = _gameDatabase.Players.FirstOrDefault(player => player.Id == discordId); - if (player == null) return NotFound(); + var defaultQuery = _gameDatabase.Players + .Include(p => p.Score) + .Include(p => p.Team); - var needed = await _maimai.PerformFriendCodeLookupAsync(player.FriendCode); - - if (needed.Length == 0) return BadRequest("Unable to retrieve needed information."); + var player = queryType switch + { + "friend_code" => defaultQuery.FirstOrDefault(p => p.FriendCode == lookupKey), + "discord" => defaultQuery.FirstOrDefault(p => p.Id == lookupKey), + _ => null + }; - var name = needed[0].TextContent; - var rating = needed[1].TextContent; + if (player == null) return NotFound(); - return new JsonResult(new Dictionary - { - ["name"] = name, - ["rating"] = rating - }); + return Ok(player); } } \ No newline at end of file diff --git a/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs b/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs index 1c18263..60b1fb1 100644 --- a/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs +++ b/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs @@ -14,17 +14,6 @@ public class PlayerRequestModel /// public ulong FriendCode { get; set; } - /// - /// maimai username. - /// - [MaxLength(16)] - public required string Username { get; set; } - - /// - /// maimai rating. - /// - public int Rating { get; set; } - /// /// Whether this player is a leader for their team. /// From 184c939910fd94a1db41cfd0c34315d0f9408e80 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 13:25:30 +0700 Subject: [PATCH 11/44] Add API documentation, and change method of API admin endpoints to... ... conform with RFC 9110 https://www.rfc-editor.org/rfc/rfc9110.html#name-patch https://www.rfc-editor.org/rfc/rfc9110.html#name-post --- .../Integration/TrackApiTest.cs | 24 ++- MSOC.Backend/Controller/AdminController.cs | 172 ++++++++++-------- .../Controller/LeaderboardController.cs | 16 +- MSOC.Backend/Controller/PlayerController.cs | 11 +- .../RequestModel/PlayerRequestModel.cs | 21 --- .../RequestModel/SchoolRequestModel.cs | 5 +- ...tModel.cs => ScoreAdditionRequestModel.cs} | 20 +- .../RequestModel/TrackAdditionRequestModel.cs | 10 +- .../RequestModel/TrackMarkingRequestModel.cs | 7 + MSOC.Backend/Controller/TestController.cs | 3 + MSOC.Backend/Controller/TrackController.cs | 46 ++--- MSOC.Backend/Database/Models/Player.cs | 6 +- MSOC.Backend/Database/Models/Score.cs | 10 +- MSOC.Backend/Enum/SchoolType.cs | 4 +- MSOC.Backend/MSOC.Backend.csproj | 50 ++--- .../20240829044540_AddSchoolDatabase.cs | 52 +++--- MSOC.Backend/Program.cs | 13 +- MSOC.Backend/Service/GameDatabaseService.cs | 9 +- MSOC.Backend/Service/MaimaiInquiryService.cs | 2 +- MSOC.Backend/Service/SchoolDatabaseService.cs | 7 +- MSOC.Backend/Service/TrackDatabaseService.cs | 3 +- 21 files changed, 264 insertions(+), 227 deletions(-) delete mode 100644 MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs rename MSOC.Backend/Controller/RequestModel/{ScoreRequestModel.cs => ScoreAdditionRequestModel.cs} (59%) create mode 100644 MSOC.Backend/Controller/RequestModel/TrackMarkingRequestModel.cs diff --git a/MSOC.Backend.Tests/Integration/TrackApiTest.cs b/MSOC.Backend.Tests/Integration/TrackApiTest.cs index c964e01..ee6b520 100644 --- a/MSOC.Backend.Tests/Integration/TrackApiTest.cs +++ b/MSOC.Backend.Tests/Integration/TrackApiTest.cs @@ -1,5 +1,8 @@ using System.Net; +using System.Text; +using System.Text.Json; using Microsoft.Extensions.DependencyInjection; +using MSOC.Backend.Controller.RequestModel; using MSOC.Backend.Service; namespace MSOC.Backend.Tests.Integration; @@ -85,7 +88,15 @@ public async Task TrackMarkFail(int trackId) var trackDatabase = scope.ServiceProvider.GetService()!; await trackDatabase.Database.EnsureCreatedAsync(); - var response = await _httpClient.GetAsync($"/api/tracks/mark?track_id={trackId}&testing=true"); + var response = await _httpClient.PatchAsync( + "/api/admin/mark-selected-track", + new StringContent( + JsonSerializer.Serialize(new TrackMarkingRequestModel + { + TrackId = trackId, + Testing = true + }), Encoding.UTF8, "application/json") + ); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.Equal("ID can only be [1-626]", await response.Content.ReadAsStringAsync()); @@ -103,8 +114,17 @@ public async Task TrackMarkPass(int trackId) var trackDatabase = scope.ServiceProvider.GetService()!; await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.PatchAsync( + "/api/admin/mark-selected-track", + new StringContent( + JsonSerializer.Serialize(new TrackMarkingRequestModel + { + TrackId = trackId, + Testing = true + }), Encoding.UTF8, "application/json") + ); - var response = await _httpClient.GetAsync($"/api/tracks/mark?track_id={trackId}&testing=true"); var content = await response.Content.ReadAsStringAsync(); Assert.Equal(HttpStatusCode.OK, response.StatusCode); diff --git a/MSOC.Backend/Controller/AdminController.cs b/MSOC.Backend/Controller/AdminController.cs index 2237637..959456f 100644 --- a/MSOC.Backend/Controller/AdminController.cs +++ b/MSOC.Backend/Controller/AdminController.cs @@ -1,6 +1,5 @@ using AngleSharp.Html.Dom; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using MSOC.Backend.Controller.RequestModel; using MSOC.Backend.Database.Models; using MSOC.Backend.Service; @@ -8,17 +7,17 @@ namespace MSOC.Backend.Controller; [ApiController] -[Route("api")] +[Route("api/admin")] public class AdminController : ControllerBase { private readonly GameDatabaseService _gameDatabase; - private readonly TrackDatabaseService _trackDatabase; - private readonly SchoolDatabaseService _schoolDatabase; private readonly MaimaiInquiryService _maimai; - + private readonly SchoolDatabaseService _schoolDatabase; + private readonly TrackDatabaseService _trackDatabase; + public AdminController( - GameDatabaseService gameDatabase, - TrackDatabaseService trackDatabase, + GameDatabaseService gameDatabase, + TrackDatabaseService trackDatabase, SchoolDatabaseService schoolDatabase, MaimaiInquiryService maimai ) @@ -28,28 +27,48 @@ MaimaiInquiryService maimai _schoolDatabase = schoolDatabase; _maimai = maimai; } - - [HttpPost("admin/add-player")] - public async Task AddPlayer([FromBody] PlayerRequestModel player) + + /// + /// Add score to database. + /// + /// Score object. + [HttpPost("add-score")] + public async Task AddScore([FromBody] ScoreAdditionRequestModel score) { - var maiInfo = await _maimai.PerformFriendCodeLookupAsync(player.FriendCode); - + var maiInfo = await _maimai.PerformFriendCodeLookupAsync(score.FriendCode); + _gameDatabase.Players.Add(new Player { - Id = player.DiscordId, - FriendCode = player.FriendCode, - IsLeader = player.IsLeader, + Id = score.DiscordId, + FriendCode = score.FriendCode, + IsLeader = false, Username = maiInfo[0].TextContent, Rating = Convert.ToInt32(maiInfo[1].TextContent), - MaimaiAvatarUrl = (maiInfo[2] as IHtmlImageElement)!.Source! + MaimaiAvatarUrl = (maiInfo[2] as IHtmlImageElement)!.Source!, + SchoolId = score.SchoolId }); - + + _gameDatabase.Scores.Add(new Score + { + IsAccepted = false, + PlayerId = score.DiscordId, + Sub1 = score.Sub1, + Sub2 = score.Sub2, + DxScore1 = score.DxScore1, + DxScore2 = score.DxScore2, + DateOfAdmission = DateTime.Now + }); + await _gameDatabase.SaveChangesAsync(); return Ok(); } - - [HttpPost("admin/add-school")] + + /// + /// Add a school to database. + /// + /// School object. + [HttpPost("add-school")] public IActionResult AddSchool([FromBody] SchoolRequestModel school) { _schoolDatabase.Schools.Add(new School @@ -57,45 +76,34 @@ public IActionResult AddSchool([FromBody] SchoolRequestModel school) Name = school.Name, Type = school.Type }); - + _schoolDatabase.SaveChanges(); return Ok(); } - - [HttpPost("admin/add-score/")] - public IActionResult AddScore([FromBody] ScoreRequestModel score) - { - _gameDatabase.Scores.Add(new Score - { - IsAccepted = false, - PlayerId = score.PlayerId, - Sub1 = score.Sub1, - Sub2 = score.Sub2, - DxScore1 = score.DxScore1, - DxScore2 = score.DxScore2, - DateOfAdmission = DateTime.Now, - }); - - _gameDatabase.SaveChanges(); - return Ok(); - } - - [HttpPost("admin/add-team")] + /// + /// Add a team to database. + /// + /// Team object. + [HttpPost("add-team")] public IActionResult AddTeam([FromBody] TeamRequestModel team) { _gameDatabase.Teams.Add(new Team { Name = team.Name }); - + _gameDatabase.SaveChanges(); return Ok(); } - - [HttpPost("admin/add-track")] + + /// + /// Add a track to database. + /// + /// Track object. + [HttpPost("add-track")] public IActionResult AddTrack([FromBody] TrackAdditionRequestModel track) { _trackDatabase.Tracks.Add(new Track @@ -109,51 +117,25 @@ public IActionResult AddTrack([FromBody] TrackAdditionRequestModel track) Version = track.Version, Type = track.Type, HasBeenPicked = false - }); - - _trackDatabase.SaveChanges(); + }); - return Ok(); - } - - [HttpGet("admin/bind-team")] - public IActionResult BindTeamToPlayer(int teamId, ulong playerId) - { - Team t = _gameDatabase.Teams.First(t => t.Id == teamId); - Player p = _gameDatabase.Players.First(p => p.Id == playerId); - - t.Players.Add(p); - p.Team = t; - - _gameDatabase.SaveChanges(); + _trackDatabase.SaveChanges(); return Ok(); } - - [HttpGet("admin/bind-school")] - public IActionResult BindSchoolToPlayer(int schoolId, ulong playerId) - { - School s = _schoolDatabase.Schools.First(s => s.Id == schoolId); - Player p = _gameDatabase.Players.First(p => p.Id == playerId); - - p.SchoolId = s.Id; - - _gameDatabase.SaveChanges(); - return Ok(); - } - - [HttpGet("leaderboard/approve")] - public IActionResult ApproveScore(ulong scoreId) + /// + /// Approve an entry on the leaderboard. + /// + /// Score ID. + [HttpPatch("approve-leaderboard")] + [ProducesResponseType(typeof(Score), 200, "application/json")] + public IActionResult ApproveScore( + [FromQuery(Name = "score_id")] ulong scoreId + ) { var scores = _gameDatabase.Scores.Where(score => score.Id == scoreId); - var player = _gameDatabase.Players - .Include(p => p.Team) - .Include(p => p.SchoolId) - .Include(p => p.Score) - .First(p => p.Id == 6969); - foreach (var score in scores) { score.IsAccepted = true; @@ -173,4 +155,34 @@ public IActionResult ApproveScore(ulong scoreId) return Ok(); } + + /// + /// Marks a track as picked. + /// + /// Track marking object. + [HttpPatch("mark-selected-track")] + [ProducesResponseType(typeof(Track), 200, "application/json")] + public IActionResult MarkTrackAsPicked( + [FromBody] TrackMarkingRequestModel trackMark + ) + { + if (trackMark.TrackId is < 1 or > 626) return BadRequest("ID can only be [1-626]"); + + var foundedTracks = _trackDatabase.Tracks + .Where(track => track.Id == trackMark.TrackId) + .Take(1) + .ToArray(); + + if (foundedTracks.Length == 0) return NotFound(); + + if (!trackMark.Testing) + foreach (var track in foundedTracks) + { + track.HasBeenPicked = true; + _trackDatabase.Update(track); + _trackDatabase.SaveChanges(); + } + + return Ok(foundedTracks); + } } \ No newline at end of file diff --git a/MSOC.Backend/Controller/LeaderboardController.cs b/MSOC.Backend/Controller/LeaderboardController.cs index aa251a0..2424ebd 100644 --- a/MSOC.Backend/Controller/LeaderboardController.cs +++ b/MSOC.Backend/Controller/LeaderboardController.cs @@ -1,11 +1,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using MSOC.Backend.Database.Models; using MSOC.Backend.Service; namespace MSOC.Backend.Controller; [ApiController] -[Route("api/game")] +[Route("api/leaderboard")] public class LeaderboardController : ControllerBase { private readonly GameDatabaseService _gameDatabase; @@ -15,7 +16,12 @@ public LeaderboardController(GameDatabaseService gameDatabase) _gameDatabase = gameDatabase; } - [HttpGet("leaderboard/individual")] + /// + /// Get individual leaderboard. Expect shit performance. + /// + /// Page number, starting from 1 + [HttpGet("individual")] + [ProducesResponseType(typeof(IEnumerable), 200, "application/json")] public IActionResult QueryIndividualLeaderboard([FromQuery] int page = 1) { if (page < 1) return BadRequest("Page number must be at least 1"); @@ -36,7 +42,11 @@ public IActionResult QueryIndividualLeaderboard([FromQuery] int page = 1) return Ok(sortedScores); } - [HttpGet("leaderboard/team")] + /// + /// Get team leaderboard. Expect shit performance. + /// + [HttpGet("team")] + [ProducesResponseType(typeof(IEnumerable), 200, "application/json")] public IActionResult QueryTeamLeaderboard() { // Do an update on the entire database. diff --git a/MSOC.Backend/Controller/PlayerController.cs b/MSOC.Backend/Controller/PlayerController.cs index c5107b7..4877c56 100644 --- a/MSOC.Backend/Controller/PlayerController.cs +++ b/MSOC.Backend/Controller/PlayerController.cs @@ -1,5 +1,6 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using MSOC.Backend.Database.Models; using MSOC.Backend.Service; namespace MSOC.Backend.Controller; @@ -15,7 +16,13 @@ public PlayerController(GameDatabaseService gameDatabase) _gameDatabase = gameDatabase; } + /// + /// Get player data. + /// + /// Key index to look up. + /// Type of the key - either "discord" or "friend_code". [HttpGet("get")] + [ProducesResponseType(typeof(Player), 200, "application/json")] public IActionResult GetPlayer( [FromQuery(Name = "id")] ulong lookupKey, [FromQuery(Name = "type")] string queryType = "friend_code" @@ -24,7 +31,7 @@ public IActionResult GetPlayer( var defaultQuery = _gameDatabase.Players .Include(p => p.Score) .Include(p => p.Team); - + var player = queryType switch { "friend_code" => defaultQuery.FirstOrDefault(p => p.FriendCode == lookupKey), diff --git a/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs b/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs deleted file mode 100644 index 60b1fb1..0000000 --- a/MSOC.Backend/Controller/RequestModel/PlayerRequestModel.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace MSOC.Backend.Controller.RequestModel; - -public class PlayerRequestModel -{ - /// - /// Discord ID associated with this player. - /// - public ulong DiscordId { get; set; } - - /// - /// maimai Friend Code associated with this player. - /// - public ulong FriendCode { get; set; } - - /// - /// Whether this player is a leader for their team. - /// - public bool IsLeader { get; set; } -} \ No newline at end of file diff --git a/MSOC.Backend/Controller/RequestModel/SchoolRequestModel.cs b/MSOC.Backend/Controller/RequestModel/SchoolRequestModel.cs index f33ec7d..ed12cff 100644 --- a/MSOC.Backend/Controller/RequestModel/SchoolRequestModel.cs +++ b/MSOC.Backend/Controller/RequestModel/SchoolRequestModel.cs @@ -5,8 +5,7 @@ namespace MSOC.Backend.Controller.RequestModel; public class SchoolRequestModel { - [MaxLength(255)] - public required string Name { get; set; } - + [MaxLength(255)] public required string Name { get; set; } + public SchoolType Type { get; set; } } \ No newline at end of file diff --git a/MSOC.Backend/Controller/RequestModel/ScoreRequestModel.cs b/MSOC.Backend/Controller/RequestModel/ScoreAdditionRequestModel.cs similarity index 59% rename from MSOC.Backend/Controller/RequestModel/ScoreRequestModel.cs rename to MSOC.Backend/Controller/RequestModel/ScoreAdditionRequestModel.cs index f27576c..25f0402 100644 --- a/MSOC.Backend/Controller/RequestModel/ScoreRequestModel.cs +++ b/MSOC.Backend/Controller/RequestModel/ScoreAdditionRequestModel.cs @@ -1,17 +1,22 @@ namespace MSOC.Backend.Controller.RequestModel; -public class ScoreRequestModel +public class ScoreAdditionRequestModel { /// - /// The player ID associated with the scoreboard entry. + /// Discord ID associated with this player. /// - public ulong PlayerId { get; set; } + public ulong DiscordId { get; set; } + + /// + /// maimai Friend Code associated with this player. + /// + public ulong FriendCode { get; set; } /// /// Achievement% score of TRACK 1. /// public double Sub1 { get; set; } - + /// /// DX score of TRACK 1. /// @@ -21,9 +26,14 @@ public class ScoreRequestModel /// Achievement% score of TRACK 2 /// public double Sub2 { get; set; } - + /// /// DX score of TRACK 2. /// public int DxScore2 { get; set; } + + /// + /// School ID. + /// + public int SchoolId { get; set; } } \ No newline at end of file diff --git a/MSOC.Backend/Controller/RequestModel/TrackAdditionRequestModel.cs b/MSOC.Backend/Controller/RequestModel/TrackAdditionRequestModel.cs index 73464f5..dbaa642 100644 --- a/MSOC.Backend/Controller/RequestModel/TrackAdditionRequestModel.cs +++ b/MSOC.Backend/Controller/RequestModel/TrackAdditionRequestModel.cs @@ -5,18 +5,18 @@ namespace MSOC.Backend.Controller.RequestModel; public class TrackAdditionRequestModel { public string Title { get; set; } - + public string Artist { get; set; } public string Category { get; set; } - + public double Constant { get; set; } - + public string CoverImageUrl { get; set; } public TrackDifficulty Difficulty { get; set; } - + public string Version { get; set; } - + public TrackType Type { get; set; } } \ No newline at end of file diff --git a/MSOC.Backend/Controller/RequestModel/TrackMarkingRequestModel.cs b/MSOC.Backend/Controller/RequestModel/TrackMarkingRequestModel.cs new file mode 100644 index 0000000..1c8c99d --- /dev/null +++ b/MSOC.Backend/Controller/RequestModel/TrackMarkingRequestModel.cs @@ -0,0 +1,7 @@ +namespace MSOC.Backend.Controller.RequestModel; + +public class TrackMarkingRequestModel +{ + public int TrackId { get; set; } + public bool Testing { get; set; } +} \ No newline at end of file diff --git a/MSOC.Backend/Controller/TestController.cs b/MSOC.Backend/Controller/TestController.cs index 223762f..012d86a 100644 --- a/MSOC.Backend/Controller/TestController.cs +++ b/MSOC.Backend/Controller/TestController.cs @@ -6,6 +6,9 @@ namespace MSOC.Backend.Controller; [Route("api")] public class TestController : ControllerBase { + /// + /// Send a GET to this. If it returns nothing, pray. + /// [HttpGet("healthcheck")] public IActionResult HealthCheck() { diff --git a/MSOC.Backend/Controller/TrackController.cs b/MSOC.Backend/Controller/TrackController.cs index 4384873..3d298e1 100644 --- a/MSOC.Backend/Controller/TrackController.cs +++ b/MSOC.Backend/Controller/TrackController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using MSOC.Backend.Database.Models; using MSOC.Backend.Service; namespace MSOC.Backend.Controller; @@ -14,7 +15,13 @@ public TrackController(TrackDatabaseService trackDatabase) _trackDatabase = trackDatabase; } + /// + /// Selects 10 random tracks from given difficulty range. + /// + /// Minimum difficulty, at least 1.0 + /// Maximum difficulty, at most 15.0 [HttpGet("select")] + [ProducesResponseType(typeof(IEnumerable), 200, "application/json")] public IActionResult SelectTracks( [FromQuery(Name = "min_diff")] double minDiff = 1.0, [FromQuery(Name = "max_diff")] double maxDiff = 15.0 @@ -22,12 +29,12 @@ public IActionResult SelectTracks( { if (minDiff > maxDiff) return BadRequest("min_diff must be less than or equal to max_diff"); if (minDiff < 1 || maxDiff > 15) return BadRequest("[min_diff; max_diff] must be in range of [1.0; 15.0]"); - + var foundedTracks = _trackDatabase.Tracks .Where(track => !track.HasBeenPicked) .Where(track => minDiff <= track.Constant && track.Constant <= maxDiff) .ToArray(); - + if (foundedTracks.Length == 0) return NotFound(); Random.Shared.Shuffle(foundedTracks); @@ -35,44 +42,23 @@ public IActionResult SelectTracks( return Ok(foundedTracks.Take(10)); } - [HttpGet("mark")] - public IActionResult MarkTrackAsPicked( - [FromQuery(Name = "track_id")] int trackId, - [FromQuery] bool testing = false - ) - { - if (trackId is < 1 or > 626) return BadRequest("ID can only be [1-626]"); - - var foundedTracks = _trackDatabase.Tracks - .Where(track => track.Id == trackId) - .Take(1) - .ToArray(); - - if (foundedTracks.Length == 0) return NotFound(); - - if (!testing) - foreach (var track in foundedTracks) - { - track.HasBeenPicked = true; - _trackDatabase.Update(track); - _trackDatabase.SaveChanges(); - } - - return Ok(); - } - + /// + /// Get a track from given ID. + /// + /// Track ID, within range 1-626. [HttpGet("get")] + [ProducesResponseType(typeof(Track), 200, "application/json")] public IActionResult GetTrack( [FromQuery(Name = "track_id")] int trackId ) { if (trackId is < 1 or > 626) return BadRequest("ID can only be [1-626]"); - + var foundedTracks = _trackDatabase.Tracks .Where(track => track.Id == trackId) .Take(1) .ToArray(); - + if (foundedTracks.Length == 0) return NotFound(); return Ok(foundedTracks[0]); diff --git a/MSOC.Backend/Database/Models/Player.cs b/MSOC.Backend/Database/Models/Player.cs index 5a6fb1c..01442c0 100644 --- a/MSOC.Backend/Database/Models/Player.cs +++ b/MSOC.Backend/Database/Models/Player.cs @@ -11,10 +11,10 @@ public class Player /// Discord ID associated with this player. /// public ulong Id { get; set; } - + /// /// maimai Friend code associated with this player. - /// + /// public ulong FriendCode { get; set; } /// @@ -32,7 +32,7 @@ public class Player /// Whether this player is a leader for their team. /// public bool IsLeader { get; set; } - + /// /// The school this player belongs to. /// diff --git a/MSOC.Backend/Database/Models/Score.cs b/MSOC.Backend/Database/Models/Score.cs index 28cd27a..cfe299e 100644 --- a/MSOC.Backend/Database/Models/Score.cs +++ b/MSOC.Backend/Database/Models/Score.cs @@ -14,7 +14,7 @@ public class Score /// The player associated with this scoreboard entry. /// public Player Player { get; set; } = null!; - + /// /// The player ID associated with the scoreboard entry. /// @@ -24,7 +24,7 @@ public class Score /// Achievement% score of TRACK 1. /// public double Sub1 { get; set; } - + /// /// DX score of TRACK 1. /// @@ -34,7 +34,7 @@ public class Score /// Achievement% score of TRACK 2 /// public double Sub2 { get; set; } - + /// /// DX score of TRACK 2. /// @@ -44,12 +44,12 @@ public class Score /// Get the time when this submission was sent. /// public DateTime DateOfAdmission { get; set; } - + /// /// Get the time when this submission was accepted. /// public DateTime DateOfAcceptance { get; set; } - + /// /// Whether the score is screened. /// diff --git a/MSOC.Backend/Enum/SchoolType.cs b/MSOC.Backend/Enum/SchoolType.cs index 11893b1..e694c68 100644 --- a/MSOC.Backend/Enum/SchoolType.cs +++ b/MSOC.Backend/Enum/SchoolType.cs @@ -9,9 +9,9 @@ public enum SchoolType /// High school, from grade 10 to grade 12. /// HighSchool = 3, - + /// /// University, as in university. /// - University = 4, + University = 4 } \ No newline at end of file diff --git a/MSOC.Backend/MSOC.Backend.csproj b/MSOC.Backend/MSOC.Backend.csproj index 4e03f95..44eb0b8 100644 --- a/MSOC.Backend/MSOC.Backend.csproj +++ b/MSOC.Backend/MSOC.Backend.csproj @@ -1,30 +1,32 @@  - - net8.0 - enable - enable - + + net8.0 + enable + enable + true + 1591 + - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - - - + + + \ No newline at end of file diff --git a/MSOC.Backend/Migrations/SchoolDatabaseServiceMigrations/20240829044540_AddSchoolDatabase.cs b/MSOC.Backend/Migrations/SchoolDatabaseServiceMigrations/20240829044540_AddSchoolDatabase.cs index c1fd226..4187f66 100644 --- a/MSOC.Backend/Migrations/SchoolDatabaseServiceMigrations/20240829044540_AddSchoolDatabase.cs +++ b/MSOC.Backend/Migrations/SchoolDatabaseServiceMigrations/20240829044540_AddSchoolDatabase.cs @@ -1,35 +1,31 @@ -using Microsoft.EntityFrameworkCore.Migrations; +#nullable disable -#nullable disable +using Microsoft.EntityFrameworkCore.Migrations; -namespace MSOC.Backend.Migrations.SchoolDatabaseServiceMigrations +namespace MSOC.Backend.Migrations.SchoolDatabaseServiceMigrations; + +/// +public partial class AddSchoolDatabase : Migration { /// - public partial class AddSchoolDatabase : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Schools", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", maxLength: 255, nullable: false), - Type = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Schools", x => x.Id); - }); - } + migrationBuilder.CreateTable( + "Schools", + table => new + { + Id = table.Column("INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column("TEXT", maxLength: 255, nullable: false), + Type = table.Column("INTEGER", nullable: false) + }, + constraints: table => { table.PrimaryKey("PK_Schools", x => x.Id); }); + } - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Schools"); - } + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + "Schools"); } -} +} \ No newline at end of file diff --git a/MSOC.Backend/Program.cs b/MSOC.Backend/Program.cs index b5e3bed..f0e549e 100644 --- a/MSOC.Backend/Program.cs +++ b/MSOC.Backend/Program.cs @@ -1,5 +1,7 @@ +using System.Reflection; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; using MSOC.Backend.Service; var builder = WebApplication.CreateBuilder(args); @@ -22,7 +24,11 @@ .AddRouting() .AddEndpointsApiExplorer() .AddHttpContextAccessor() - .AddSwaggerGen(); + .AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "MSOC API", Version = "v1" }); + c.IncludeXmlComments(Assembly.GetExecutingAssembly()); + }); var app = builder.Build(); @@ -31,7 +37,10 @@ if (app.Environment.IsDevelopment()) { app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwaggerUI(c => + { + c.DefaultModelsExpandDepth(-1); + }); app.UseDeveloperExceptionPage(); } diff --git a/MSOC.Backend/Service/GameDatabaseService.cs b/MSOC.Backend/Service/GameDatabaseService.cs index d9182dc..72f5ba5 100644 --- a/MSOC.Backend/Service/GameDatabaseService.cs +++ b/MSOC.Backend/Service/GameDatabaseService.cs @@ -1,4 +1,3 @@ -using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using MSOC.Backend.Database.Models; @@ -10,14 +9,14 @@ namespace MSOC.Backend.Service; /// public class GameDatabaseService : DbContext { - public DbSet Players { get; set; } - public DbSet Teams { get; set; } - public DbSet Scores { get; set; } - public GameDatabaseService(DbContextOptions options) : base(options) { } + public DbSet Players { get; set; } + public DbSet Teams { get; set; } + public DbSet Scores { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring( diff --git a/MSOC.Backend/Service/MaimaiInquiryService.cs b/MSOC.Backend/Service/MaimaiInquiryService.cs index fe00ca0..694d466 100644 --- a/MSOC.Backend/Service/MaimaiInquiryService.cs +++ b/MSOC.Backend/Service/MaimaiInquiryService.cs @@ -56,7 +56,7 @@ public async Task PerformFriendCodeLookupAsync(ulong friendCode) { document.First(x => x.LocalName == "div" && x.ClassName == "name_block f_l f_16"), document.First(x => x.LocalName == "div" && x.ClassName == "rating_block"), - document.First(x => x.LocalName == "img" && x.ClassName == "w_112 f_l"), + document.First(x => x.LocalName == "img" && x.ClassName == "w_112 f_l") }; return results.ToArray(); diff --git a/MSOC.Backend/Service/SchoolDatabaseService.cs b/MSOC.Backend/Service/SchoolDatabaseService.cs index c98016c..2d27123 100644 --- a/MSOC.Backend/Service/SchoolDatabaseService.cs +++ b/MSOC.Backend/Service/SchoolDatabaseService.cs @@ -1,4 +1,3 @@ -using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using MSOC.Backend.Database.Models; @@ -10,12 +9,12 @@ namespace MSOC.Backend.Service; /// public class SchoolDatabaseService : DbContext { - public DbSet Schools { get; set; } - public SchoolDatabaseService(DbContextOptions options) : base(options) { } - + + public DbSet Schools { get; set; } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring( diff --git a/MSOC.Backend/Service/TrackDatabaseService.cs b/MSOC.Backend/Service/TrackDatabaseService.cs index 1b47a4c..a7b8af0 100644 --- a/MSOC.Backend/Service/TrackDatabaseService.cs +++ b/MSOC.Backend/Service/TrackDatabaseService.cs @@ -1,4 +1,3 @@ -using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using MSOC.Backend.Database.Models; @@ -13,7 +12,7 @@ public class TrackDatabaseService : DbContext public TrackDatabaseService(DbContextOptions options) : base(options) { } - + public DbSet Tracks { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) From 8893de56e9c89922984794cf78c62c32bb26a029 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 14:13:00 +0700 Subject: [PATCH 12/44] Add MSOC membership levels. :eyes: --- MSOC.Backend/Enum/MembershipLevel.cs | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 MSOC.Backend/Enum/MembershipLevel.cs diff --git a/MSOC.Backend/Enum/MembershipLevel.cs b/MSOC.Backend/Enum/MembershipLevel.cs new file mode 100644 index 0000000..e75a247 --- /dev/null +++ b/MSOC.Backend/Enum/MembershipLevel.cs @@ -0,0 +1,43 @@ +namespace MSOC.Backend.Enum; + +/// +/// MSOC membership tiers, to prove your loyalty to MSOC. +/// +[Flags] +public enum MembershipLevel +{ + /// + /// MSOC + /// + MSOC = 1 << 1, + + /// + /// MSOC PLUS + /// + MSOC_PLUS = MSOC | (1 << 2), + + /// + /// MSOC PLUS+ + /// + MSOC_PLUS_PLUS = MSOC_PLUS | (1 << 3), + + /// + /// MSOC PRO + /// + MSOC_PRO = MSOC_PLUS | (1 << 4), + + /// + /// MSOC ULTIMATE + /// + MSOC_ULTIMATE = MSOC_PRO | (1 << 5), + + /// + /// MSOC UNLIMITED + /// + MSOC_UNLIMITED = MSOC_ULTIMATE | (1 << 6), + + /// + /// MSOC "SUPER DREAM TEAM" + /// + MSOC_SUPER_DREAM_TEAM = MSOC_UNLIMITED | (1 << 7), +} \ No newline at end of file From 6f377f5cd9f56482a112f2405035712c1d2a66d8 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Fri, 30 Aug 2024 14:26:02 +0700 Subject: [PATCH 13/44] Change unit/integration tests to use Microshit-intended way. --- MSOC.Backend.Tests/GameApplicationFactory.cs | 6 +- .../Integration/AdminControllerTest.cs | 195 ++++++++++++++++++ ...TrackApiTest.cs => TrackControllerTest.cs} | 59 +----- MSOC.Backend.Tests/MSOC.Backend.Tests.csproj | 1 - MSOC.Backend.Tests/ServiceTestBedFixture.cs | 52 ----- .../Unit/GameDatabaseServiceTest.cs | 31 --- .../Unit/MaimaiInquiryServiceTest.cs | 26 ++- .../Unit/SchoolDatabaseServiceTest.cs | 27 ++- .../Unit/TrackDatabaseServiceTest.cs | 25 ++- MSOC.Backend/Controller/AdminController.cs | 37 +++- .../RequestModel/PlayerBindingRequestModel.cs | 7 + MSOC.Backend/Database/Models/Player.cs | 6 + 12 files changed, 297 insertions(+), 175 deletions(-) create mode 100644 MSOC.Backend.Tests/Integration/AdminControllerTest.cs rename MSOC.Backend.Tests/Integration/{TrackApiTest.cs => TrackControllerTest.cs} (67%) delete mode 100644 MSOC.Backend.Tests/ServiceTestBedFixture.cs delete mode 100644 MSOC.Backend.Tests/Unit/GameDatabaseServiceTest.cs create mode 100644 MSOC.Backend/Controller/RequestModel/PlayerBindingRequestModel.cs diff --git a/MSOC.Backend.Tests/GameApplicationFactory.cs b/MSOC.Backend.Tests/GameApplicationFactory.cs index 98261cd..b30c904 100644 --- a/MSOC.Backend.Tests/GameApplicationFactory.cs +++ b/MSOC.Backend.Tests/GameApplicationFactory.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using MSOC.Backend.Service; @@ -34,9 +35,12 @@ protected override IHost CreateHost(IHostBuilder builder) ); services.AddDbContext( - o => o.UseSqlite("Filename=:memory:"), + o => o.UseSqlite($"Filename={path}/MSOC.Backend/MSOC.Test.db"), ServiceLifetime.Transient ); + + // services.AddSingleton(); + services.AddTransient(); }); return base.CreateHost(builder); diff --git a/MSOC.Backend.Tests/Integration/AdminControllerTest.cs b/MSOC.Backend.Tests/Integration/AdminControllerTest.cs new file mode 100644 index 0000000..034036a --- /dev/null +++ b/MSOC.Backend.Tests/Integration/AdminControllerTest.cs @@ -0,0 +1,195 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using MSOC.Backend.Controller.RequestModel; +using MSOC.Backend.Service; + +namespace MSOC.Backend.Tests.Integration; + +public class AdminControllerTest : IClassFixture> +{ + private readonly GameApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public AdminControllerTest(GameApplicationFactory factory) + { + _factory = factory; + _httpClient = factory.CreateClient(); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(627)] + [InlineData(628)] + public async Task TrackMarkFail(int trackId) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var trackDatabase = scope.ServiceProvider.GetService()!; + await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.PatchAsync( + "/api/admin/mark-selected-track", + new StringContent( + JsonSerializer.Serialize(new TrackMarkingRequestModel + { + TrackId = trackId, + Testing = true + }), Encoding.UTF8, "application/json") + ); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Equal("ID can only be [1-626]", await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(625)] + [InlineData(626)] + public async Task TrackMarkPass(int trackId) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var trackDatabase = scope.ServiceProvider.GetService()!; + await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.PatchAsync( + "/api/admin/mark-selected-track", + new StringContent( + JsonSerializer.Serialize(new TrackMarkingRequestModel + { + TrackId = trackId, + Testing = true + }), Encoding.UTF8, "application/json") + ); + + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotStrictEqual("[]", content); + } + + [Fact] + public async Task PlayerAndScoreAdditionTest() + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var gameDatabase = scope.ServiceProvider.GetService()!; + await gameDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.PostAsync( + "/api/admin/add-score", + new StringContent( + JsonSerializer.Serialize(new ScoreAdditionRequestModel + { + // @hidr0 on Discord, *mostly* real data. + DiscordId = 317587311279734784, + FriendCode = 8090803305987, + Sub1 = 101.0000, + DxScore1 = 6969, + Sub2 = 101.0000, + DxScore2 = 7270, + SchoolId = 237 + }), Encoding.UTF8, "application/json") + ); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var playerByDiscord = gameDatabase.Players.FirstOrDefault(p => p.Id == 317587311279734784); + var playerByFriendCode = gameDatabase.Players.FirstOrDefault(p => p.FriendCode == 8090803305987); + + Assert.NotNull(playerByDiscord); + Assert.NotNull(playerByFriendCode); + + await gameDatabase.Database.EnsureDeletedAsync(); + } + + [Fact] + public async Task TeamAdditionTest() + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var gameDatabase = scope.ServiceProvider.GetService()!; + await gameDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.PostAsync( + "/api/admin/add-team", + new StringContent( + JsonSerializer.Serialize(new TeamRequestModel + { + Name = "MSOC" + }), Encoding.UTF8, "application/json") + ); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var team = gameDatabase.Teams.FirstOrDefault(t => t.Name == "MSOC"); + + Assert.NotNull(team); + + await gameDatabase.Database.EnsureDeletedAsync(); + } + + [Fact] + public async Task PlayerAndTeamBindingTest() + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var gameDatabase = scope.ServiceProvider.GetService()!; + await gameDatabase.Database.EnsureCreatedAsync(); + + await _httpClient.PostAsync( + "/api/admin/add-score", + new StringContent( + JsonSerializer.Serialize(new ScoreAdditionRequestModel + { + // @hidr0 on Discord, *mostly* real data. + DiscordId = 317587311279734784, + FriendCode = 8090803305987, + Sub1 = 101.0000, + DxScore1 = 6969, + Sub2 = 101.0000, + DxScore2 = 7270, + SchoolId = 237 + }), Encoding.UTF8, "application/json") + ); + + await _httpClient.PostAsync( + "/api/admin/add-team", + new StringContent( + JsonSerializer.Serialize(new TeamRequestModel + { + Name = "MSOC" + }), Encoding.UTF8, "application/json") + ); + + var player = gameDatabase.Players.FirstOrDefault(p => p.FriendCode == 8090803305987); + var team = gameDatabase.Teams.FirstOrDefault(t => t.Name == "MSOC"); + + Assert.NotNull(player); + Assert.NotNull(team); + + var response = await _httpClient.PatchAsync( + "/api/admin/bind-player-to-team", + new StringContent( + JsonSerializer.Serialize(new PlayerBindingRequestModel + { + PlayerId = player.Id, + TeamId = team.Id + }), Encoding.UTF8, "application/json") + ); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var editedTeam = gameDatabase.Teams.FirstOrDefault(t => t.Name == "MSOC"); + + Assert.NotNull(editedTeam); + Assert.StrictEqual(1, editedTeam.Players.Count); + + await gameDatabase.Database.EnsureDeletedAsync(); + } +} \ No newline at end of file diff --git a/MSOC.Backend.Tests/Integration/TrackApiTest.cs b/MSOC.Backend.Tests/Integration/TrackControllerTest.cs similarity index 67% rename from MSOC.Backend.Tests/Integration/TrackApiTest.cs rename to MSOC.Backend.Tests/Integration/TrackControllerTest.cs index ee6b520..21e8317 100644 --- a/MSOC.Backend.Tests/Integration/TrackApiTest.cs +++ b/MSOC.Backend.Tests/Integration/TrackControllerTest.cs @@ -7,12 +7,12 @@ namespace MSOC.Backend.Tests.Integration; -public class TrackApiTest : IClassFixture> +public class TrackControllerTest : IClassFixture> { private readonly GameApplicationFactory _factory; private readonly HttpClient _httpClient; - public TrackApiTest(GameApplicationFactory factory) + public TrackControllerTest(GameApplicationFactory factory) { _factory = factory; _httpClient = factory.CreateClient(); @@ -76,61 +76,6 @@ public async Task TrackSelectPass(double minDiff, double maxDiff) Assert.NotStrictEqual("[]", content); } - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(627)] - [InlineData(628)] - public async Task TrackMarkFail(int trackId) - { - await using var scope = _factory.Services.CreateAsyncScope(); - - var trackDatabase = scope.ServiceProvider.GetService()!; - await trackDatabase.Database.EnsureCreatedAsync(); - - var response = await _httpClient.PatchAsync( - "/api/admin/mark-selected-track", - new StringContent( - JsonSerializer.Serialize(new TrackMarkingRequestModel - { - TrackId = trackId, - Testing = true - }), Encoding.UTF8, "application/json") - ); - - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - Assert.Equal("ID can only be [1-626]", await response.Content.ReadAsStringAsync()); - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(3)] - [InlineData(625)] - [InlineData(626)] - public async Task TrackMarkPass(int trackId) - { - await using var scope = _factory.Services.CreateAsyncScope(); - - var trackDatabase = scope.ServiceProvider.GetService()!; - await trackDatabase.Database.EnsureCreatedAsync(); - - var response = await _httpClient.PatchAsync( - "/api/admin/mark-selected-track", - new StringContent( - JsonSerializer.Serialize(new TrackMarkingRequestModel - { - TrackId = trackId, - Testing = true - }), Encoding.UTF8, "application/json") - ); - - var content = await response.Content.ReadAsStringAsync(); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotStrictEqual("[]", content); - } - [Theory] [InlineData(-1)] [InlineData(0)] diff --git a/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj b/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj index 79507ee..d19993f 100644 --- a/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj +++ b/MSOC.Backend.Tests/MSOC.Backend.Tests.csproj @@ -18,7 +18,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MSOC.Backend.Tests/ServiceTestBedFixture.cs b/MSOC.Backend.Tests/ServiceTestBedFixture.cs deleted file mode 100644 index ba297af..0000000 --- a/MSOC.Backend.Tests/ServiceTestBedFixture.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using MSOC.Backend.Service; -using Xunit.Microsoft.DependencyInjection; -using Xunit.Microsoft.DependencyInjection.Abstracts; - -namespace MSOC.Backend.Tests; - -public class ServiceTestBedFixture : TestBedFixture -{ - protected override void AddServices(IServiceCollection services, IConfiguration? configuration) - { - var path = Directory.CreateDirectory(AppContext.BaseDirectory) - .Parent!.Parent!.Parent!.Parent!.ToString(); - - services.AddSingleton(configuration!); - services.AddSingleton(); - - services.AddDbContext( - o => o.UseSqlite($"Filename={path}/MSOC.Backend/schools.db"), - ServiceLifetime.Transient - ); - - services.AddDbContext( - o => o.UseSqlite($"Filename={path}/MSOC.Backend/tracks.db"), - ServiceLifetime.Transient - ); - - services.AddDbContext( - o => o.UseSqlite("Filename=:memory:"), - ServiceLifetime.Transient - ); - } - - protected override IEnumerable GetTestAppSettings() - { - var path = Directory.CreateDirectory(AppContext.BaseDirectory) - .Parent!.Parent!.Parent!.Parent!.ToString(); - - yield return new TestAppSettings - { - Filename = $"{path}/MSOC.Backend/config.json", - IsOptional = false - }; - } - - protected override ValueTask DisposeAsyncCore() => new(); - - protected override void AddUserSecrets(IConfigurationBuilder configurationBuilder) - => configurationBuilder.AddUserSecrets(); -} \ No newline at end of file diff --git a/MSOC.Backend.Tests/Unit/GameDatabaseServiceTest.cs b/MSOC.Backend.Tests/Unit/GameDatabaseServiceTest.cs deleted file mode 100644 index 93a240a..0000000 --- a/MSOC.Backend.Tests/Unit/GameDatabaseServiceTest.cs +++ /dev/null @@ -1,31 +0,0 @@ -using MSOC.Backend.Service; -using Xunit.Abstractions; -using Xunit.Microsoft.DependencyInjection.Abstracts; - -namespace MSOC.Backend.Tests.Unit; - -public class GameDatabaseServiceTest(ITestOutputHelper testOutputHelper, ServiceTestBedFixture fixture) - : TestBed(testOutputHelper, fixture) -{ - [Fact] - public void DatabaseInjectionWorks() - { - var gameDatabase = _fixture.GetService(_testOutputHelper)!; - gameDatabase.Database.EnsureCreated(); - - Assert.NotNull(gameDatabase); - } - - [Fact] - public void AllTablesExists() - { - var gameDatabase = _fixture.GetService(_testOutputHelper)!; - gameDatabase.Database.EnsureCreated(); - - var players = gameDatabase.Players; - var teams = gameDatabase.Teams; - var scores = gameDatabase.Scores; - - Assert.True(true); - } -} \ No newline at end of file diff --git a/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs b/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs index 41dd6c7..619872e 100644 --- a/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs @@ -1,21 +1,27 @@ using AngleSharp.Html.Dom; +using Microsoft.Extensions.DependencyInjection; using MSOC.Backend.Service; -using Xunit.Abstractions; -using Xunit.Microsoft.DependencyInjection.Abstracts; namespace MSOC.Backend.Tests.Unit; -[CollectionDefinition("Dependency Injection")] -public class MaimaiInquiryServiceTest(ITestOutputHelper testOutputHelper, ServiceTestBedFixture fixture) - : TestBed(testOutputHelper, fixture) +public class MaimaiInquiryServiceTest : IClassFixture> { + private readonly GameApplicationFactory _factory; + + public MaimaiInquiryServiceTest(GameApplicationFactory factory) + { + _factory = factory; + } + [Theory] [InlineData(1234)] [InlineData(69420)] [InlineData(177013)] public async Task InvalidFriendCodeTest(ulong friendCode) { - var maimai = _fixture.GetService(_testOutputHelper)!; + using var scope = _factory.Services.CreateAsyncScope(); + + var maimai = scope.ServiceProvider.GetService()!; var result = await maimai.PerformFriendCodeLookupAsync(friendCode); Assert.StrictEqual(0, result.Length); @@ -27,7 +33,9 @@ public async Task InvalidFriendCodeTest(ulong friendCode) [InlineData(9020119099087)] public async Task ValidFamiliarFriendCodeTest(ulong friendCode) { - var maimai = _fixture.GetService(_testOutputHelper)!; + using var scope = _factory.Services.CreateAsyncScope(); + + var maimai = scope.ServiceProvider.GetService()!; var result = await maimai.PerformFriendCodeLookupAsync(friendCode); Assert.StrictEqual(3, result.Length); @@ -40,7 +48,9 @@ public async Task ValidFamiliarFriendCodeTest(ulong friendCode) [InlineData(8069933165057)] public async Task ValidStrangerFriendCodeTest(ulong friendCode) { - var maimai = _fixture.GetService(_testOutputHelper)!; + using var scope = _factory.Services.CreateAsyncScope(); + + var maimai = scope.ServiceProvider.GetService()!; var result = await maimai.PerformFriendCodeLookupAsync(friendCode); Assert.StrictEqual(3, result.Length); diff --git a/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs b/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs index 6e57286..f824617 100644 --- a/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/SchoolDatabaseServiceTest.cs @@ -1,16 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; using MSOC.Backend.Service; -using Xunit.Abstractions; -using Xunit.Microsoft.DependencyInjection.Abstracts; namespace MSOC.Backend.Tests.Unit; -public class SchoolDatabaseServiceTest(ITestOutputHelper testOutputHelper, ServiceTestBedFixture fixture) - : TestBed(testOutputHelper, fixture) +public class SchoolDatabaseServiceTest : IClassFixture> { + private readonly GameApplicationFactory _factory; + + public SchoolDatabaseServiceTest(GameApplicationFactory factory) + { + _factory = factory; + } + [Fact] public void DatabaseInjectionWorks() { - var schoolDatabase = _fixture.GetService(_testOutputHelper)!; + using var scope = _factory.Services.CreateScope(); + var schoolDatabase = scope.ServiceProvider.GetService()!; + schoolDatabase.Database.EnsureCreated(); Assert.NotNull(schoolDatabase); @@ -18,9 +25,9 @@ public void DatabaseInjectionWorks() [Fact] public void AllTablesExists() - { - var schoolDatabase = _fixture.GetService(_testOutputHelper)!; - schoolDatabase.Database.EnsureCreated(); + { + using var scope = _factory.Services.CreateScope(); + var schoolDatabase = scope.ServiceProvider.GetService()!; var _ = schoolDatabase.Schools; @@ -30,8 +37,8 @@ public void AllTablesExists() [Fact] public void AllEntriesExists() { - var schoolDatabase = _fixture.GetService(_testOutputHelper)!; - schoolDatabase.Database.EnsureCreated(); + using var scope = _factory.Services.CreateScope(); + var schoolDatabase = scope.ServiceProvider.GetService()!; Assert.StrictEqual(256, schoolDatabase.Schools.Count()); } diff --git a/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs b/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs index 24f530b..37f7705 100644 --- a/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/TrackDatabaseServiceTest.cs @@ -1,16 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; using MSOC.Backend.Service; -using Xunit.Abstractions; -using Xunit.Microsoft.DependencyInjection.Abstracts; namespace MSOC.Backend.Tests.Unit; -public class TrackDatabaseServiceTest(ITestOutputHelper testOutputHelper, ServiceTestBedFixture fixture) - : TestBed(testOutputHelper, fixture) +public class TrackDatabaseServiceTest : IClassFixture> { + private readonly GameApplicationFactory _factory; + + public TrackDatabaseServiceTest(GameApplicationFactory factory) + { + _factory = factory; + } + [Fact] public void DatabaseInjectionWorks() { - var trackDatabase = _fixture.GetService(_testOutputHelper)!; + using var scope = _factory.Services.CreateScope(); + var trackDatabase = scope.ServiceProvider.GetService()!; + trackDatabase.Database.EnsureCreated(); Assert.NotNull(trackDatabase); @@ -19,8 +26,8 @@ public void DatabaseInjectionWorks() [Fact] public void AllTablesExists() { - var trackDatabase = _fixture.GetService(_testOutputHelper)!; - trackDatabase.Database.EnsureCreated(); + using var scope = _factory.Services.CreateScope(); + var trackDatabase = scope.ServiceProvider.GetService()!; var _ = trackDatabase.Tracks; @@ -30,8 +37,8 @@ public void AllTablesExists() [Fact] public void AllEntriesExists() { - var trackDatabase = _fixture.GetService(_testOutputHelper)!; - trackDatabase.Database.EnsureCreated(); + using var scope = _factory.Services.CreateScope(); + var trackDatabase = scope.ServiceProvider.GetService()!; Assert.StrictEqual(626, trackDatabase.Tracks.Count()); } diff --git a/MSOC.Backend/Controller/AdminController.cs b/MSOC.Backend/Controller/AdminController.cs index 959456f..af9c214 100644 --- a/MSOC.Backend/Controller/AdminController.cs +++ b/MSOC.Backend/Controller/AdminController.cs @@ -124,24 +124,50 @@ public IActionResult AddTrack([FromBody] TrackAdditionRequestModel track) return Ok(); } + /// + /// Bind a player to a team. + /// + /// Bind object. + [HttpPatch("bind-player-to-team")] + public IActionResult BindPlayerToTeam( + [FromBody] PlayerBindingRequestModel bind + ) + { + var player = _gameDatabase.Players.FirstOrDefault(p => p.Id == bind.PlayerId); + var team = _gameDatabase.Teams.FirstOrDefault(t => t.Id == bind.TeamId); + + if (player is null || team is null) return NotFound(); + + player.Team = team; + + // EF Core implicitly did this for me. + // team.Players.Add(player); + + _gameDatabase.SaveChanges(); + + return Ok(); + } + /// /// Approve an entry on the leaderboard. /// /// Score ID. [HttpPatch("approve-leaderboard")] - [ProducesResponseType(typeof(Score), 200, "application/json")] public IActionResult ApproveScore( [FromQuery(Name = "score_id")] ulong scoreId ) { - var scores = _gameDatabase.Scores.Where(score => score.Id == scoreId); + var scores = _gameDatabase.Scores.Where(score => score.Id == scoreId).ToArray(); + + if (scores.Length == 0) return NotFound(); foreach (var score in scores) { score.IsAccepted = true; score.DateOfAcceptance = DateTime.Now; + } - + // Do an update on the entire database. var _ = _gameDatabase.Scores .Where(score => score.IsAccepted) @@ -152,7 +178,7 @@ public IActionResult ApproveScore( // TODO: Hit the SignalR endpoint to yell at the front end. _gameDatabase.SaveChanges(); - + return Ok(); } @@ -161,7 +187,6 @@ public IActionResult ApproveScore( /// /// Track marking object. [HttpPatch("mark-selected-track")] - [ProducesResponseType(typeof(Track), 200, "application/json")] public IActionResult MarkTrackAsPicked( [FromBody] TrackMarkingRequestModel trackMark ) @@ -183,6 +208,6 @@ [FromBody] TrackMarkingRequestModel trackMark _trackDatabase.SaveChanges(); } - return Ok(foundedTracks); + return Ok(); } } \ No newline at end of file diff --git a/MSOC.Backend/Controller/RequestModel/PlayerBindingRequestModel.cs b/MSOC.Backend/Controller/RequestModel/PlayerBindingRequestModel.cs new file mode 100644 index 0000000..213aa12 --- /dev/null +++ b/MSOC.Backend/Controller/RequestModel/PlayerBindingRequestModel.cs @@ -0,0 +1,7 @@ +namespace MSOC.Backend.Controller.RequestModel; + +public class PlayerBindingRequestModel +{ + public ulong PlayerId { get; set; } + public int TeamId { get; set; } +} \ No newline at end of file diff --git a/MSOC.Backend/Database/Models/Player.cs b/MSOC.Backend/Database/Models/Player.cs index 01442c0..d0885b4 100644 --- a/MSOC.Backend/Database/Models/Player.cs +++ b/MSOC.Backend/Database/Models/Player.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using MSOC.Backend.Enum; namespace MSOC.Backend.Database.Models; @@ -53,4 +54,9 @@ public class Player /// The score associated with this player. /// public Score? Score { get; set; } + + /// + /// MSOC membership level. + /// + public MembershipLevel MembershipLevel { get; set; } = MembershipLevel.MSOC; } \ No newline at end of file From b2f4f42558ba078fd0f3eee4ba776e3d4540efa9 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 31 Aug 2024 12:43:54 +0700 Subject: [PATCH 14/44] Fix entity pinning in `AdminControllerTest::PlayerAndTeamBindingTest`. --- .../Integration/AdminControllerTest.cs | 48 ++++++++++++++----- MSOC.Backend/Controller/AdminController.cs | 8 +--- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/MSOC.Backend.Tests/Integration/AdminControllerTest.cs b/MSOC.Backend.Tests/Integration/AdminControllerTest.cs index 034036a..7b0163c 100644 --- a/MSOC.Backend.Tests/Integration/AdminControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/AdminControllerTest.cs @@ -1,13 +1,15 @@ using System.Net; using System.Text; using System.Text.Json; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using MSOC.Backend.Controller.RequestModel; using MSOC.Backend.Service; +[assembly: CollectionBehavior(DisableTestParallelization = true)] namespace MSOC.Backend.Tests.Integration; -public class AdminControllerTest : IClassFixture> +public class AdminControllerTest : IClassFixture>, IDisposable { private readonly GameApplicationFactory _factory; private readonly HttpClient _httpClient; @@ -16,6 +18,21 @@ public AdminControllerTest(GameApplicationFactory factory) { _factory = factory; _httpClient = factory.CreateClient(); + + // ensure the game database is nuked. + // so we start fresh everytime. + using var scope = _factory.Services.CreateScope(); + var gameDatabase = scope.ServiceProvider.GetService()!; + gameDatabase.Database.EnsureDeleted(); + } + + public void Dispose() + { + // ensure the game database is nuked. + // so we start fresh everytime. + using var scope = _factory.Services.CreateScope(); + var gameDatabase = scope.ServiceProvider.GetService()!; + gameDatabase.Database.EnsureDeleted(); } [Theory] @@ -99,11 +116,21 @@ public async Task PlayerAndScoreAdditionTest() Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var playerByDiscord = gameDatabase.Players.FirstOrDefault(p => p.Id == 317587311279734784); - var playerByFriendCode = gameDatabase.Players.FirstOrDefault(p => p.FriendCode == 8090803305987); + var playerByDiscord = gameDatabase.Players + .Include(p => p.Score) + .FirstOrDefault(p => p.Id == 317587311279734784); Assert.NotNull(playerByDiscord); + Assert.NotNull(playerByDiscord.Score); + Assert.True(playerByDiscord.Score.PlayerId == playerByDiscord.Id); + + var playerByFriendCode = gameDatabase.Players + .Include(p => p.Score) + .FirstOrDefault(p => p.FriendCode == 8090803305987); + Assert.NotNull(playerByFriendCode); + Assert.NotNull(playerByFriendCode.Score); + Assert.True(playerByDiscord.Score.PlayerId == playerByDiscord.Id); await gameDatabase.Database.EnsureDeletedAsync(); } @@ -130,6 +157,7 @@ public async Task TeamAdditionTest() var team = gameDatabase.Teams.FirstOrDefault(t => t.Name == "MSOC"); Assert.NotNull(team); + Assert.Equal("MSOC", team.Name); await gameDatabase.Database.EnsureDeletedAsync(); } @@ -167,25 +195,21 @@ await _httpClient.PostAsync( }), Encoding.UTF8, "application/json") ); - var player = gameDatabase.Players.FirstOrDefault(p => p.FriendCode == 8090803305987); - var team = gameDatabase.Teams.FirstOrDefault(t => t.Name == "MSOC"); - - Assert.NotNull(player); - Assert.NotNull(team); - var response = await _httpClient.PatchAsync( "/api/admin/bind-player-to-team", new StringContent( JsonSerializer.Serialize(new PlayerBindingRequestModel { - PlayerId = player.Id, - TeamId = team.Id + PlayerId = 317587311279734784, + TeamId = 1 }), Encoding.UTF8, "application/json") ); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var editedTeam = gameDatabase.Teams.FirstOrDefault(t => t.Name == "MSOC"); + var editedTeam = gameDatabase.Teams + .Include(t => t.Players) + .First(t => t.Name == "MSOC"); Assert.NotNull(editedTeam); Assert.StrictEqual(1, editedTeam.Players.Count); diff --git a/MSOC.Backend/Controller/AdminController.cs b/MSOC.Backend/Controller/AdminController.cs index af9c214..b8adb04 100644 --- a/MSOC.Backend/Controller/AdminController.cs +++ b/MSOC.Backend/Controller/AdminController.cs @@ -1,5 +1,6 @@ using AngleSharp.Html.Dom; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using MSOC.Backend.Controller.RequestModel; using MSOC.Backend.Database.Models; using MSOC.Backend.Service; @@ -137,12 +138,7 @@ [FromBody] PlayerBindingRequestModel bind var team = _gameDatabase.Teams.FirstOrDefault(t => t.Id == bind.TeamId); if (player is null || team is null) return NotFound(); - - player.Team = team; - - // EF Core implicitly did this for me. - // team.Players.Add(player); - + team.Players.Add(player); _gameDatabase.SaveChanges(); return Ok(); From 582488ff0dfee6dfe272884e33dd1ad9f43843f0 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 31 Aug 2024 12:44:43 +0700 Subject: [PATCH 15/44] Add MSOC membership column in database. --- ...240831054226_AddMSOCMembership.Designer.cs | 150 ++++++++++++++++++ .../20240831054226_AddMSOCMembership.cs | 29 ++++ .../GameDatabaseServiceModelSnapshot.cs | 9 +- 3 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 MSOC.Backend/Migrations/20240831054226_AddMSOCMembership.Designer.cs create mode 100644 MSOC.Backend/Migrations/20240831054226_AddMSOCMembership.cs diff --git a/MSOC.Backend/Migrations/20240831054226_AddMSOCMembership.Designer.cs b/MSOC.Backend/Migrations/20240831054226_AddMSOCMembership.Designer.cs new file mode 100644 index 0000000..e2cc6c1 --- /dev/null +++ b/MSOC.Backend/Migrations/20240831054226_AddMSOCMembership.Designer.cs @@ -0,0 +1,150 @@ +// +using System; +using MSOC.Backend.Service; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MSOC.Backend.Migrations +{ + [DbContext(typeof(GameDatabaseService))] + [Migration("20240831054226_AddMSOCMembership")] + partial class AddMSOCMembership + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FriendCode") + .HasColumnType("INTEGER"); + + b.Property("IsLeader") + .HasColumnType("INTEGER"); + + b.Property("MaimaiAvatarUrl") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("MembershipLevel") + .HasColumnType("INTEGER"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("SchoolId") + .HasColumnType("INTEGER"); + + b.Property("TeamId") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("TeamId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Score", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateOfAcceptance") + .HasColumnType("TEXT"); + + b.Property("DateOfAdmission") + .HasColumnType("TEXT"); + + b.Property("DxScore1") + .HasColumnType("INTEGER"); + + b.Property("DxScore2") + .HasColumnType("INTEGER"); + + b.Property("IsAccepted") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Sub1") + .HasColumnType("REAL"); + + b.Property("Sub2") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.HasIndex("PlayerId") + .IsUnique(); + + b.ToTable("Scores"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Team", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Teams"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => + { + b.HasOne("MSOC.Backend.Database.Models.Team", "Team") + .WithMany("Players") + .HasForeignKey("TeamId"); + + b.Navigation("Team"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Score", b => + { + b.HasOne("MSOC.Backend.Database.Models.Player", "Player") + .WithOne("Score") + .HasForeignKey("MSOC.Backend.Database.Models.Score", "PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => + { + b.Navigation("Score"); + }); + + modelBuilder.Entity("MSOC.Backend.Database.Models.Team", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/MSOC.Backend/Migrations/20240831054226_AddMSOCMembership.cs b/MSOC.Backend/Migrations/20240831054226_AddMSOCMembership.cs new file mode 100644 index 0000000..5084088 --- /dev/null +++ b/MSOC.Backend/Migrations/20240831054226_AddMSOCMembership.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MSOC.Backend.Migrations +{ + /// + public partial class AddMSOCMembership : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MembershipLevel", + table: "Players", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MembershipLevel", + table: "Players"); + } + } +} diff --git a/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs b/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs index 8c5f4c2..7da7996 100644 --- a/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs +++ b/MSOC.Backend/Migrations/GameDatabaseServiceModelSnapshot.cs @@ -34,6 +34,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(256) .HasColumnType("TEXT"); + b.Property("MembershipLevel") + .HasColumnType("INTEGER"); + b.Property("Rating") .HasColumnType("INTEGER"); @@ -52,7 +55,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("TeamId"); - b.ToTable("Players", (string)null); + b.ToTable("Players"); }); modelBuilder.Entity("MSOC.Backend.Database.Models.Score", b => @@ -90,7 +93,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("PlayerId") .IsUnique(); - b.ToTable("Scores", (string)null); + b.ToTable("Scores"); }); modelBuilder.Entity("MSOC.Backend.Database.Models.Team", b => @@ -106,7 +109,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Teams", (string)null); + b.ToTable("Teams"); }); modelBuilder.Entity("MSOC.Backend.Database.Models.Player", b => From 840603075cb65619534e29a2908b4da0bfc7486a Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 31 Aug 2024 12:46:03 +0700 Subject: [PATCH 16/44] Prevent recursion in PlayerController. And add a test to back it up. --- .../Integration/PlayerControllerTest.cs | 80 +++++++++++++++++++ MSOC.Backend/Controller/PlayerController.cs | 5 +- 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 MSOC.Backend.Tests/Integration/PlayerControllerTest.cs diff --git a/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs b/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs new file mode 100644 index 0000000..3bf6316 --- /dev/null +++ b/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs @@ -0,0 +1,80 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using MSOC.Backend.Controller.RequestModel; +using MSOC.Backend.Database.Models; +using MSOC.Backend.Service; + +namespace MSOC.Backend.Tests.Integration; + +public class PlayerControllerTest : IClassFixture>, IDisposable +{ + private readonly GameApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public PlayerControllerTest(GameApplicationFactory factory) + { + _factory = factory; + _httpClient = factory.CreateClient(); + + // ensure the game database is nuked. + // so we start fresh everytime. + using var scope = _factory.Services.CreateScope(); + var gameDatabase = scope.ServiceProvider.GetService()!; + gameDatabase.Database.EnsureDeleted(); + } + + public void Dispose() + { + // ensure the game database is nuked. + // so we start fresh everytime. + using var scope = _factory.Services.CreateScope(); + var gameDatabase = scope.ServiceProvider.GetService()!; + gameDatabase.Database.EnsureDeleted(); + } + + [Theory] + [InlineData(317587311279734784, "discord")] + [InlineData(8090803305987, "friend_code")] + public async Task PlayerQueryTest(ulong key, string type) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var gameDatabase = scope.ServiceProvider.GetService()!; + await gameDatabase.Database.EnsureCreatedAsync(); + + await _httpClient.PostAsync( + "/api/admin/add-score", + new StringContent( + JsonSerializer.Serialize(new ScoreAdditionRequestModel + { + // @hidr0 on Discord, *mostly* real data. + DiscordId = 317587311279734784, + FriendCode = 8090803305987, + Sub1 = 101.0000, + DxScore1 = 6969, + Sub2 = 101.0000, + DxScore2 = 7270, + SchoolId = 237 + }), Encoding.UTF8, "application/json") + ); + + var response = await _httpClient.GetAsync($"/api/player/get?id={key}&type={type}"); + var content = await response.Content.ReadAsStringAsync(); + + var data = JsonSerializer.Deserialize(content, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + ReferenceHandler = ReferenceHandler.IgnoreCycles + }); + + Assert.NotNull(data); + Assert.NotNull(data.Score); + Assert.Null(data.Score.Player); + + await gameDatabase.Database.EnsureDeletedAsync(); + } +} \ No newline at end of file diff --git a/MSOC.Backend/Controller/PlayerController.cs b/MSOC.Backend/Controller/PlayerController.cs index 4877c56..7c874a3 100644 --- a/MSOC.Backend/Controller/PlayerController.cs +++ b/MSOC.Backend/Controller/PlayerController.cs @@ -38,8 +38,11 @@ public IActionResult GetPlayer( "discord" => defaultQuery.FirstOrDefault(p => p.Id == lookupKey), _ => null }; - + if (player == null) return NotFound(); + + // we prevent recursion? + player.Score!.Player = null!; return Ok(player); } From 14667f332a5fdfc1aa554d874aa35c5b184e1278 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 31 Aug 2024 18:51:27 +0700 Subject: [PATCH 17/44] Mark parallelism of integration tests running disabled. --- MSOC.Backend.Tests/Integration/DisableTestParallelism.cs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 MSOC.Backend.Tests/Integration/DisableTestParallelism.cs diff --git a/MSOC.Backend.Tests/Integration/DisableTestParallelism.cs b/MSOC.Backend.Tests/Integration/DisableTestParallelism.cs new file mode 100644 index 0000000..a5642f7 --- /dev/null +++ b/MSOC.Backend.Tests/Integration/DisableTestParallelism.cs @@ -0,0 +1,6 @@ +[assembly: CollectionBehavior(DisableTestParallelization = true)] +namespace MSOC.Backend.Tests.Integration; + +public class DisableTestParallelism +{ +} \ No newline at end of file From 93142d43eb21d45fcb855bf464a1f0e43cbee149 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 31 Aug 2024 18:52:16 +0700 Subject: [PATCH 18/44] Change score approval API request body. And a test to back it up. --- .../Integration/AdminControllerTest.cs | 60 +++++++++++++------ MSOC.Backend/Controller/AdminController.cs | 8 +-- .../RequestModel/ScoreApprovalRequestModel.cs | 6 ++ 3 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 MSOC.Backend/Controller/RequestModel/ScoreApprovalRequestModel.cs diff --git a/MSOC.Backend.Tests/Integration/AdminControllerTest.cs b/MSOC.Backend.Tests/Integration/AdminControllerTest.cs index 7b0163c..75294ef 100644 --- a/MSOC.Backend.Tests/Integration/AdminControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/AdminControllerTest.cs @@ -6,10 +6,8 @@ using MSOC.Backend.Controller.RequestModel; using MSOC.Backend.Service; -[assembly: CollectionBehavior(DisableTestParallelization = true)] namespace MSOC.Backend.Tests.Integration; - -public class AdminControllerTest : IClassFixture>, IDisposable +public class AdminControllerTest : IClassFixture> { private readonly GameApplicationFactory _factory; private readonly HttpClient _httpClient; @@ -18,21 +16,6 @@ public AdminControllerTest(GameApplicationFactory factory) { _factory = factory; _httpClient = factory.CreateClient(); - - // ensure the game database is nuked. - // so we start fresh everytime. - using var scope = _factory.Services.CreateScope(); - var gameDatabase = scope.ServiceProvider.GetService()!; - gameDatabase.Database.EnsureDeleted(); - } - - public void Dispose() - { - // ensure the game database is nuked. - // so we start fresh everytime. - using var scope = _factory.Services.CreateScope(); - var gameDatabase = scope.ServiceProvider.GetService()!; - gameDatabase.Database.EnsureDeleted(); } [Theory] @@ -216,4 +199,45 @@ await _httpClient.PostAsync( await gameDatabase.Database.EnsureDeletedAsync(); } + + [Fact] + public async Task ScoreApprovalTest() + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var gameDatabase = scope.ServiceProvider.GetService()!; + await gameDatabase.Database.EnsureCreatedAsync(); + + for (ulong testId = 1001; testId <= 1005; testId++) + { + await _httpClient.PostAsync( + "/api/admin/add-score", + new StringContent( + JsonSerializer.Serialize(new ScoreAdditionRequestModel + { + DiscordId = testId, + FriendCode = 8090803305987, + Sub1 = 101.0000, + DxScore1 = 6969, + Sub2 = 101.0000, + DxScore2 = 7270, + SchoolId = 123 + }), Encoding.UTF8, "application/json") + ); + } + + for (ulong testId = 1; testId <= 5; testId++) + await _httpClient.PatchAsync( + "/api/admin/approve-leaderboard", + new StringContent(JsonSerializer.Serialize(new ScoreApprovalRequestModel + { + ScoreId = testId + }), Encoding.UTF8, "application/json") + ); + + var approvedScores = gameDatabase.Scores.Where(score => score.IsAccepted == true); + + Assert.StrictEqual(5, approvedScores.Count()); + await gameDatabase.Database.EnsureDeletedAsync(); + } } \ No newline at end of file diff --git a/MSOC.Backend/Controller/AdminController.cs b/MSOC.Backend/Controller/AdminController.cs index b8adb04..5e76758 100644 --- a/MSOC.Backend/Controller/AdminController.cs +++ b/MSOC.Backend/Controller/AdminController.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using AngleSharp.Html.Dom; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -147,13 +148,13 @@ [FromBody] PlayerBindingRequestModel bind /// /// Approve an entry on the leaderboard. /// - /// Score ID. + /// Approval object. [HttpPatch("approve-leaderboard")] public IActionResult ApproveScore( - [FromQuery(Name = "score_id")] ulong scoreId + [FromBody] ScoreApprovalRequestModel approval ) { - var scores = _gameDatabase.Scores.Where(score => score.Id == scoreId).ToArray(); + var scores = _gameDatabase.Scores.Where(score => score.Id == approval.ScoreId).ToArray(); if (scores.Length == 0) return NotFound(); @@ -161,7 +162,6 @@ public IActionResult ApproveScore( { score.IsAccepted = true; score.DateOfAcceptance = DateTime.Now; - } // Do an update on the entire database. diff --git a/MSOC.Backend/Controller/RequestModel/ScoreApprovalRequestModel.cs b/MSOC.Backend/Controller/RequestModel/ScoreApprovalRequestModel.cs new file mode 100644 index 0000000..71adb91 --- /dev/null +++ b/MSOC.Backend/Controller/RequestModel/ScoreApprovalRequestModel.cs @@ -0,0 +1,6 @@ +namespace MSOC.Backend.Controller.RequestModel; + +public class ScoreApprovalRequestModel +{ + public ulong ScoreId { get; set; } +} \ No newline at end of file From 7ae1c281b13fdfbd34ae438eb473fbe3330bf945 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 31 Aug 2024 18:57:25 +0700 Subject: [PATCH 19/44] Add LeaderboardController test suite. --- .../Integration/LeaderboardControllerTest.cs | 66 +++++++++++++++++++ .../Controller/LeaderboardController.cs | 3 + 2 files changed, 69 insertions(+) create mode 100644 MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs diff --git a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs new file mode 100644 index 0000000..1cf863b --- /dev/null +++ b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs @@ -0,0 +1,66 @@ +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using MSOC.Backend.Controller.RequestModel; +using MSOC.Backend.Database.Models; +using MSOC.Backend.Service; + +namespace MSOC.Backend.Tests.Integration; + +public class LeaderboardControllerTest : IClassFixture> +{ + private readonly GameApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public LeaderboardControllerTest(GameApplicationFactory factory) + { + _factory = factory; + _httpClient = factory.CreateClient(); + } + + [Fact] + public async Task IndividualLeaderboardTest() + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var gameDatabase = scope.ServiceProvider.GetService()!; + await gameDatabase.Database.EnsureCreatedAsync(); + + for (ulong testId = 1001; testId <= 1005; testId++) + { + await _httpClient.PostAsync( + "/api/admin/add-score", + new StringContent( + JsonSerializer.Serialize(new ScoreAdditionRequestModel + { + DiscordId = testId, + FriendCode = 8090803305987, + Sub1 = 101.0000, + DxScore1 = 6969, + Sub2 = 101.0000, + DxScore2 = 7270, + SchoolId = 123 + }), Encoding.UTF8, "application/json") + ); + } + + for (ulong testId = 1; testId <= 5; testId++) + await _httpClient.PatchAsync( + "/api/admin/approve-leaderboard", + new StringContent(JsonSerializer.Serialize(new ScoreApprovalRequestModel + { + ScoreId = testId + }), Encoding.UTF8, "application/json") + ); + + var response = await _httpClient.GetAsync("/api/leaderboard/individual?page=1"); + var content = await response.Content.ReadAsStringAsync(); + + var scores = JsonSerializer.Deserialize>(content); + + Assert.StrictEqual(5, scores!.Count); + scores.ForEach(score => Assert.Null(score.Player)); + + await gameDatabase.Database.EnsureDeletedAsync(); + } +} \ No newline at end of file diff --git a/MSOC.Backend/Controller/LeaderboardController.cs b/MSOC.Backend/Controller/LeaderboardController.cs index 2424ebd..8e4bf82 100644 --- a/MSOC.Backend/Controller/LeaderboardController.cs +++ b/MSOC.Backend/Controller/LeaderboardController.cs @@ -36,6 +36,9 @@ public IActionResult QueryIndividualLeaderboard([FromQuery] int page = 1) .Skip(10 * (page - 1)) .Take(10); + // recursion prevention + foreach (var score in sortedScores) score.Player.Score = null!; + // TODO: Hit the SignalR endpoint to yell at the front end. // _database.SaveChanges(); From bdc413d87e5f82cc715bbf18f38b00d1fe390481 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 31 Aug 2024 19:51:26 +0700 Subject: [PATCH 20/44] Update README.md --- README.md | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8d1668d..030bc6c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,35 @@ -# What's this? +
-*yes.* +![Image](https://raw.githubusercontent.com/team-nameless/msoc-web/main/MSOC/public/fac_1.png) -OK, let's be real, we founded a studio dedicated for event organization. And in that case, we need a website to serve for: +Welcome to *MSOC* - *maimai Open Student Cup*. -- Our branding. -- Tournament information (participating players, qualifier standings and posting updates) -- Serving for the tournament (game updates, ban/pick-ing throughout the course of the game, final standings) +This is `msoc-web` repository, containing **both** the user-facing and the back side of the MSOC website. -## What's more? +**If you intend to register for MSOC, don't hestitate to smash the button below:** + +![Registration](https://img.shields.io/badge/Registration-Coming%20Soon!-gray?labelColor=fdaa48&style=for-the-badge) -idk, we still cooking. +
+ +# What is this place? + +- This place serves as a code repository for the entire website - both Front End & Back End part. +- We did this because transparency is a thing that exists, and to prove that we do not have the intentions to promote shady things. + - *coughs* MSOC+ *coughs*. +- Perhaps to *show off* our programming skill and let the team has something to put on their CV? + +# Can you explain the directory structure? + +- `MSOC`: the user facing portion. +- `MSOC.Backend`: the backside of the web, containing *most* of the useful APIs. +- `MSOC.Backend.Tests`: containing the Unit and Integration tests for the backend. Since the APIs in the backend make heavy uses of the database, we try our best to test the entire backend in transactional manner, and to cover many edge cases as possible. + - [We leverage GitHub Action to test on every commit](https://github.com/team-nameless/msoc-web/blob/main/.github/workflows/basic.yml). The result can be seen here: [![Sanity check](https://github.com/team-nameless/msoc-web/actions/workflows/basic.yml/badge.svg)](https://github.com/team-nameless/msoc-web/actions/workflows/basic.yml) + +# How can I run the code? + +- If you ask this question, I doubt you are a developer. + +# Can I help? + +- Sure, send a message to `@hidr0` on Discord. From a2af46b644f71baa5b8a67894ceb33f5cbb69784 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sat, 31 Aug 2024 22:02:44 +0700 Subject: [PATCH 21/44] Escalate the environment of our CI. --- .github/workflows/basic.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index abba5f6..926e354 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -5,9 +5,10 @@ on: push: branches: - main - pull_request: + pull_request_target: branches: - main + types: [opened, reopened, synchronize, assigned] jobs: build-and-test: From 7e3d8d5e9cadf30cbd042a2d90770ba3e6d69a71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 31 Aug 2024 15:04:28 +0000 Subject: [PATCH 22/44] Bump webpack from 5.93.0 to 5.94.0 in /MSOC Bumps [webpack](https://github.com/webpack/webpack) from 5.93.0 to 5.94.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.93.0...v5.94.0) --- updated-dependencies: - dependency-name: webpack dependency-type: indirect ... Signed-off-by: dependabot[bot] --- MSOC/package-lock.json | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/MSOC/package-lock.json b/MSOC/package-lock.json index e90732e..1daa9a8 100644 --- a/MSOC/package-lock.json +++ b/MSOC/package-lock.json @@ -4027,15 +4027,6 @@ "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -17416,11 +17407,10 @@ } }, "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -17429,7 +17419,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", From 78ab30faf2a50db0289c30681c1594882698185a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 31 Aug 2024 15:06:51 +0000 Subject: [PATCH 23/44] Bump micromatch from 4.0.7 to 4.0.8 in /MSOC Bumps [micromatch](https://github.com/micromatch/micromatch) from 4.0.7 to 4.0.8. - [Release notes](https://github.com/micromatch/micromatch/releases) - [Changelog](https://github.com/micromatch/micromatch/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/micromatch/compare/4.0.7...4.0.8) --- updated-dependencies: - dependency-name: micromatch dependency-type: indirect ... Signed-off-by: dependabot[bot] --- MSOC/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MSOC/package-lock.json b/MSOC/package-lock.json index e90732e..c8b8aab 100644 --- a/MSOC/package-lock.json +++ b/MSOC/package-lock.json @@ -12530,9 +12530,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" From baaf658eaa8a3bf40b0f9ef237ad21fc5abc3d04 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sun, 1 Sep 2024 08:33:41 +0700 Subject: [PATCH 24/44] Skip MaimaiInquiryService tests. --- MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs b/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs index 619872e..e5b892e 100644 --- a/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs @@ -13,7 +13,7 @@ public MaimaiInquiryServiceTest(GameApplicationFactory factory) _factory = factory; } - [Theory] + [Theory(Skip = "Rely much on internet speed.")] [InlineData(1234)] [InlineData(69420)] [InlineData(177013)] @@ -27,7 +27,7 @@ public async Task InvalidFriendCodeTest(ulong friendCode) Assert.StrictEqual(0, result.Length); } - [Theory] + [Theory(Skip = "Rely much on internet speed.")] [InlineData(9051555929120)] [InlineData(8095773611588)] [InlineData(9020119099087)] @@ -44,7 +44,7 @@ public async Task ValidFamiliarFriendCodeTest(ulong friendCode) Assert.NotEmpty((result[2] as IHtmlImageElement)!.Source!); } - [Theory] + [Theory(Skip = "Rely much on internet speed.")] [InlineData(8069933165057)] public async Task ValidStrangerFriendCodeTest(ulong friendCode) { From dd7c82eeaa1ce3ffeed49153de61d1884ab28230 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sun, 1 Sep 2024 08:57:09 +0700 Subject: [PATCH 25/44] Use Microshit-provided healthcheck API. --- MSOC.Backend.Tests/Integration/SmokeTest.cs | 2 +- MSOC.Backend/Controller/TestController.cs | 17 ----------------- MSOC.Backend/Program.cs | 2 ++ 3 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 MSOC.Backend/Controller/TestController.cs diff --git a/MSOC.Backend.Tests/Integration/SmokeTest.cs b/MSOC.Backend.Tests/Integration/SmokeTest.cs index 287ee7f..c35fe12 100644 --- a/MSOC.Backend.Tests/Integration/SmokeTest.cs +++ b/MSOC.Backend.Tests/Integration/SmokeTest.cs @@ -24,6 +24,6 @@ public async Task ApiSmokeTest() var response = await _httpClient.GetAsync("/api/healthcheck"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("I am OK", await response.Content.ReadAsStringAsync()); + Assert.Equal("Healthy", await response.Content.ReadAsStringAsync()); } } \ No newline at end of file diff --git a/MSOC.Backend/Controller/TestController.cs b/MSOC.Backend/Controller/TestController.cs deleted file mode 100644 index 012d86a..0000000 --- a/MSOC.Backend/Controller/TestController.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace MSOC.Backend.Controller; - -[ApiController] -[Route("api")] -public class TestController : ControllerBase -{ - /// - /// Send a GET to this. If it returns nothing, pray. - /// - [HttpGet("healthcheck")] - public IActionResult HealthCheck() - { - return Ok("I am OK"); - } -} \ No newline at end of file diff --git a/MSOC.Backend/Program.cs b/MSOC.Backend/Program.cs index f0e549e..fb2b267 100644 --- a/MSOC.Backend/Program.cs +++ b/MSOC.Backend/Program.cs @@ -14,6 +14,7 @@ // :thinking: builder.Services.AddControllers(); builder.Services.AddSignalR(); +builder.Services.AddHealthChecks(); // Add crapwares to the controller builder.Services @@ -45,6 +46,7 @@ } app + .UseHealthChecks("/api/healthcheck") .UseHsts() .UseRouting() .UseHttpsRedirection() From eb41268a956a95d91e6b3aa21a585dbec2bc9ff1 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sun, 1 Sep 2024 09:44:36 +0700 Subject: [PATCH 26/44] Add simple authorization for /api/admin/* endpoints. --- .../Integration/AdminControllerTest.cs | 9 +++++++ .../Integration/LeaderboardControllerTest.cs | 7 +++++ .../Integration/PlayerControllerTest.cs | 8 ++++++ .../AdminControllerAuthentication.cs | 26 +++++++++++++++++++ MSOC.Backend/Program.cs | 5 ++++ 5 files changed, 55 insertions(+) create mode 100644 MSOC.Backend/Middleware/AdminControllerAuthentication.cs diff --git a/MSOC.Backend.Tests/Integration/AdminControllerTest.cs b/MSOC.Backend.Tests/Integration/AdminControllerTest.cs index 75294ef..1b2755d 100644 --- a/MSOC.Backend.Tests/Integration/AdminControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/AdminControllerTest.cs @@ -1,7 +1,9 @@ using System.Net; +using System.Net.Http.Headers; using System.Text; using System.Text.Json; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MSOC.Backend.Controller.RequestModel; using MSOC.Backend.Service; @@ -15,7 +17,14 @@ public class AdminControllerTest : IClassFixture public AdminControllerTest(GameApplicationFactory factory) { _factory = factory; + + var configuration = _factory.Services.GetService()!; + _httpClient = factory.CreateClient(); + _httpClient.DefaultRequestHeaders.Add( + "Authorization", + configuration.GetSection("API:Authorization").Value + ); } [Theory] diff --git a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs index 1cf863b..19cf3c7 100644 --- a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs @@ -1,5 +1,6 @@ using System.Text; using System.Text.Json; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MSOC.Backend.Controller.RequestModel; using MSOC.Backend.Database.Models; @@ -15,7 +16,13 @@ public class LeaderboardControllerTest : IClassFixture factory) { _factory = factory; + var configuration = _factory.Services.GetService()!; + _httpClient = factory.CreateClient(); + _httpClient.DefaultRequestHeaders.Add( + "Authorization", + configuration.GetSection("API:Authorization").Value + ); } [Fact] diff --git a/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs b/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs index 3bf6316..5e4c8a9 100644 --- a/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MSOC.Backend.Controller.RequestModel; using MSOC.Backend.Database.Models; @@ -18,7 +19,14 @@ public class PlayerControllerTest : IClassFixture factory) { _factory = factory; + + var configuration = _factory.Services.GetService()!; + _httpClient = factory.CreateClient(); + _httpClient.DefaultRequestHeaders.Add( + "Authorization", + configuration.GetSection("API:Authorization").Value + ); // ensure the game database is nuked. // so we start fresh everytime. diff --git a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs new file mode 100644 index 0000000..8a20b01 --- /dev/null +++ b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs @@ -0,0 +1,26 @@ +namespace MSOC.Backend.Middleware; + +public class AdminControllerAuthentication +{ + private readonly RequestDelegate _next; + private readonly IConfiguration _configuration; + + public AdminControllerAuthentication(RequestDelegate next, IConfiguration configuration) + { + _next = next; + _configuration = configuration; + } + + public async Task InvokeAsync(HttpContext context) + { + var auth = context.Request.Headers.Authorization; + + if ( + !string.IsNullOrWhiteSpace(auth) && + auth == _configuration.GetValue("API:Authorization") + ) + { + await _next(context); + } + } +} \ No newline at end of file diff --git a/MSOC.Backend/Program.cs b/MSOC.Backend/Program.cs index fb2b267..7545afe 100644 --- a/MSOC.Backend/Program.cs +++ b/MSOC.Backend/Program.cs @@ -2,6 +2,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; +using MSOC.Backend.Middleware; using MSOC.Backend.Service; var builder = WebApplication.CreateBuilder(args); @@ -46,6 +47,10 @@ } app + .UseWhen( + ctx => ctx.Request.Path.StartsWithSegments("/api/admin"), + cfg => cfg.UseMiddleware() + ) .UseHealthChecks("/api/healthcheck") .UseHsts() .UseRouting() From 78432120b0ad1e4aa0e9f409d9dacb8ee521e4e2 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sun, 1 Sep 2024 10:14:14 +0700 Subject: [PATCH 27/44] Throw a 401 on unauthenticated /api/admin requests. --- MSOC.Backend/Middleware/AdminControllerAuthentication.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs index 8a20b01..cc9743b 100644 --- a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs +++ b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs @@ -22,5 +22,9 @@ public async Task InvokeAsync(HttpContext context) { await _next(context); } + else + { + context.Response.StatusCode = 401; + } } } \ No newline at end of file From 8c200327cf0393d4c54c60fd2a204dff0465555f Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sun, 1 Sep 2024 10:14:57 +0700 Subject: [PATCH 28/44] Add Authorization information to Swagger. --- MSOC.Backend/Program.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/MSOC.Backend/Program.cs b/MSOC.Backend/Program.cs index 7545afe..d01e1f2 100644 --- a/MSOC.Backend/Program.cs +++ b/MSOC.Backend/Program.cs @@ -30,6 +30,31 @@ { c.SwaggerDoc("v1", new OpenApiInfo { Title = "MSOC API", Version = "v1" }); c.IncludeXmlComments(Assembly.GetExecutingAssembly()); + + c.AddSecurityDefinition("Basic", new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + Scheme = "Basic", + BearerFormat = "String", + In = ParameterLocation.Header, + Description = "Authorization header value using the Basic scheme." + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Basic" + } + }, + [] + } + }); }); var app = builder.Build(); From d0f7451756f68cebc47c83bc9b1a1e6afacde319 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sun, 1 Sep 2024 10:23:24 +0700 Subject: [PATCH 29/44] Change how authentication is sent to conform with RFC 7235. https://datatracker.ietf.org/doc/html/rfc7235 --- MSOC.Backend.Tests/Integration/AdminControllerTest.cs | 4 ++-- MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs | 5 +++-- MSOC.Backend.Tests/Integration/PlayerControllerTest.cs | 5 +++-- MSOC.Backend/Middleware/AdminControllerAuthentication.cs | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/MSOC.Backend.Tests/Integration/AdminControllerTest.cs b/MSOC.Backend.Tests/Integration/AdminControllerTest.cs index 1b2755d..6a47667 100644 --- a/MSOC.Backend.Tests/Integration/AdminControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/AdminControllerTest.cs @@ -21,8 +21,8 @@ public AdminControllerTest(GameApplicationFactory factory) var configuration = _factory.Services.GetService()!; _httpClient = factory.CreateClient(); - _httpClient.DefaultRequestHeaders.Add( - "Authorization", + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + "Basic", configuration.GetSection("API:Authorization").Value ); } diff --git a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs index 19cf3c7..3038818 100644 --- a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs @@ -1,3 +1,4 @@ +using System.Net.Http.Headers; using System.Text; using System.Text.Json; using Microsoft.Extensions.Configuration; @@ -19,8 +20,8 @@ public LeaderboardControllerTest(GameApplicationFactory factory) var configuration = _factory.Services.GetService()!; _httpClient = factory.CreateClient(); - _httpClient.DefaultRequestHeaders.Add( - "Authorization", + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + "Basic", configuration.GetSection("API:Authorization").Value ); } diff --git a/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs b/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs index 5e4c8a9..a7151c6 100644 --- a/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -23,8 +24,8 @@ public PlayerControllerTest(GameApplicationFactory factory) var configuration = _factory.Services.GetService()!; _httpClient = factory.CreateClient(); - _httpClient.DefaultRequestHeaders.Add( - "Authorization", + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + "Basic", configuration.GetSection("API:Authorization").Value ); diff --git a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs index cc9743b..e66bd7e 100644 --- a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs +++ b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs @@ -17,7 +17,7 @@ public async Task InvokeAsync(HttpContext context) if ( !string.IsNullOrWhiteSpace(auth) && - auth == _configuration.GetValue("API:Authorization") + auth == $"Basic {_configuration.GetValue("API:Authorization")}" ) { await _next(context); From 1e26b350843e46aa5a707428f0d2744446cf1446 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Sun, 1 Sep 2024 10:45:23 +0700 Subject: [PATCH 30/44] Fix a blatant edge case when the API internal "password" is unset. And that let unauthorized users freely use the Admin stuffs. --- .../Middleware/AdminControllerAuthentication.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs index e66bd7e..90ab9de 100644 --- a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs +++ b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs @@ -13,18 +13,27 @@ public AdminControllerAuthentication(RequestDelegate next, IConfiguration config public async Task InvokeAsync(HttpContext context) { + var authConfig = _configuration.GetValue("API:Authorization"); + + // EDGE CASE: if no authorization configuration, exit immediately. + if (string.IsNullOrWhiteSpace(_configuration.GetValue("API:Authorization"))) + { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + return; + } + var auth = context.Request.Headers.Authorization; if ( !string.IsNullOrWhiteSpace(auth) && - auth == $"Basic {_configuration.GetValue("API:Authorization")}" + auth == $"Basic {authConfig}" ) { await _next(context); } else { - context.Response.StatusCode = 401; + context.Response.StatusCode = StatusCodes.Status401Unauthorized; } } } \ No newline at end of file From 036fe574f4e19e1234b3b67889c5709832ad4cec Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Mon, 2 Sep 2024 09:15:09 +0700 Subject: [PATCH 31/44] Make admin "lookup" endpoints not query the entire database. --- MSOC.Backend/Controller/AdminController.cs | 42 ++++++++-------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/MSOC.Backend/Controller/AdminController.cs b/MSOC.Backend/Controller/AdminController.cs index 5e76758..4ae1a2e 100644 --- a/MSOC.Backend/Controller/AdminController.cs +++ b/MSOC.Backend/Controller/AdminController.cs @@ -154,25 +154,16 @@ public IActionResult ApproveScore( [FromBody] ScoreApprovalRequestModel approval ) { - var scores = _gameDatabase.Scores.Where(score => score.Id == approval.ScoreId).ToArray(); + var foundScore = _gameDatabase.Scores.FirstOrDefault(score => score.Id == approval.ScoreId); - if (scores.Length == 0) return NotFound(); - - foreach (var score in scores) - { - score.IsAccepted = true; - score.DateOfAcceptance = DateTime.Now; - } - - // Do an update on the entire database. - var _ = _gameDatabase.Scores - .Where(score => score.IsAccepted) - .OrderByDescending(score => score.Sub1 + score.Sub2) - .ThenByDescending(score => score.DxScore1 + score.DxScore2) - .ThenBy(score => score.DateOfAdmission); + if (foundScore == null) return NotFound(); - // TODO: Hit the SignalR endpoint to yell at the front end. + foundScore.IsAccepted = true; + foundScore.DateOfAcceptance = DateTime.Now; + // TODO: Hit the SignalR endpoint to yell at the front end. + + _gameDatabase.Update(foundScore); _gameDatabase.SaveChanges(); return Ok(); @@ -189,20 +180,17 @@ [FromBody] TrackMarkingRequestModel trackMark { if (trackMark.TrackId is < 1 or > 626) return BadRequest("ID can only be [1-626]"); - var foundedTracks = _trackDatabase.Tracks - .Where(track => track.Id == trackMark.TrackId) - .Take(1) - .ToArray(); + var foundedTrack = _trackDatabase.Tracks + .FirstOrDefault(track => track.Id == trackMark.TrackId); - if (foundedTracks.Length == 0) return NotFound(); + if (foundedTrack == null) return NotFound(); if (!trackMark.Testing) - foreach (var track in foundedTracks) - { - track.HasBeenPicked = true; - _trackDatabase.Update(track); - _trackDatabase.SaveChanges(); - } + { + foundedTrack.HasBeenPicked = true; + _trackDatabase.Update(foundedTrack); + _trackDatabase.SaveChanges(); + } return Ok(); } From e7a087a033d192ab194443531998dc81a860276a Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Mon, 2 Sep 2024 09:15:34 +0700 Subject: [PATCH 32/44] Cache the API Authorization key. --- .../Middleware/AdminControllerAuthentication.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs index 90ab9de..dca3e56 100644 --- a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs +++ b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs @@ -4,19 +4,21 @@ public class AdminControllerAuthentication { private readonly RequestDelegate _next; private readonly IConfiguration _configuration; + + private readonly string _authKey; public AdminControllerAuthentication(RequestDelegate next, IConfiguration configuration) { _next = next; _configuration = configuration; + + _authKey = _configuration.GetValue("API:Authorization")!; } public async Task InvokeAsync(HttpContext context) { - var authConfig = _configuration.GetValue("API:Authorization"); - // EDGE CASE: if no authorization configuration, exit immediately. - if (string.IsNullOrWhiteSpace(_configuration.GetValue("API:Authorization"))) + if (string.IsNullOrWhiteSpace(_authKey)) { context.Response.StatusCode = StatusCodes.Status500InternalServerError; return; @@ -26,7 +28,7 @@ public async Task InvokeAsync(HttpContext context) if ( !string.IsNullOrWhiteSpace(auth) && - auth == $"Basic {authConfig}" + auth == $"Basic {_authKey}" ) { await _next(context); From 04376f82798692b6e29506a33c3a370de9bac723 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Mon, 2 Sep 2024 09:31:06 +0700 Subject: [PATCH 33/44] Add sorted assertion for LeaderboardControllerTest. --- .../Integration/LeaderboardControllerTest.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs index 3038818..7935fa2 100644 --- a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs @@ -43,9 +43,9 @@ await _httpClient.PostAsync( { DiscordId = testId, FriendCode = 8090803305987, - Sub1 = 101.0000, + Sub1 = Random.Shared.Next(97, 101), DxScore1 = 6969, - Sub2 = 101.0000, + Sub2 = Random.Shared.Next(97, 101), DxScore2 = 7270, SchoolId = 123 }), Encoding.UTF8, "application/json") @@ -68,6 +68,12 @@ await _httpClient.PatchAsync( Assert.StrictEqual(5, scores!.Count); scores.ForEach(score => Assert.Null(score.Player)); + + var sortedScores = scores.OrderByDescending(score => score.Sub1 + score.Sub2) + .ThenByDescending(score => score.DxScore1 + score.DxScore2) + .ThenBy(score => score.DateOfAdmission); + + Assert.Equal(scores, sortedScores); await gameDatabase.Database.EnsureDeletedAsync(); } From 8dc996720c3497aced30edfbfb2a8c4907ff7a71 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Mon, 2 Sep 2024 09:33:36 +0700 Subject: [PATCH 34/44] Why am I still here? Just to suffer? --- MSOC.Backend/Controller/TrackController.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/MSOC.Backend/Controller/TrackController.cs b/MSOC.Backend/Controller/TrackController.cs index 3d298e1..20415f6 100644 --- a/MSOC.Backend/Controller/TrackController.cs +++ b/MSOC.Backend/Controller/TrackController.cs @@ -54,13 +54,11 @@ public IActionResult GetTrack( { if (trackId is < 1 or > 626) return BadRequest("ID can only be [1-626]"); - var foundedTracks = _trackDatabase.Tracks - .Where(track => track.Id == trackId) - .Take(1) - .ToArray(); + var foundedTrack = _trackDatabase.Tracks + .FirstOrDefault(track => track.Id == trackId); - if (foundedTracks.Length == 0) return NotFound(); + if (foundedTrack == null) return NotFound(); - return Ok(foundedTracks[0]); + return Ok(foundedTrack); } } \ No newline at end of file From e1a60a4b1150ac337d598cff6c4bd89a3b63d469 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Mon, 2 Sep 2024 09:47:21 +0700 Subject: [PATCH 35/44] Only include player information after the leaderboard is sorted. --- MSOC.Backend/Controller/LeaderboardController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MSOC.Backend/Controller/LeaderboardController.cs b/MSOC.Backend/Controller/LeaderboardController.cs index 8e4bf82..635a290 100644 --- a/MSOC.Backend/Controller/LeaderboardController.cs +++ b/MSOC.Backend/Controller/LeaderboardController.cs @@ -28,13 +28,13 @@ public IActionResult QueryIndividualLeaderboard([FromQuery] int page = 1) // Do an update on the entire database. var sortedScores = _gameDatabase.Scores - .Include(s => s.Player) .Where(score => score.IsAccepted) .OrderByDescending(score => score.Sub1 + score.Sub2) .ThenByDescending(score => score.DxScore1 + score.DxScore2) .ThenBy(score => score.DateOfAdmission) .Skip(10 * (page - 1)) - .Take(10); + .Take(10) + .Include(s => s.Player); // recursion prevention foreach (var score in sortedScores) score.Player.Score = null!; From a1f6f9aeb8b393d3da0f62c9d4bfb2ceb6b196ae Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Mon, 2 Sep 2024 10:35:45 +0700 Subject: [PATCH 36/44] Add AdminControllerMiddleware tests. --- .../AdminControllerMiddlewareTest.cs | 88 +++++++++++++++++++ .../AdminControllerAuthentication.cs | 29 +++--- 2 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 MSOC.Backend.Tests/Middleware/AdminControllerMiddlewareTest.cs diff --git a/MSOC.Backend.Tests/Middleware/AdminControllerMiddlewareTest.cs b/MSOC.Backend.Tests/Middleware/AdminControllerMiddlewareTest.cs new file mode 100644 index 0000000..6cb154c --- /dev/null +++ b/MSOC.Backend.Tests/Middleware/AdminControllerMiddlewareTest.cs @@ -0,0 +1,88 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MSOC.Backend.Controller.RequestModel; +using MSOC.Backend.Service; + +namespace MSOC.Backend.Tests.Middleware; + +public class AdminControllerMiddlewareTest : IClassFixture> +{ + private readonly GameApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public AdminControllerMiddlewareTest(GameApplicationFactory factory) + { + _factory = factory; + _httpClient = factory.CreateClient(); + } + + [Fact] + public async Task AdminControllerMiddlewareUnauthorized() + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var gameDatabase = scope.ServiceProvider.GetService()!; + await gameDatabase.Database.EnsureCreatedAsync(); + + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + "Basic", + "Hey I am an administrator!" + ); + + var response = await _httpClient.PostAsync( + "/api/admin/add-score", + new StringContent( + JsonSerializer.Serialize(new ScoreAdditionRequestModel + { + // @hidr0 on Discord, *mostly* real data. + DiscordId = 317587311279734784, + FriendCode = 8090803305987, + Sub1 = 101.0000, + DxScore1 = 6969, + Sub2 = 101.0000, + DxScore2 = 7270, + SchoolId = 237 + }), Encoding.UTF8, "application/json") + ); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task AdminControllerMiddlewarePass() + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var gameDatabase = scope.ServiceProvider.GetService()!; + await gameDatabase.Database.EnsureCreatedAsync(); + + var configuration = scope.ServiceProvider.GetService()!; + + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + "Basic", + configuration.GetSection("API:Authorization").Value + ); + + var response = await _httpClient.PostAsync( + "/api/admin/add-score", + new StringContent( + JsonSerializer.Serialize(new ScoreAdditionRequestModel + { + // @hidr0 on Discord, *mostly* real data. + DiscordId = 317587311279734784, + FriendCode = 8090803305987, + Sub1 = 101.0000, + DxScore1 = 6969, + Sub2 = 101.0000, + DxScore2 = 7270, + SchoolId = 237 + }), Encoding.UTF8, "application/json") + ); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } +} \ No newline at end of file diff --git a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs index dca3e56..9769e0c 100644 --- a/MSOC.Backend/Middleware/AdminControllerAuthentication.cs +++ b/MSOC.Backend/Middleware/AdminControllerAuthentication.cs @@ -18,24 +18,21 @@ public AdminControllerAuthentication(RequestDelegate next, IConfiguration config public async Task InvokeAsync(HttpContext context) { // EDGE CASE: if no authorization configuration, exit immediately. - if (string.IsNullOrWhiteSpace(_authKey)) + if (!string.IsNullOrWhiteSpace(_authKey)) { - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - return; - } - - var auth = context.Request.Headers.Authorization; + var auth = context.Request.Headers.Authorization; - if ( - !string.IsNullOrWhiteSpace(auth) && - auth == $"Basic {_authKey}" - ) - { - await _next(context); - } - else - { - context.Response.StatusCode = StatusCodes.Status401Unauthorized; + if ( + !string.IsNullOrWhiteSpace(auth) && + auth == $"Basic {_authKey}" + ) + { + await _next(context); + } + else + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + } } } } \ No newline at end of file From b7b7bb1baa8af40baf692cdae55aa4d79ec7b57f Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Mon, 2 Sep 2024 10:36:20 +0700 Subject: [PATCH 37/44] Reintroduce back MaimaiInquiryService tests. --- MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs b/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs index e5b892e..619872e 100644 --- a/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs +++ b/MSOC.Backend.Tests/Unit/MaimaiInquiryServiceTest.cs @@ -13,7 +13,7 @@ public MaimaiInquiryServiceTest(GameApplicationFactory factory) _factory = factory; } - [Theory(Skip = "Rely much on internet speed.")] + [Theory] [InlineData(1234)] [InlineData(69420)] [InlineData(177013)] @@ -27,7 +27,7 @@ public async Task InvalidFriendCodeTest(ulong friendCode) Assert.StrictEqual(0, result.Length); } - [Theory(Skip = "Rely much on internet speed.")] + [Theory] [InlineData(9051555929120)] [InlineData(8095773611588)] [InlineData(9020119099087)] @@ -44,7 +44,7 @@ public async Task ValidFamiliarFriendCodeTest(ulong friendCode) Assert.NotEmpty((result[2] as IHtmlImageElement)!.Source!); } - [Theory(Skip = "Rely much on internet speed.")] + [Theory] [InlineData(8069933165057)] public async Task ValidStrangerFriendCodeTest(ulong friendCode) { From bbfa24de48fdb4ad6c7c6c901825859ade020b71 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Wed, 4 Sep 2024 11:23:06 +0700 Subject: [PATCH 38/44] Move SmokeTest to the root test directory. --- MSOC.Backend.Tests/{Integration => }/SmokeTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename MSOC.Backend.Tests/{Integration => }/SmokeTest.cs (95%) diff --git a/MSOC.Backend.Tests/Integration/SmokeTest.cs b/MSOC.Backend.Tests/SmokeTest.cs similarity index 95% rename from MSOC.Backend.Tests/Integration/SmokeTest.cs rename to MSOC.Backend.Tests/SmokeTest.cs index c35fe12..7f7283d 100644 --- a/MSOC.Backend.Tests/Integration/SmokeTest.cs +++ b/MSOC.Backend.Tests/SmokeTest.cs @@ -1,6 +1,6 @@ using System.Net; -namespace MSOC.Backend.Tests.Integration; +namespace MSOC.Backend.Tests; public class SmokeTest : IClassFixture> { From e763158e6444276e633ef85a2c497a1c41bdb742 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Wed, 4 Sep 2024 11:25:33 +0700 Subject: [PATCH 39/44] Add SchoolController and some tests. --- .../Integration/SchoolControllerTest.cs | 56 +++++++++++++++++++ MSOC.Backend/Controller/SchoolController.cs | 37 ++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 MSOC.Backend.Tests/Integration/SchoolControllerTest.cs create mode 100644 MSOC.Backend/Controller/SchoolController.cs diff --git a/MSOC.Backend.Tests/Integration/SchoolControllerTest.cs b/MSOC.Backend.Tests/Integration/SchoolControllerTest.cs new file mode 100644 index 0000000..5752ba7 --- /dev/null +++ b/MSOC.Backend.Tests/Integration/SchoolControllerTest.cs @@ -0,0 +1,56 @@ +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using MSOC.Backend.Service; + +namespace MSOC.Backend.Tests.Integration; + +public class SchoolControllerTest : IClassFixture> +{ + private readonly GameApplicationFactory _factory; + private readonly HttpClient _httpClient; + + public SchoolControllerTest(GameApplicationFactory factory) + { + _factory = factory; + _httpClient = factory.CreateClient(); + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(257)] + [InlineData(258)] + public async Task SchoolGetFail(int schoolId) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var schoolDatabase = scope.ServiceProvider.GetService()!; + await schoolDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.GetAsync($"/api/schools/get?school_id={schoolId}"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + Assert.Equal("ID can only be [1-256]", content); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(255)] + [InlineData(256)] + public async Task SchoolGetPass(int schoolId) + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var trackDatabase = scope.ServiceProvider.GetService()!; + await trackDatabase.Database.EnsureCreatedAsync(); + + var response = await _httpClient.GetAsync($"/api/schools/get?school_id={schoolId}"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(content); + } +} \ No newline at end of file diff --git a/MSOC.Backend/Controller/SchoolController.cs b/MSOC.Backend/Controller/SchoolController.cs new file mode 100644 index 0000000..073aee5 --- /dev/null +++ b/MSOC.Backend/Controller/SchoolController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Mvc; +using MSOC.Backend.Database.Models; +using MSOC.Backend.Service; + +namespace MSOC.Backend.Controller; + +[Route("api/schools")] +[ApiController] +public class SchoolController : ControllerBase +{ + private readonly SchoolDatabaseService _schoolDatabase; + + public SchoolController(SchoolDatabaseService schoolDatabase) + { + _schoolDatabase = schoolDatabase; + } + + /// + /// Get a school from given ID. + /// + /// School ID, within range 1-256. + [HttpGet("get")] + [ProducesResponseType(typeof(School), 200, "application/json")] + public IActionResult GetSchool( + [FromQuery(Name = "school_id")] int schoolId + ) + { + if (schoolId is < 1 or > 256) return BadRequest("ID can only be [1-256]"); + + var foundedTrack = _schoolDatabase.Schools + .FirstOrDefault(track => track.Id == schoolId); + + if (foundedTrack == null) return NotFound(); + + return Ok(foundedTrack); + } +} \ No newline at end of file From b26e223cb3774f66e72a86ed3f7541ee41317f28 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Wed, 4 Sep 2024 11:34:00 +0700 Subject: [PATCH 40/44] Add a headcount check to Player-Team binding API. --- MSOC.Backend/Controller/AdminController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MSOC.Backend/Controller/AdminController.cs b/MSOC.Backend/Controller/AdminController.cs index 4ae1a2e..f972cf9 100644 --- a/MSOC.Backend/Controller/AdminController.cs +++ b/MSOC.Backend/Controller/AdminController.cs @@ -139,6 +139,9 @@ [FromBody] PlayerBindingRequestModel bind var team = _gameDatabase.Teams.FirstOrDefault(t => t.Id == bind.TeamId); if (player is null || team is null) return NotFound(); + + if (team.Players.Count == 4) return BadRequest("Team is already full."); + team.Players.Add(player); _gameDatabase.SaveChanges(); From 8f99f536f9aeb66e17a7d78c4045fe5a3bb4aa91 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Wed, 4 Sep 2024 11:59:56 +0700 Subject: [PATCH 41/44] I guess we JSON case insensitive now. --- .../Integration/LeaderboardControllerTest.cs | 3 ++- MSOC.Backend.Tests/Integration/PlayerControllerTest.cs | 7 ++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs index 7935fa2..7387b2c 100644 --- a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs @@ -13,6 +13,7 @@ public class LeaderboardControllerTest : IClassFixture _factory; private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonCaseInsensitive = new() { PropertyNameCaseInsensitive = true }; public LeaderboardControllerTest(GameApplicationFactory factory) { @@ -64,7 +65,7 @@ await _httpClient.PatchAsync( var response = await _httpClient.GetAsync("/api/leaderboard/individual?page=1"); var content = await response.Content.ReadAsStringAsync(); - var scores = JsonSerializer.Deserialize>(content); + var scores = JsonSerializer.Deserialize>(content, _jsonCaseInsensitive); Assert.StrictEqual(5, scores!.Count); scores.ForEach(score => Assert.Null(score.Player)); diff --git a/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs b/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs index a7151c6..f5acb86 100644 --- a/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/PlayerControllerTest.cs @@ -16,6 +16,7 @@ public class PlayerControllerTest : IClassFixture _factory; private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonCaseInsensitive = new() { PropertyNameCaseInsensitive = true }; public PlayerControllerTest(GameApplicationFactory factory) { @@ -74,11 +75,7 @@ await _httpClient.PostAsync( var response = await _httpClient.GetAsync($"/api/player/get?id={key}&type={type}"); var content = await response.Content.ReadAsStringAsync(); - var data = JsonSerializer.Deserialize(content, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - ReferenceHandler = ReferenceHandler.IgnoreCycles - }); + var data = JsonSerializer.Deserialize(content, _jsonCaseInsensitive); Assert.NotNull(data); Assert.NotNull(data.Score); From c760d9ac1f1c103c96ac4552c67b4f8c447e9b5d Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Wed, 4 Sep 2024 12:01:01 +0700 Subject: [PATCH 42/44] Make test database Scoped lifetime to be in line with the main code. --- MSOC.Backend.Tests/GameApplicationFactory.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MSOC.Backend.Tests/GameApplicationFactory.cs b/MSOC.Backend.Tests/GameApplicationFactory.cs index b30c904..1397717 100644 --- a/MSOC.Backend.Tests/GameApplicationFactory.cs +++ b/MSOC.Backend.Tests/GameApplicationFactory.cs @@ -25,18 +25,18 @@ protected override IHost CreateHost(IHostBuilder builder) .Parent!.Parent!.Parent!.Parent!.ToString(); services.AddDbContext( - o => o.UseSqlite($"Filename={path}/MSOC.Backend/schools.db"), - ServiceLifetime.Transient + o => o.UseSqlite($"Filename={path}/MSOC.Backend/schools.db") + // ServiceLifetime.Transient ); services.AddDbContext( - o => o.UseSqlite($"Filename={path}/MSOC.Backend/tracks.db"), - ServiceLifetime.Transient + o => o.UseSqlite($"Filename={path}/MSOC.Backend/tracks.db") + // ServiceLifetime.Transient ); services.AddDbContext( - o => o.UseSqlite($"Filename={path}/MSOC.Backend/MSOC.Test.db"), - ServiceLifetime.Transient + o => o.UseSqlite($"Filename={path}/MSOC.Backend/MSOC.Test.db") + //ServiceLifetime.Transient ); // services.AddSingleton(); From 59d511d6987e3b8df089d5a275da7201c4d41327 Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Wed, 4 Sep 2024 12:07:41 +0700 Subject: [PATCH 43/44] Rework code for team leaderboard and add a long-running test. --- .../Integration/LeaderboardControllerTest.cs | 89 +++++++++++++++++++ .../Controller/LeaderboardController.cs | 23 ++--- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs index 7387b2c..9f6c07b 100644 --- a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs @@ -78,4 +78,93 @@ await _httpClient.PatchAsync( await gameDatabase.Database.EnsureDeletedAsync(); } + + [Fact] + public async Task TeamLeaderboardTest() + { + await using var scope = _factory.Services.CreateAsyncScope(); + + var gameDatabase = scope.ServiceProvider.GetService()!; + await gameDatabase.Database.EnsureCreatedAsync(); + + // add test players + for (ulong testId = 1001; testId <= 1032; testId++) + { + await _httpClient.PostAsync( + "/api/admin/add-score", + new StringContent( + JsonSerializer.Serialize(new ScoreAdditionRequestModel + { + DiscordId = testId, + FriendCode = 8090803305987, + Sub1 = Random.Shared.Next(97, 101), + DxScore1 = 6969, + Sub2 = Random.Shared.Next(97, 101), + DxScore2 = 7270, + SchoolId = Random.Shared.Next(123, 150) + }), Encoding.UTF8, "application/json") + ); + } + + // add test teams + for (ulong teamId = 1; teamId <= 8; teamId++) + { + await _httpClient.PostAsync( + "/api/admin/add-team", + new StringContent( + JsonSerializer.Serialize(new TeamRequestModel + { + Name = $"MSOC-TestTeam-{teamId}", + }), Encoding.UTF8, "application/json") + ); + } + + // approve all scores + for (ulong scoreId = 1; scoreId <= 32; scoreId++) + { + await _httpClient.PatchAsync( + "/api/admin/approve-leaderboard", + new StringContent(JsonSerializer.Serialize(new ScoreApprovalRequestModel + { + ScoreId = scoreId + }), Encoding.UTF8, "application/json") + ); + } + + // perform team binding + for (ulong testId = 1001; testId <= 1032; testId++) + { + await _httpClient.PatchAsync( + "/api/admin/bind-player-to-team", + new StringContent( + JsonSerializer.Serialize(new PlayerBindingRequestModel + { + PlayerId = testId, + TeamId = (int) ((testId - 1001) / 4) + 1 + }), Encoding.UTF8, "application/json") + ); + } + + var response = await _httpClient.GetAsync("/api/leaderboard/team"); + var content = await response.Content.ReadAsStringAsync(); + + var teams = JsonSerializer.Deserialize>(content, _jsonCaseInsensitive); + + Assert.StrictEqual(8, teams!.Count); + + teams.ForEach(team => team.Players.ToList().ForEach(player => + { + Assert.Null(player.Score); + Assert.Null(player.Team); + } + )); + + var sortedTeams = teams + .OrderByDescending(team => team.Players.Sum(p => p.Score!.Sub1 + p.Score.Sub2)) + .ThenByDescending(team => team.Players.Sum(p => p.Score!.DxScore1 + p.Score.DxScore2)); + + Assert.Equal(teams, sortedTeams); + + await gameDatabase.Database.EnsureDeletedAsync(); + } } \ No newline at end of file diff --git a/MSOC.Backend/Controller/LeaderboardController.cs b/MSOC.Backend/Controller/LeaderboardController.cs index 635a290..4420694 100644 --- a/MSOC.Backend/Controller/LeaderboardController.cs +++ b/MSOC.Backend/Controller/LeaderboardController.cs @@ -26,7 +26,6 @@ public IActionResult QueryIndividualLeaderboard([FromQuery] int page = 1) { if (page < 1) return BadRequest("Page number must be at least 1"); - // Do an update on the entire database. var sortedScores = _gameDatabase.Scores .Where(score => score.IsAccepted) .OrderByDescending(score => score.Sub1 + score.Sub2) @@ -38,30 +37,34 @@ public IActionResult QueryIndividualLeaderboard([FromQuery] int page = 1) // recursion prevention foreach (var score in sortedScores) score.Player.Score = null!; - - // TODO: Hit the SignalR endpoint to yell at the front end. - // _database.SaveChanges(); return Ok(sortedScores); } /// - /// Get team leaderboard. Expect shit performance. + /// Get TOP 8 of team leaderboard. Expect shit performance. /// [HttpGet("team")] [ProducesResponseType(typeof(IEnumerable), 200, "application/json")] public IActionResult QueryTeamLeaderboard() { - // Do an update on the entire database. - var sortedScores = _gameDatabase.Teams + var sortedTeams = _gameDatabase.Teams .Include(t => t.Players) + .ThenInclude(p => p.Score) + .OrderByDescending(team => team.Players.Count) + .Where(team => team.Players.Count(p => p.Score!.IsAccepted) > 0) .OrderByDescending(team => team.Players.Sum(p => p.Score!.Sub1 + p.Score.Sub2)) .ThenByDescending(team => team.Players.Sum(p => p.Score!.DxScore1 + p.Score.DxScore2)) .Take(8); - // TODO: Hit the SignalR endpoint to yell at the front end. - // _database.SaveChanges(); + // recursion prevention + foreach (var team in sortedTeams) + foreach (var player in team.Players) + { + player.Score!.Player = null!; + player.Team = null!; + } - return Ok(sortedScores); + return Ok(sortedTeams); } } \ No newline at end of file From 07fc31c28f1ab78cffdff8e780efa39f5b1e9cea Mon Sep 17 00:00:00 2001 From: Tien Dat Pham Date: Wed, 4 Sep 2024 12:17:46 +0700 Subject: [PATCH 44/44] Fix cycle detection test. [GONE WRONG] [GONE S*XUAL] --- MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs index 9f6c07b..690a554 100644 --- a/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs +++ b/MSOC.Backend.Tests/Integration/LeaderboardControllerTest.cs @@ -68,7 +68,7 @@ await _httpClient.PatchAsync( var scores = JsonSerializer.Deserialize>(content, _jsonCaseInsensitive); Assert.StrictEqual(5, scores!.Count); - scores.ForEach(score => Assert.Null(score.Player)); + scores.ForEach(score => Assert.Null(score.Player.Score)); var sortedScores = scores.OrderByDescending(score => score.Sub1 + score.Sub2) .ThenByDescending(score => score.DxScore1 + score.DxScore2) @@ -154,7 +154,7 @@ await _httpClient.PatchAsync( teams.ForEach(team => team.Players.ToList().ForEach(player => { - Assert.Null(player.Score); + Assert.Null(player.Score!.Player); Assert.Null(player.Team); } ));