Repository navigation
chore(sync): merge upstream main (2775faf) into the fork #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
b21d005
Add a CLR linear-model method for differential abundance
hyperpolymath 3acf607
clr_lm: filter by prevalence before cmultRepl, and test with Welch
hyperpolymath c548451
Rename the clr_lm method to clr_welch
hyperpolymath 657120c
Test clr_welch when zCompositions is missing
hyperpolymath b86d59f
Run the zCompositions-absent test in CI instead of skipping it
hyperpolymath 7799de8
Test the cmultRepl output check directly
hyperpolymath 3f9a5bc
proofs(agda): Benjamini-Hochberg adjustment over exact rationals
hyperpolymath 1d8572f
proofs(agda): TSS proportions and RLE median positivity over exact ra…
hyperpolymath b3e871d
proofs: axiom audit for the Agda tree; align headers with D1's Julia
hyperpolymath a3ccd2e
proofs: ignore Agda interface files
hyperpolymath 6df0e56
ci: type-check the Agda proofs; docs: map each theorem to its Julia code
hyperpolymath aacf5d1
docs: keep the theorem-to-code map with the proofs
hyperpolymath af75c8c
fix(differential): refuse complete separation instead of reporting p≈1
hyperpolymath c860980
fix: separate a significance test that did not run from a null result
hyperpolymath f71fa03
Test that an omnibus statistic R cannot compute is a failed test
hyperpolymath c4b2efc
fix(server): log an unreadable config at warn, not error
hyperpolymath 144d0c1
Test that an unreadable primers.yml is logged as a warning
hyperpolymath d0b31f4
fix(ci): give the required status check a version-stable name
hyperpolymath 5b45d09
fix(ci): drop the 1x1 matrix so the check name is actually stable
hyperpolymath f9cf729
docs(ci): name the fork PR that measured the matrix-suffixed check name
hyperpolymath 9d888f1
docs: the Julia pin is repeated in the CI workflow, which has no matrix
hyperpolymath a37a642
Gate file-name config values and remote keys; guard pipeline tables o…
hyperpolymath 578915c
Test that a remote host without a staging_dir is refused
hyperpolymath 592ddd7
fix(permanova): test each term, report permutations, refuse untestabl…
hyperpolymath bcf52af
Clear stale analysis results and surface request failures in the UI
hyperpolymath 8dda16d
Harden server origin checks, disable Oxygen docs, serve uploads inert
hyperpolymath e877e13
Accept the SPA index for /docs paths when a frontend build is present
hyperpolymath 2775faf
docs: describe split deployments and the origins the server must be t…
hyperpolymath 678cacd
chore(sync): merge upstream main (2775faf) into the fork — resolve th…
hyperpolymath 17d5289
Merge branch 'main' into chore/sync-upstream-2775faf
hyperpolymath 5a60887
docs(server): clarify origin guards, file middleware, and startup beh…
coderabbitai[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| # Unit tests for the server's request guards: the Origin allow-list, Oxygen's | ||
| # docs and metrics being off, and the headers report uploads are served with. | ||
| # Requests go through the app middleware in-process; no socket is opened. | ||
| using MetaManifold | ||
| SV = MetaManifold.Server | ||
| using JSON3 | ||
|
|
||
| # A request through the middleware `start` installs. | ||
| _ss_request(method, path, headers=Pair{String,String}[], body="") = | ||
| SV.Oxygen.internalrequest(SV.HTTP.Request(method, path, headers, body); middleware=SV._MIDDLEWARE) | ||
|
|
||
| # A request through the handler Oxygen's `serve` builds from `_SERVE_OPTIONS`, | ||
| # docs and metrics included, without starting the listener. | ||
| function _ss_served(path) | ||
| ctx = SV.Oxygen.CONTEXT[] | ||
| opts = SV._SERVE_OPTIONS | ||
| saved = ctx.docs.router[] | ||
| try | ||
| ctx.docs.router[] = SV.HTTP.Router() | ||
| opts.docs && SV.Oxygen.Core.setupdocs(ctx) | ||
| opts.metrics && SV.Oxygen.Core.setupmetrics(ctx) | ||
| handler = SV.Oxygen.Core.setupmiddleware(ctx; middleware=SV._MIDDLEWARE, | ||
| docs=opts.docs, metrics=opts.metrics, show_errors=opts.show_errors) | ||
| handler(SV.HTTP.Request("GET", path, ["Host" => "127.0.0.1:8080"])) | ||
| finally | ||
| ctx.docs.router[] = saved | ||
| end | ||
| end | ||
|
|
||
| @testset "Server request guards" begin | ||
| tmp = mktempdir() | ||
| old_root = SV.ServerState._root[] | ||
| old_host, old_port = SV._bound_host[], SV._bound_port[] | ||
| SV.ServerState.set_root!(tmp) | ||
| SV._bound_host[] = "127.0.0.1" | ||
| SV._bound_port[] = 8080 | ||
| mkpath(joinpath(tmp, "data", "StudyS", "run1")) | ||
| touch(joinpath(tmp, "data", "StudyS", "run1", "s1_R1.fastq.gz")) | ||
| mkpath(joinpath(tmp, "projects", "StudyS", "run1")) | ||
| try | ||
| @testset "Origin allow-list" begin | ||
| studies = "/api/v1/studies" | ||
| host = "Host" => "127.0.0.1:8080" | ||
| # No Origin (same-origin GET, curl): unchanged. | ||
| @test _ss_request("GET", studies, [host]).status == 200 | ||
| # The served origin, in both spellings, and the Vite dev server. | ||
| for o in ("http://127.0.0.1:8080", "http://localhost:8080", "http://localhost:5173", | ||
| "http://127.0.0.1:5173", "http://LOCALHOST:8080/") | ||
| r = _ss_request("GET", studies, [host, "Origin" => o]) | ||
| @test r.status == 200 | ||
| @test SV.HTTP.header(r, "Access-Control-Allow-Origin") == o | ||
| @test _ss_request("OPTIONS", studies, [host, "Origin" => o]).status == 204 | ||
| end | ||
| # Another loopback page: other port, other scheme, or no origin at all. | ||
| for o in ("http://localhost:3000", "http://127.0.0.1:9999", "https://localhost:8080", | ||
| "http://[::1]:8080", "null", "http://evil.example") | ||
| @test _ss_request("GET", studies, [host, "Origin" => o]).status == 403 | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| @test _ss_request("OPTIONS", studies, [host, "Origin" => o]).status == 403 | ||
| # A mutating request is refused before it reaches the route. | ||
| r = _ss_request("POST", "/api/v1/studies/StudyS/report?kind=figure&ext=.txt", | ||
| [host, "Origin" => o, "Content-Type" => "text/plain"], "x") | ||
| @test r.status == 403 | ||
| end | ||
| @test !isdir(joinpath(tmp, "projects", "StudyS", "report")) | ||
| # The allowed set follows the bound port. | ||
| SV._bound_port[] = 9090 | ||
| @test _ss_request("GET", studies, [host, "Origin" => "http://localhost:9090"]).status == 200 | ||
| @test _ss_request("GET", studies, [host, "Origin" => "http://localhost:8080"]).status == 403 | ||
| SV._bound_port[] = 8080 | ||
| # Extra origins come from JULIA_METAMANIFOLD_ALLOWED_ORIGINS. | ||
| withenv("JULIA_METAMANIFOLD_ALLOWED_ORIGINS" => " http://lab-box:8080 , http://localhost:5174/") do | ||
| @test _ss_request("GET", studies, [host, "Origin" => "http://lab-box:8080"]).status == 200 | ||
| @test _ss_request("GET", studies, [host, "Origin" => "http://localhost:5174"]).status == 200 | ||
| @test _ss_request("GET", studies, [host, "Origin" => "http://localhost:3000"]).status == 403 | ||
| end | ||
| # The Host check still applies first. | ||
| @test _ss_request("GET", studies, ["Host" => "evil.example:8080"]).status == 403 | ||
| end | ||
|
|
||
| @testset "Oxygen docs and metrics are not served" begin | ||
| @test SV._SERVE_OPTIONS.docs === false | ||
| @test SV._SERVE_OPTIONS.metrics === false | ||
| # With a frontend build present, an unknown path falls through to the | ||
| # SPA catch-all and answers 200 with index.html. That is the app, not | ||
| # the docs, so accept exactly that body as well as a non-200 answer. | ||
| index = joinpath(SV._frontend_dir, "index.html") | ||
| spa = isfile(index) ? read(index) : nothing | ||
| for path in ("/docs", "/docs/schema", "/docs/swagger", "/docs/metrics") | ||
| r = _ss_served(path) | ||
| body = lowercase(String(copy(r.body))) | ||
| @test !occursin("openapi", body) | ||
| @test !occursin("swagger", body) | ||
| @test !occursin("redoc", body) | ||
| @test r.status != 200 || (spa !== nothing && r.body == spa) | ||
| end | ||
| # The served handler still answers the API. | ||
| @test _ss_served("/api/v1/studies").status == 200 | ||
| end | ||
|
|
||
| @testset "Report uploads are served inert" begin | ||
| host = "Host" => "127.0.0.1:8080" | ||
| base = "/api/v1/studies/StudyS/report" | ||
| svg = "<svg xmlns='http://www.w3.org/2000/svg'><script>alert(1)</script></svg>" | ||
| r = _ss_request("POST", "$base?kind=figure&title=X&ext=.svg", [host], svg) | ||
| @test r.status == 200 | ||
| item = JSON3.read(String(r.body)) | ||
| for path in ("$base/$(item.id)/file", "/files/StudyS/runs/report/$(item.file)") | ||
| f = _ss_request("GET", path, [host]) | ||
| @test f.status == 200 | ||
| @test String(f.body) == svg | ||
| @test SV.HTTP.header(f, "Content-Type") == "image/svg+xml" | ||
| @test SV.HTTP.header(f, "X-Content-Type-Options") == "nosniff" | ||
| @test SV.HTTP.header(f, "Content-Security-Policy") == | ||
| "default-src 'none'; style-src 'unsafe-inline'; sandbox" | ||
| end | ||
| # Pipeline output under /files/ is sniff-proof but keeps working inline. | ||
| write(joinpath(tmp, "projects", "StudyS", "run1", "plot.pdf"), "%PDF-1.4") | ||
| f = _ss_request("GET", "/files/StudyS/runs/run1/plot.pdf", [host]) | ||
| @test f.status == 200 | ||
| @test SV.HTTP.header(f, "X-Content-Type-Options") == "nosniff" | ||
| @test SV.HTTP.header(f, "Content-Security-Policy") == "" | ||
| end | ||
| finally | ||
| SV.ServerState._root[] = old_root | ||
| SV._bound_host[], SV._bound_port[] = old_host, old_port | ||
| rm(tmp; recursive=true, force=true) | ||
| end | ||
| end | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.