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-6889-ac88046d59fecc2a9310769313b5b87d6dd446c1
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 47ceb0f2def8fe7685b814e5fe66290bc99ab90b
Author: Xinyuan Lin <[email protected]>
AuthorDate: Sat Jul 25 19:18:04 2026 -0700

    test(amber): extend HuggingFaceModelResource unit test coverage (#6889)
    
    ### What changes were proposed in this PR?
    
    Extends the existing unit spec from 87 to **126 tests** (+39), covering
    the previously-untested upstream-facing code (`fetchSearchResults`,
    `fetchAllModelsForTask`, `extractNextLink`, `buildSimplifiedList`,
    `hasModelsForTask`, the `/tasks` walk, and `streamMediaWithCap`).
    
    The Hub URLs are hard-coded in the resource, so there is no seam to
    point at a local server; instead the spec swaps Unirest's global HTTP
    client for an in-spec stub `Client`/`RawResponse` that serves canned
    status/body/headers and records the outgoing URL and Authorization
    header. Real Unirest plumbing still runs (including `asObject`, so the
    media proxy is exercised end to end), and `afterEach` resets the config.
    **No network calls** — the 50 MiB cap test uses a lazily generated
    stream.
    
    Covered: search vs browse listing (field simplification and defaulting,
    exact query parameters, bearer-token forwarding, cache hits and
    token-scoped cache bypass), Link-header pagination including the
    `MAX_PAGES=50` cut-off and mid-pagination failure, the truncation-header
    boundary at exactly `SEARCH_LIMIT`, task listing with warm-model
    filtering and label fallback, and all three `413` size-cap paths in the
    media proxy.
    
    No source changes.
    
    ### Any related issues, documentation, discussions?
    
    Closes #6886.
    
    ### How was this PR tested?
    
    `sbt -java-home <jbr-17> "WorkflowExecutionService/testOnly
    *HuggingFaceModelResourceSpec"` -> 126 succeeded, 0 failed.
    `Test/scalafmtCheck` + `Test/scalafix --check` clean.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8 [1M context])
---
 .../resource/HuggingFaceModelResourceSpec.scala    | 710 ++++++++++++++++++++-
 1 file changed, 704 insertions(+), 6 deletions(-)

diff --git 
a/amber/src/test/scala/org/apache/texera/web/resource/HuggingFaceModelResourceSpec.scala
 
b/amber/src/test/scala/org/apache/texera/web/resource/HuggingFaceModelResourceSpec.scala
index 38402bd647..d195e11640 100644
--- 
a/amber/src/test/scala/org/apache/texera/web/resource/HuggingFaceModelResourceSpec.scala
+++ 
b/amber/src/test/scala/org/apache/texera/web/resource/HuggingFaceModelResourceSpec.scala
@@ -19,21 +19,37 @@
 
 package org.apache.texera.web.resource
 
-import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
+import kong.unirest.{
+  Client,
+  Headers,
+  HttpRequest,
+  HttpRequestSummary,
+  HttpResponse,
+  HttpResponseSummary,
+  RawResponse,
+  Unirest,
+  Config => UnirestConfig
+}
 import org.scalatest.BeforeAndAfterEach
 import org.scalatest.funsuite.AnyFunSuite
 
-import java.io.{ByteArrayInputStream, InputStream}
+import java.io.{ByteArrayInputStream, InputStream, InputStreamReader}
 import java.nio.charset.StandardCharsets
 import java.nio.file.{Files, Path, Paths}
+import java.util.function.{Function => JFunction}
 import javax.ws.rs.core.Response
+import scala.collection.mutable
+import scala.jdk.CollectionConverters._
 
 /**
   * Tests for [[HuggingFaceModelResource]] covering the validation, security,
-  * caching, and filesystem behavior that can be exercised without contacting
-  * Hugging Face Hub. Paths that require live HF API calls (the actual fetch
-  * loops in `listModels` browse-mode-uncached and `listTasks` uncached) are
-  * left to integration testing.
+  * caching, and filesystem behavior of the resource.
+  *
+  * The HF Hub URLs are hard-coded in the resource, so the upstream calls are
+  * exercised by swapping Unirest's global HTTP client for an in-process stub
+  * (`StubClient` below) that serves canned responses and records the outgoing
+  * requests. Nothing in this suite touches the network.
   */
 class HuggingFaceModelResourceSpec extends AnyFunSuite with BeforeAndAfterEach 
{
 
@@ -66,6 +82,9 @@ class HuggingFaceModelResourceSpec extends AnyFunSuite with 
BeforeAndAfterEach {
     }
     modelCache.invalidateAll()
     taskCache.invalidateAll()
+    // Drop any stub HTTP client this test installed so the global Unirest
+    // config never leaks between tests.
+    Unirest.config().reset()
   }
 
   // Helper: read a Response's string entity (assumes the body is a String).
@@ -728,4 +747,683 @@ class HuggingFaceModelResourceSpec extends AnyFunSuite 
with BeforeAndAfterEach {
     // cleanup
     Files.deleteIfExists(subdir)
   }
+
+  // ════════════════════════════════════════════════════════════════════════
+  // In-process Unirest stub — lets the upstream-facing paths (search, browse
+  // pagination, task fan-out, media proxy) run for real without any network.
+  // ════════════════════════════════════════════════════════════════════════
+
+  /** One outgoing request as the resource issued it. */
+  private case class StubRequest(url: String, authorization: Option[String])
+
+  /** One canned upstream response. */
+  private case class StubHttpResponse(
+      status: Int,
+      body: Array[Byte],
+      headers: Map[String, String],
+      contentType: String,
+      content: Option[() => InputStream]
+  )
+
+  private def jsonResponse(
+      body: String,
+      status: Int = 200,
+      headers: Map[String, String] = Map.empty
+  ): StubHttpResponse =
+    StubHttpResponse(
+      status,
+      body.getBytes(StandardCharsets.UTF_8),
+      headers,
+      "application/json",
+      None
+    )
+
+  private def bytesResponse(
+      body: Array[Byte],
+      contentType: String,
+      status: Int = 200,
+      headers: Map[String, String] = Map.empty
+  ): StubHttpResponse =
+    StubHttpResponse(status, body, headers, contentType, None)
+
+  /** A response whose body is produced lazily, so huge payloads never hit 
heap. */
+  private def streamResponse(
+      content: () => InputStream,
+      contentType: String,
+      headers: Map[String, String] = Map.empty
+  ): StubHttpResponse =
+    StubHttpResponse(200, Array.emptyByteArray, headers, contentType, 
Some(content))
+
+  private val stubUnirestConfig = new UnirestConfig()
+
+  private class StubRawResponse(canned: StubHttpResponse) extends RawResponse {
+    private lazy val stream: InputStream =
+      canned.content.map(_.apply()).getOrElse(new 
ByteArrayInputStream(canned.body))
+    private val hdrs: Headers = {
+      val h = new Headers()
+      canned.headers.foreach { case (k, v) => h.add(k, v) }
+      h
+    }
+    override def getStatus: Int = canned.status
+    override def getStatusText: String = s"stub-${canned.status}"
+    override def getHeaders: Headers = hdrs
+    override def getContent: InputStream = stream
+    override def getContentAsBytes: Array[Byte] = canned.body
+    override def getContentAsString: String = new String(canned.body, 
StandardCharsets.UTF_8)
+    override def getContentAsString(charset: String): String =
+      new String(canned.body, StandardCharsets.UTF_8)
+    override def getContentReader: InputStreamReader =
+      new InputStreamReader(getContent, StandardCharsets.UTF_8)
+    override def hasContent: Boolean = canned.body.nonEmpty || 
canned.content.isDefined
+    override def getContentType: String = canned.contentType
+    override def getEncoding: String = StandardCharsets.UTF_8.name()
+    override def getConfig: UnirestConfig = stubUnirestConfig
+    override def toSummary: HttpResponseSummary = null
+    override def getRequestSummary: HttpRequestSummary = null
+  }
+
+  /**
+    * Unirest `Client` that answers every request from `handler` and records
+    * what was asked for. Thread-safe: `listTasks` fans out over a 
ForkJoinPool.
+    */
+  private class StubClient(handler: StubRequest => StubHttpResponse) extends 
Client {
+    private val recorded = mutable.ListBuffer[StubRequest]()
+
+    def requests: List[StubRequest] = recorded.synchronized(recorded.toList)
+    def urls: List[String] = requests.map(_.url)
+
+    override def getClient: Object = this
+
+    // The signature mirrors Unirest's raw-typed `Client.request` as Scala 
sees it.
+    override def request[T](
+        req: HttpRequest[_ <: HttpRequest[_ <: AnyRef]],
+        transformer: JFunction[RawResponse, HttpResponse[T]]
+    ): HttpResponse[T] = {
+      // Unirest's Headers.getFirst yields "" (not null) for an absent header.
+      val captured =
+        StubRequest(req.getUrl, 
Option(req.getHeaders.getFirst("Authorization")).filter(_.nonEmpty))
+      recorded.synchronized(recorded += captured)
+      transformer.apply(new StubRawResponse(handler(captured)))
+    }
+
+    override def close(): java.util.stream.Stream[Exception] =
+      java.util.stream.Stream.empty[Exception]()
+
+    override def registerShutdownHook(): Unit = ()
+  }
+
+  /** Install a stub HTTP client for the duration of `body`. */
+  private def withStubClient[A](handler: StubRequest => StubHttpResponse)(
+      body: StubClient => A
+  ): A = {
+    val client = new StubClient(handler)
+    Unirest.config().reset()
+    Unirest.config().httpClient(client)
+    try body(client)
+    finally Unirest.config().reset()
+  }
+
+  /** A raw HF model payload; omitted fields exercise the defaulting branches. 
*/
+  private def hfModel(
+      id: String = "openai-community/gpt2",
+      downloads: String = "5000",
+      likes: String = "42",
+      pipelineTag: String = "text-generation"
+  ): String =
+    
s"""{"id":"$id","downloads":$downloads,"likes":$likes,"pipeline_tag":"$pipelineTag",
+       
|"private":false,"library_name":"transformers"}""".stripMargin.replace("\n", "")
+
+  private def parseArray(response: Response): JsonNode = {
+    val node = mapper.readTree(entityString(response))
+    assert(node.isArray, s"expected a JSON array, got: 
${entityString(response)}")
+    node
+  }
+
+  private def fieldNamesOf(node: JsonNode): List[String] = 
node.fieldNames().asScala.toList
+
+  private def nextLinkHeader(url: String): Map[String, String] =
+    Map("Link" -> s"""<$url>; rel="next"""")
+
+  /** An InputStream of `total` zero bytes that allocates nothing up front. */
+  private class ZeroInputStream(total: Long) extends InputStream {
+    private var remaining = total
+    override def read(): Int = {
+      if (remaining <= 0) -1
+      else { remaining -= 1; 0 }
+    }
+    override def read(b: Array[Byte], off: Int, len: Int): Int = {
+      if (remaining <= 0) -1
+      else {
+        val n = math.min(len.toLong, remaining).toInt
+        java.util.Arrays.fill(b, off, off + n, 0.toByte)
+        remaining -= n
+        n
+      }
+    }
+  }
+
+  // ────────────────────────────────────────────────────────────────────────
+  // listModels — search mode
+  // ────────────────────────────────────────────────────────────────────────
+
+  test("listModels search mode simplifies each HF model to the five UI 
fields") {
+    val response = withStubClient(_ => jsonResponse(s"[${hfModel()}]")) { _ =>
+      resource.listModels("text-generation", "gpt2", null)
+    }
+
+    assert(response.getStatus == 200)
+    val models = parseArray(response)
+    assert(models.size() == 1)
+    val entry = models.get(0)
+    // Order matters: the resource builds a LinkedHashMap in this exact order.
+    assert(fieldNamesOf(entry) == List("id", "label", "pipeline_tag", 
"downloads", "likes"))
+    assert(entry.get("id").asText() == "openai-community/gpt2")
+    assert(entry.get("label").asText() == "openai-community/gpt2")
+    assert(entry.get("pipeline_tag").asText() == "text-generation")
+    assert(entry.get("downloads").asLong() == 5000L)
+    assert(entry.get("likes").asLong() == 42L)
+  }
+
+  test("listModels search mode defaults missing id, pipeline_tag, downloads 
and likes") {
+    val raw = 
"""[{"author":"nobody"},{"id":"x","downloads":"lots","likes":null}]"""
+    val response = withStubClient(_ => jsonResponse(raw)) { _ =>
+      resource.listModels("text-generation", "x", null)
+    }
+
+    val models = parseArray(response)
+    assert(models.size() == 2)
+    assert(models.get(0).get("id").asText() == "")
+    assert(models.get(0).get("label").asText() == "")
+    assert(models.get(0).get("pipeline_tag").asText() == "")
+    assert(models.get(0).get("downloads").asLong() == 0L)
+    assert(models.get(0).get("likes").asLong() == 0L)
+    // Non-numeric downloads and a null likes both fall back to 0 rather than 
throwing.
+    assert(models.get(1).get("downloads").asLong() == 0L)
+    assert(models.get(1).get("likes").asLong() == 0L)
+  }
+
+  test("listModels search mode sends the trimmed query and warm-inference 
filters to HF") {
+    val client = withStubClient(_ => jsonResponse("[]")) { c =>
+      resource.listModels("image-classification", "  res net  ", null)
+      c
+    }
+
+    assert(client.requests.size == 1)
+    val url = client.urls.head
+    assert(url.startsWith("https://huggingface.co/api/models?";))
+    assert(url.contains("pipeline_tag=image-classification"))
+    assert(url.contains("filter=image-classification"))
+    assert(url.contains("sort=downloads"))
+    assert(url.contains("direction=-1"))
+    assert(url.contains("limit=100"))
+    assert(url.contains("inference=warm"))
+    // Leading/trailing whitespace is trimmed; the inner space is URL-encoded.
+    assert(url.contains("search=res%20net") || url.contains("search=res+net"), 
url)
+    assert(client.requests.head.authorization.isEmpty)
+  }
+
+  test("listModels search mode forwards the user token as a bearer 
Authorization header") {
+    val client = withStubClient(_ => jsonResponse("[]")) { c =>
+      resource.listModels("text-generation", "gpt2", "  hf_secret  ")
+      c
+    }
+
+    assert(client.requests.head.authorization.contains("Bearer hf_secret"))
+  }
+
+  test("listModels search mode propagates the upstream HF status") {
+    val response = withStubClient(_ => jsonResponse("""{"error":"rate 
limited"}""", status = 429)) {
+      _ => resource.listModels("text-generation", "gpt2", null)
+    }
+
+    assert(response.getStatus == 429)
+    assertErrorBody(response)
+    assert(modelCache.getIfPresent("text-generation") == null, "errors must 
not be cached")
+  }
+
+  test("listModels search mode flags truncation when HF returns SEARCH_LIMIT 
results") {
+    val full = (1 to 100).map(i => hfModel(id = s"model-$i")).mkString("[", 
",", "]")
+    val response = withStubClient(_ => jsonResponse(full)) { _ =>
+      resource.listModels("text-generation", "gpt2", null)
+    }
+
+    assert(response.getStatus == 200)
+    assert(parseArray(response).size() == 100)
+    assert(response.getHeaderString(TRUNCATED_HEADER) == "true")
+  }
+
+  test("listModels search mode does not flag truncation below the search 
limit") {
+    val partial = (1 to 99).map(i => hfModel(id = s"model-$i")).mkString("[", 
",", "]")
+    val response = withStubClient(_ => jsonResponse(partial)) { _ =>
+      resource.listModels("text-generation", "gpt2", null)
+    }
+
+    assert(parseArray(response).size() == 99)
+    assert(response.getHeaderString(TRUNCATED_HEADER) == null)
+  }
+
+  test("listModels search mode bypasses the browse cache in both directions") {
+    modelCache.put("text-generation", """[{"id":"stale-cache-entry"}]""")
+
+    val response = withStubClient(_ => jsonResponse(s"[${hfModel(id = 
"fresh/model")}]")) { c =>
+      val r = resource.listModels("text-generation", "gpt2", null)
+      assert(c.requests.size == 1, "a search must always hit HF, cache or not")
+      r
+    }
+
+    assert(parseArray(response).get(0).get("id").asText() == "fresh/model")
+    // Search results must not overwrite the browse cache slot.
+    assert(modelCache.getIfPresent("text-generation") == 
"""[{"id":"stale-cache-entry"}]""")
+  }
+
+  test("listModels returns 500 when HF returns a malformed search payload") {
+    val response = withStubClient(_ => jsonResponse("not-json-at-all")) { _ =>
+      resource.listModels("text-generation", "gpt2", null)
+    }
+
+    assert(response.getStatus == 500)
+    val node = mapper.readTree(entityString(response))
+    assert(node.get("error").asText() == "Failed to fetch models.")
+  }
+
+  test("listModels treats a whitespace-only search as browse mode") {
+    modelCache.put("text-generation", """[{"id":"cached"}]""")
+
+    val response = withStubClient(_ => fail("browse mode must be served from 
the cache")) { c =>
+      val r = resource.listModels("text-generation", "   ", null)
+      assert(c.requests.isEmpty)
+      r
+    }
+
+    assert(response.getStatus == 200)
+    assert(entityString(response) == """[{"id":"cached"}]""")
+  }
+
+  // ────────────────────────────────────────────────────────────────────────
+  // listModels — browse mode (pagination via the Link header)
+  // ────────────────────────────────────────────────────────────────────────
+
+  test("listModels browse mode fetches a single page, simplifies it, and 
caches the result") {
+    val body = s"[${hfModel(id = "a/one")},${hfModel(id = "b/two")}]"
+    val client = withStubClient(_ => jsonResponse(body)) { c =>
+      val response = resource.listModels("text-generation", null, null)
+      assert(response.getStatus == 200)
+      assert(response.getHeaderString(TRUNCATED_HEADER) == null)
+      val models = parseArray(response)
+      assert(models.size() == 2)
+      assert(models.get(0).get("id").asText() == "a/one")
+      assert(models.get(1).get("id").asText() == "b/two")
+      assert(modelCache.getIfPresent("text-generation") == 
entityString(response))
+      c
+    }
+
+    assert(client.requests.size == 1)
+    val url = client.urls.head
+    assert(url.contains("limit=1000"), s"browse mode uses the page size, got 
$url")
+    assert(url.contains("inference=warm"))
+    assert(!url.contains("search="))
+  }
+
+  test("listModels browse mode follows the Link rel=next chain across pages") {
+    val page2Url = "https://huggingface.co/api/models?cursor=page2";
+    val response = withStubClient { req =>
+      if (req.url == page2Url) jsonResponse(s"[${hfModel(id = "p2/model")}]")
+      else jsonResponse(s"[${hfModel(id = "p1/model")}]", headers = 
nextLinkHeader(page2Url))
+    } { c =>
+      val r = resource.listModels("text-generation", null, null)
+      assert(c.urls.size == 2, s"unexpected request chain: ${c.urls}")
+      assert(c.urls.head.contains("pipeline_tag=text-generation"))
+      assert(c.urls(1) == page2Url)
+      r
+    }
+
+    assert(response.getStatus == 200)
+    val models = parseArray(response)
+    assert(models.size() == 2)
+    assert(models.get(0).get("id").asText() == "p1/model")
+    assert(models.get(1).get("id").asText() == "p2/model")
+    // A complete walk of the chain is not truncated.
+    assert(response.getHeaderString(TRUNCATED_HEADER) == null)
+  }
+
+  test("listModels browse mode ignores a Link header without a next relation") 
{
+    val headers = Map("Link" -> 
"""<https://huggingface.co/api/models?cursor=prev>; rel="prev"""")
+    val client = withStubClient(_ => jsonResponse(s"[${hfModel()}]", headers = 
headers)) { c =>
+      resource.listModels("text-generation", null, null)
+      c
+    }
+
+    assert(client.requests.size == 1)
+  }
+
+  test("listModels browse mode ignores a next Link that has no bracketed URL") 
{
+    val headers = Map("Link" -> """https://huggingface.co/api/models?cursor=2; 
rel="next"""")
+    val client = withStubClient(_ => jsonResponse(s"[${hfModel()}]", headers = 
headers)) { c =>
+      resource.listModels("text-generation", null, null)
+      c
+    }
+
+    assert(client.requests.size == 1)
+  }
+
+  test("listModels browse mode stops at MAX_PAGES and flags the response 
truncated") {
+    val nextUrl = "https://huggingface.co/api/models?cursor=endless";
+    val response = withStubClient { _ =>
+      jsonResponse(s"[${hfModel()}]", headers = nextLinkHeader(nextUrl))
+    } { c =>
+      val r = resource.listModels("text-generation", null, null)
+      assert(c.requests.size == 50, s"expected MAX_PAGES requests, got 
${c.requests.size}")
+      r
+    }
+
+    assert(response.getStatus == 200)
+    assert(parseArray(response).size() == 50)
+    assert(response.getHeaderString(TRUNCATED_HEADER) == "true")
+    // Even a truncated browse result is cached for anonymous callers.
+    assert(modelCache.getIfPresent("text-generation") == 
entityString(response))
+  }
+
+  test("listModels browse mode keeps earlier pages and flags truncation when a 
page fails") {
+    val page2Url = "https://huggingface.co/api/models?cursor=page2";
+    val response = withStubClient { req =>
+      if (req.url == page2Url) jsonResponse("""{"error":"boom"}""", status = 
503)
+      else jsonResponse(s"[${hfModel(id = "p1/model")}]", headers = 
nextLinkHeader(page2Url))
+    } { _ => resource.listModels("text-generation", null, null) }
+
+    assert(response.getStatus == 200)
+    val models = parseArray(response)
+    assert(models.size() == 1)
+    assert(models.get(0).get("id").asText() == "p1/model")
+    assert(response.getHeaderString(TRUNCATED_HEADER) == "true")
+  }
+
+  test("listModels browse mode returns 500 when the first HF page fails") {
+    val response = withStubClient(_ => jsonResponse("""{"error":"boom"}""", 
status = 500)) { _ =>
+      resource.listModels("text-generation", null, null)
+    }
+
+    assert(response.getStatus == 500)
+    val node = mapper.readTree(entityString(response))
+    assert(node.get("error").asText() == "Failed to fetch models.")
+    assert(modelCache.getIfPresent("text-generation") == null, "failures must 
not be cached")
+  }
+
+  test("listModels browse mode with a user token authenticates every page and 
skips the cache") {
+    val page2Url = "https://huggingface.co/api/models?cursor=page2";
+    val client = withStubClient { req =>
+      if (req.url == page2Url) jsonResponse(s"[${hfModel(id = "p2/model")}]")
+      else jsonResponse(s"[${hfModel(id = "p1/model")}]", headers = 
nextLinkHeader(page2Url))
+    } { c =>
+      val response = resource.listModels("text-generation", null, "hf_secret")
+      assert(response.getStatus == 200)
+      assert(parseArray(response).size() == 2)
+      c
+    }
+
+    assert(client.requests.size == 2)
+    assert(client.requests.forall(_.authorization.contains("Bearer 
hf_secret")))
+    assert(
+      modelCache.getIfPresent("text-generation") == null,
+      "token-scoped results must never populate the shared cache"
+    )
+  }
+
+  test("listModels browse mode serves the second call from the cache without 
hitting HF") {
+    val client = withStubClient(_ => jsonResponse(s"[${hfModel(id = 
"a/one")}]")) { c =>
+      val first = resource.listModels("text-generation", null, null)
+      val second = resource.listModels("text-generation", null, null)
+      assert(entityString(first) == entityString(second))
+      c
+    }
+
+    assert(client.requests.size == 1, "the second browse call must be a cache 
hit")
+  }
+
+  // ────────────────────────────────────────────────────────────────────────
+  // listTasks — pipeline-tag listing and availability fan-out
+  // ────────────────────────────────────────────────────────────────────────
+
+  private val tasksUrl = "https://huggingface.co/api/tasks";
+
+  test("listTasks keeps only tags that have at least one warm model") {
+    val tasksBody =
+      """{"text-generation":{"label":"Text 
Generation"},"other-task":{"label":"Other"}}"""
+    val response = withStubClient { req =>
+      if (req.url == tasksUrl) jsonResponse(tasksBody)
+      else if (req.url.contains("pipeline_tag=text-generation")) 
jsonResponse(s"[${hfModel()}]")
+      else jsonResponse("[]")
+    } { _ => resource.listTasks(null) }
+
+    assert(response.getStatus == 200)
+    val tasks = parseArray(response)
+    assert(tasks.size() == 1)
+    assert(tasks.get(0).get("tag").asText() == "text-generation")
+    assert(tasks.get(0).get("label").asText() == "Text Generation")
+    assert(fieldNamesOf(tasks.get(0)) == List("tag", "label"))
+    assert(taskCache.getIfPresent(TASKS_CACHE_KEY) == entityString(response))
+  }
+
+  test("listTasks falls back to the tag when the label is missing or the value 
is not an object") {
+    val tasksBody = """{"no-label":{"foo":1},"scalar-value":"not-an-object"}"""
+    val response = withStubClient { req =>
+      if (req.url == tasksUrl) jsonResponse(tasksBody) else 
jsonResponse(s"[${hfModel()}]")
+    } { _ => resource.listTasks(null) }
+
+    val tasks = parseArray(response)
+    assert(tasks.size() == 2)
+    val labels = (0 until tasks.size()).map { i =>
+      tasks.get(i).get("tag").asText() -> tasks.get(i).get("label").asText()
+    }.toMap
+    assert(labels("no-label") == "no-label")
+    assert(labels("scalar-value") == "scalar-value")
+  }
+
+  test("listTasks probes each tag with limit=1 and warm inference") {
+    val client = withStubClient { req =>
+      if (req.url == tasksUrl) 
jsonResponse("""{"summarization":{"label":"Summarization"}}""")
+      else jsonResponse(s"[${hfModel()}]")
+    } { c =>
+      resource.listTasks(null)
+      c
+    }
+
+    val probe = client.urls.find(_ != tasksUrl).getOrElse(fail("no model probe 
was issued"))
+    assert(probe.contains("pipeline_tag=summarization"))
+    assert(probe.contains("filter=summarization"))
+    assert(probe.contains("limit=1"))
+    assert(probe.contains("inference=warm"))
+  }
+
+  test("listTasks drops a tag whose probe is rate-limited") {
+    val response = withStubClient { req =>
+      if (req.url == tasksUrl) 
jsonResponse("""{"text-generation":{"label":"TG"}}""")
+      else jsonResponse("""{"error":"too many requests"}""", status = 429)
+    } { _ => resource.listTasks(null) }
+
+    assert(response.getStatus == 200)
+    assert(parseArray(response).size() == 0)
+  }
+
+  test("listTasks drops a tag whose probe returns an unexpected status") {
+    val response = withStubClient { req =>
+      if (req.url == tasksUrl) 
jsonResponse("""{"text-generation":{"label":"TG"}}""")
+      else jsonResponse("""{"error":"nope"}""", status = 404)
+    } { _ => resource.listTasks(null) }
+
+    assert(parseArray(response).size() == 0)
+  }
+
+  test("listTasks drops a tag whose probe throws") {
+    val response = withStubClient { req =>
+      if (req.url == tasksUrl) 
jsonResponse("""{"text-generation":{"label":"TG"}}""")
+      else throw new RuntimeException("connection reset")
+    } { _ => resource.listTasks(null) }
+
+    assert(response.getStatus == 200)
+    assert(parseArray(response).size() == 0)
+  }
+
+  test("listTasks propagates the upstream status when the tasks endpoint 
fails") {
+    val response = withStubClient(_ => jsonResponse("""{"error":"bad 
gateway"}""", status = 502)) {
+      _ => resource.listTasks(null)
+    }
+
+    assert(response.getStatus == 502)
+    assertErrorBody(response)
+    assert(taskCache.getIfPresent(TASKS_CACHE_KEY) == null)
+  }
+
+  test("listTasks returns 500 when the tasks payload is malformed") {
+    val response = withStubClient(_ => jsonResponse("{not-json")) { _ => 
resource.listTasks(null) }
+
+    assert(response.getStatus == 500)
+    val node = mapper.readTree(entityString(response))
+    assert(node.get("error").asText() == "Failed to fetch tasks.")
+    assert(taskCache.getIfPresent(TASKS_CACHE_KEY) == null)
+  }
+
+  test("listTasks with a user token authenticates the listing and the probes 
and skips the cache") {
+    val client = withStubClient { req =>
+      if (req.url == tasksUrl) 
jsonResponse("""{"text-generation":{"label":"TG"}}""")
+      else jsonResponse(s"[${hfModel()}]")
+    } { c =>
+      val response = resource.listTasks("hf_secret")
+      assert(response.getStatus == 200)
+      assert(parseArray(response).size() == 1)
+      c
+    }
+
+    assert(client.requests.size == 2)
+    assert(client.requests.forall(_.authorization.contains("Bearer 
hf_secret")))
+    assert(taskCache.getIfPresent(TASKS_CACHE_KEY) == null)
+  }
+
+  test("listTasks serves the second anonymous call from the cache without 
hitting HF") {
+    val client = withStubClient { req =>
+      if (req.url == tasksUrl) 
jsonResponse("""{"text-generation":{"label":"TG"}}""")
+      else jsonResponse(s"[${hfModel()}]")
+    } { c =>
+      val first = resource.listTasks(null)
+      val second = resource.listTasks(null)
+      assert(entityString(first) == entityString(second))
+      c
+    }
+
+    assert(client.requests.size == 2, "the second listTasks call must be 
served from the cache")
+  }
+
+  // ────────────────────────────────────────────────────────────────────────
+  // proxyRemoteMedia — upstream streaming with the size cap
+  // ────────────────────────────────────────────────────────────────────────
+
+  private val allowedMediaUrl = 
"https://cdn-lfs.huggingface.co/generated/image.png";
+
+  test("proxyRemoteMedia returns the upstream bytes and content type for an 
allowlisted host") {
+    val payload = "‰PNG-fake-image-bytes".getBytes(StandardCharsets.UTF_8)
+    val client = withStubClient(_ => bytesResponse(payload, "image/png")) { c 
=>
+      val response = resource.proxyRemoteMedia(allowedMediaUrl)
+      assert(response.getStatus == 200)
+      assert(entityBytes(response).sameElements(payload))
+      assert(response.getMediaType.toString == "image/png")
+      c
+    }
+
+    assert(client.urls == List(allowedMediaUrl))
+  }
+
+  test("proxyRemoteMedia trims the upstream content type") {
+    val response =
+      withStubClient(_ => bytesResponse("x".getBytes(StandardCharsets.UTF_8), 
"  audio/wav  ")) {
+        _ =>
+          resource.proxyRemoteMedia(allowedMediaUrl)
+      }
+
+    assert(response.getMediaType.toString == "audio/wav")
+  }
+
+  test("proxyRemoteMedia falls back to octet-stream when upstream omits the 
content type") {
+    val payload = "bytes".getBytes(StandardCharsets.UTF_8)
+    val response = withStubClient(_ => bytesResponse(payload, null)) { _ =>
+      resource.proxyRemoteMedia(allowedMediaUrl)
+    }
+
+    assert(response.getStatus == 200)
+    assert(response.getMediaType.toString == "application/octet-stream")
+  }
+
+  test("proxyRemoteMedia falls back to octet-stream when the content type is 
blank") {
+    val response =
+      withStubClient(_ => 
bytesResponse("bytes".getBytes(StandardCharsets.UTF_8), "   ")) { _ =>
+        resource.proxyRemoteMedia(allowedMediaUrl)
+      }
+
+    assert(response.getMediaType.toString == "application/octet-stream")
+  }
+
+  test("proxyRemoteMedia propagates a non-200 upstream status") {
+    val response = withStubClient(_ => bytesResponse(Array.emptyByteArray, 
"text/plain", 404)) {
+      _ => resource.proxyRemoteMedia(allowedMediaUrl)
+    }
+
+    assert(response.getStatus == 404)
+    assertErrorBody(response)
+  }
+
+  test("proxyRemoteMedia rejects an upstream declaring a Content-Length above 
the cap") {
+    val declared = Map("Content-Length" -> (MAX_MEDIA_PROXY_BYTES + 
1).toString)
+    val client = withStubClient { _ =>
+      // Body is tiny: the declared length alone must trigger the rejection, so
+      // the oversized payload is never read into heap.
+      bytesResponse("tiny".getBytes(StandardCharsets.UTF_8), "image/png", 
headers = declared)
+    } { c =>
+      val response = resource.proxyRemoteMedia(allowedMediaUrl)
+      assert(response.getStatus == 413)
+      assertErrorBody(response)
+      c
+    }
+
+    assert(client.requests.size == 1)
+  }
+
+  test("proxyRemoteMedia serves the body when Content-Length is present but 
unparsable") {
+    val payload = "ok".getBytes(StandardCharsets.UTF_8)
+    val headers = Map("Content-Length" -> "not-a-number")
+    val response = withStubClient(_ => bytesResponse(payload, "image/png", 
headers = headers)) {
+      _ => resource.proxyRemoteMedia(allowedMediaUrl)
+    }
+
+    assert(response.getStatus == 200)
+    assert(entityBytes(response).sameElements(payload))
+  }
+
+  test("proxyRemoteMedia serves a body whose declared Content-Length is within 
the cap") {
+    val payload = "small".getBytes(StandardCharsets.UTF_8)
+    val headers = Map("Content-Length" -> payload.length.toString)
+    val response = withStubClient(_ => bytesResponse(payload, "image/png", 
headers = headers)) {
+      _ => resource.proxyRemoteMedia(allowedMediaUrl)
+    }
+
+    assert(response.getStatus == 200)
+    assert(entityBytes(response).sameElements(payload))
+  }
+
+  test("proxyRemoteMedia rejects a body that crosses the cap mid-stream") {
+    // No Content-Length at all: the cap has to be enforced while reading.
+    val response = withStubClient { _ =>
+      streamResponse(() => new ZeroInputStream(MAX_MEDIA_PROXY_BYTES + 8192), 
"video/mp4")
+    } { _ => resource.proxyRemoteMedia(allowedMediaUrl) }
+
+    assert(response.getStatus == 413)
+    assertErrorBody(response)
+  }
+
+  test("proxyRemoteMedia returns 500 when the upstream call throws") {
+    val response = withStubClient(_ => throw new RuntimeException("connection 
reset")) { _ =>
+      resource.proxyRemoteMedia(allowedMediaUrl)
+    }
+
+    assert(response.getStatus == 500)
+    val node = mapper.readTree(entityString(response))
+    assert(node.get("error").asText() == "Failed to proxy remote media.")
+  }
 }


Reply via email to