You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/08/23 07:29:52 UTC
(texera) 01/01: test(file-service): cover DatasetResource's public presigned and file-node reads (#7881)
This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-7881-1d1eee1ef431568824ccc2ba494dd5c24bfdf8ad
in repository https://gitbox.apache.org/repos/asf/texera.git
commit a557a1bcb33ebaca99321e98116f4a1ec5b6770a
Author: Meng Wang <me...@uci.edu>
AuthorDate: Sun Aug 23 07:29:29 2026 +0000
test(file-service): cover DatasetResource's public presigned and file-node reads (#7881)
### What changes were proposed in this PR?
Extends `DatasetResourceSpec` to cover `DatasetResource`'s public
(anonymous)
read path, which no test called before: the presigned-download
endpoints, the
anonymous dataset read, and the file-node tree behind the version reads.
Measured with JaCoCo, this takes `DatasetResource.scala` from **73.10 %
to
83.10 %** line coverage (78 → 49 uncovered lines).
8 added tests:
- `getPublicPresignedUrl` — returns a presigned address for a public
dataset's file, and returns **400** when only one of `repositoryName` /
`commitHash` is supplied (the shared `resolveDatasetAndPath` error arm).
- `getPublicPresignedUrlWithS3` — the same for the S3 variant, plus a
percent-encoded path (spaces and an escaped separator) that only
resolves because the endpoint decodes the query parameter first.
- `getPublicDataset` — returns the dashboard dataset for a public
dataset, and raises `ForbiddenException` for a private one.
- `retrievePublicDatasetVersionRootFileNodes` — returns the version's
tree to an anonymous caller, asserting it is **assembled rather than
flattened** (a directory keeps its own children, two levels deep) across
files seeded under more than one directory.
- `retrieveDatasetVersionRootFileNodes` — the authenticated sibling,
which shared the same uncovered helper.
Every region the issue named is now fully covered.
### Notes from writing these
- **The presigned URL carries no file name.** It is MinIO's *physical*
object address built from LakeFS's internal object ids
(`…/<bucket>/<repositoryName>/data/<objectId>?X-Amz-…`). The tests
therefore assert that the address belongs to the dataset under test (its
unique `repositoryName`) and is actually signed (`X-Amz-Signature`) — no
port and no absolute host, per the issue's determinism note.
- **The file-node reads list committed objects,** so the helper commits
and pins the returned hash rather than reading a branch head.
- **`getDatasetVersionZip` was already covered** — the baseline report
shows no uncovered lines there — so it is not duplicated here.
- The `(None, None)` arm of `resolveDatasetAndPath` (lines
1644/1646/1667) is left uncovered: reaching it means driving
`FileResolver.resolve` + `DocumentFactory.openReadonlyDocument` with a
resolvable `texera://` URI, which is a different (and much heavier)
fixture than this issue's scope.
**Determinism.** Each test seeds its own dataset, repository and
version, so
nothing depends on another test's uploads or on the shared fixture's
publicity,
which existing tests mutate in place. Assertions are on structure — node
names,
nesting, status codes — never on byte sizes, timestamps, ports or
absolute URLs.
No production code was changed.
### Any related issues, documentation, discussions?
Closes #7878
### How was this PR tested?
Extended unit tests, run locally against the spec's existing embedded
Postgres +
LakeFS/MinIO mocks:
```
sbt "FileService/testOnly *DatasetResourceSpec"
# Tests: succeeded 138, failed 0
sbt "FileService/test" # whole module: 306 succeeded, 0 failed (twice)
sbt "FileService/Test/scalafmtCheck" "FileService/Test/scalafix --check" # clean
sbt "FileService/jacoco" # DatasetResource.scala: 73.10% -> 83.10%
```
The failure path was verified by breaking the file-tree assertion and
confirming
the suite exits non-zero, then restoring it.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8 [1M context])
---
.../service/resource/DatasetResourceSpec.scala | 169 +++++++++++++++++++++
1 file changed, 169 insertions(+)
diff --git a/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourceSpec.scala b/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourceSpec.scala
index 8d89ffe578..baac6770ae 100644
--- a/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourceSpec.scala
+++ b/file-service/src/test/scala/org/apache/texera/service/resource/DatasetResourceSpec.scala
@@ -3484,4 +3484,173 @@ class DatasetResourceSpec
val commit = LakeFSStorageClient.createCommit(repoName, "main", "commit all files")
LakeFSStorageClient.retrieveObjectsOfVersion(repoName, commit.getId).size shouldEqual totalFiles
}
+
+ // ===========================================================================
+ // Public (anonymous) read path: presigned download, dataset read, file nodes
+ // ===========================================================================
+
+ /** Seeds a dataset owned by `ownerUser` plus a committed version holding `files`.
+ * Each call gets its own repository and rows, so these tests never depend on
+ * another test's uploads nor on a shared fixture's publicity being left in a
+ * particular state.
+ */
+ private def seedDatasetWithVersion(
+ prefix: String,
+ files: Seq[(String, String)],
+ isPublic: Boolean = true
+ ): (Dataset, DatasetVersion) = {
+ val repositoryName =
+ s"$prefix-${System.nanoTime()}-${Random.alphanumeric.take(6).mkString.toLowerCase}"
+
+ val dataset = new Dataset
+ dataset.setName(repositoryName)
+ dataset.setRepositoryName(repositoryName)
+ dataset.setIsPublic(isPublic)
+ dataset.setIsDownloadable(true)
+ dataset.setDescription("dataset for public-read tests")
+ dataset.setOwnerUid(ownerUser.getUid)
+ datasetDao.insert(dataset)
+
+ LakeFSStorageClient.initRepo(repositoryName)
+ files.foreach {
+ case (path, content) =>
+ LakeFSStorageClient.writeFileToRepo(
+ repositoryName,
+ path,
+ new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))
+ )
+ }
+ // The file-node reads list COMMITTED objects, so commit before pinning the hash.
+ val commit = LakeFSStorageClient.createCommit(repositoryName, "main", s"seed $prefix")
+
+ val version = new DatasetVersion
+ version.setDid(dataset.getDid)
+ version.setCreatorUid(ownerUser.getUid)
+ version.setName("v1")
+ version.setVersionHash(commit.getId)
+ new DatasetVersionDao(getDSLContext.configuration()).insert(version)
+
+ (dataset, version)
+ }
+
+ "getPublicPresignedUrl" should "hand an anonymous caller a presigned URL for a public dataset's file" in {
+ val (dataset, version) =
+ seedDatasetWithVersion("public-presign", Seq("data/report.csv" -> "a,b"))
+
+ val response = datasetResource.getPublicPresignedUrl(
+ urlEnc("data/report.csv"),
+ dataset.getRepositoryName,
+ version.getVersionHash
+ )
+
+ response.getStatus shouldEqual 200
+ // The address is a physical object URL, so assert it belongs to this dataset and
+ // is actually signed — never on the mock's port or on the logical file name.
+ val presigned = entityAsScalaMap(response)("presignedUrl").toString
+ presigned should include(dataset.getRepositoryName)
+ presigned should include("X-Amz-Signature")
+ }
+
+ it should "reject a request that supplies a repository name without a commit hash" in {
+ val (dataset, _) = seedDatasetWithVersion("public-presign-half", Seq("f.txt" -> "x"))
+
+ val response =
+ datasetResource.getPublicPresignedUrl(urlEnc("f.txt"), dataset.getRepositoryName, null)
+
+ response.getStatus shouldEqual 400
+ }
+
+ "getPublicPresignedUrlWithS3" should "hand an anonymous caller a presigned URL for a public dataset's file" in {
+ val (dataset, version) =
+ seedDatasetWithVersion("public-presign-s3", Seq("model.bin" -> "weights"))
+
+ val response = datasetResource.getPublicPresignedUrlWithS3(
+ urlEnc("model.bin"),
+ dataset.getRepositoryName,
+ version.getVersionHash
+ )
+
+ response.getStatus shouldEqual 200
+ val presigned = entityAsScalaMap(response)("presignedUrl").toString
+ presigned should include(dataset.getRepositoryName)
+ presigned should include("X-Amz-Signature")
+ }
+
+ it should "decode a percent-encoded file path before resolving it" in {
+ val filePath = "nested dir/a b.txt"
+ val (dataset, version) =
+ seedDatasetWithVersion("public-presign-enc", Seq(filePath -> "encoded"))
+
+ // The encoded form differs from the raw path, so this only resolves if the
+ // endpoint decodes the query parameter first.
+ val encoded = urlEnc(filePath)
+ encoded should not equal filePath
+
+ val response = datasetResource.getPublicPresignedUrlWithS3(
+ encoded,
+ dataset.getRepositoryName,
+ version.getVersionHash
+ )
+
+ response.getStatus shouldEqual 200
+ }
+
+ "getPublicDataset" should "return the dashboard dataset for a public dataset" in {
+ val (dataset, _) = seedDatasetWithVersion("public-dataset", Seq("f.txt" -> "x"))
+
+ val dashboard = datasetResource.getPublicDataset(dataset.getDid)
+
+ dashboard.dataset.getDid shouldEqual dataset.getDid
+ dashboard.dataset.getName shouldEqual dataset.getName
+ dashboard.ownerEmail shouldEqual ownerUser.getEmail
+ dashboard.isOwner shouldBe false
+ }
+
+ it should "refuse an anonymous read of a private dataset" in {
+ val (dataset, _) =
+ seedDatasetWithVersion("private-dataset", Seq("f.txt" -> "x"), isPublic = false)
+
+ assertThrows[ForbiddenException] {
+ datasetResource.getPublicDataset(dataset.getDid)
+ }
+ }
+
+ "retrievePublicDatasetVersionRootFileNodes" should "return the version's nested file tree to an anonymous caller" in {
+ val (dataset, version) = seedDatasetWithVersion(
+ "public-nodes",
+ Seq(
+ "top.txt" -> "root file",
+ "docs/readme.md" -> "docs",
+ "docs/img/logo.png" -> "deeper",
+ "data/train.csv" -> "data"
+ )
+ )
+
+ val response =
+ datasetResource.retrievePublicDatasetVersionRootFileNodes(dataset.getDid, version.getDvid)
+
+ response.fileNodes.map(_.getName).toSet shouldEqual Set("top.txt", "docs", "data")
+
+ // The tree is assembled rather than flattened: docs/ keeps its own children.
+ val docs = response.fileNodes.find(_.getName == "docs").get
+ docs.getChildren.map(_.getName).toSet shouldEqual Set("readme.md", "img")
+ docs.getChildren
+ .find(_.getName == "img")
+ .get
+ .getChildren
+ .map(_.getName) shouldEqual List("logo.png")
+ }
+
+ "retrieveDatasetVersionRootFileNodes" should "return the same tree to the dataset's owner" in {
+ val (dataset, version) =
+ seedDatasetWithVersion("owner-nodes", Seq("a.txt" -> "1", "sub/b.txt" -> "2"))
+
+ val response = datasetResource.retrieveDatasetVersionRootFileNodes(
+ dataset.getDid,
+ version.getDvid,
+ sessionUser
+ )
+
+ response.fileNodes.map(_.getName).toSet shouldEqual Set("a.txt", "sub")
+ }
}