Skip to content

fix(gateway): serve only web endpoint addresses on their domain - #7451

Open
otavio wants to merge 8 commits into
masterfrom
test/e2e-web-endpoints
Open

otavio wants to merge 8 commits into
masterfrom
test/e2e-web-endpoints

Conversation

@otavio

@otavio otavio commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Summary

testcontainers cases for Domain 22 (Web Endpoints) of shellhub-io/team#243 and the web endpoint items of Domain 27 (Gateway), plus the fixes they turned up. Web endpoints are enterprise only, so the cases are TestEnterprise* and run in the validate (tests - enterprise) job against the cloud branch of the same name, shellhub-io/cloud#2653. That PR carries the cloud fixes and must merge with this one.

The worst fix is in cloud. After one request to a web endpoint, the server left the gateway's pooled upstream connection piped to that endpoint's device. Every later request on the web endpoint site then reached that device, whatever its address: another tenant's endpoint, a deleted or expired endpoint, an address no endpoint has. The second, also in cloud, let a user with an accepted device in one namespace delete another namespace's web endpoint through DELETE /api/devices/:uid/tunnels/:address, once a lookup had cached it. The user chose to ship both as plain fixes rather than advisories.

shellhub
fix(server)    leave the web endpoint proxy out of OpenAPI validation   pkg/webendpoints.ProxyPath, skipper
fix(gateway)   serve only web endpoint addresses on their domain       Caddyfile: other subdomains 404
test           serve a test stack's gateway over HTTPS                 WithAutoSSL, HTTPSAddress
test           cover web endpoints end to end                          cmd/device-http, web_endpoints_test.go
test           refuse deleting another namespace's web endpoint        a second user, namespace and device
test(ui)       wait for the namespace deletion before reading what is left   billing.spec.ts flake
fix(openapi)   document the 402 a namespace delete answers while billing refuses
test(ui)       pin the statuses of the namespace deletions             402, then 200

cloud (shellhub-io/cloud#2653)
fix(cloud)     proxy each web endpoint request on its own              ReverseProxy over a dial used once
fix(cloud)     name the device's namespace in the web endpoint list    Relation("Device.Namespace")
fix(cloud)     answer the created tunnel on the deprecated create route
fix(cloud)     reach a merged device through its web endpoints         cache drop + uncached re-read
fix(cloud)     keep a cached web endpoint to the device that holds it  cache hit must name namespace and device
refactor(cloud) declare the web endpoint proxy in the anonymous allowlist
 request to <address>.<domain>, on a pooled gateway connection
   Resolve -> /http/proxy -> LookupWebEndpoint -> dial the device
-  hijack the connection, pipe it to the device until either side closes
-  next request on that connection: straight into the pipe, no lookup
+  ReverseProxy the one request over the tunnel, answer through the router
+  next request on that connection: Resolve and LookupWebEndpoint again

 Host under *.<web endpoint domain>
-  any label          -> server            (console.localhost/internal/metrics answered 200)
+  32 hex digit label -> server
+  anything else      -> 404

The device side is tests/cmd/device-http, built like cmd/enrollment-webhook and started in the gateway's network namespace like the agent, so the agent dials it on 127.0.0.1. It answers with its name and the Host, request URI and TLS state it received, on plain HTTP and on TLS with a self-signed certificate it makes at startup. Every case acts as the namespace owner, so shellhub-io/cloud#2642's tunnel permissions change nothing here.

Lines for the issue, once merged

Domain 22, Web Endpoints:

- [x] `[testcontainers]` Reject non-IP host (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/creating/an_endpoint_for_a_host_that_is_not_an_IP_address_is_refused`)
- [x] `[testcontainers]` Reject port 0 (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/creating/an_endpoint_for_port_0_is_refused`)
- [x] `[testcontainers]` Reject TTL = 0 (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/creating/an_endpoint_for_a_TTL_of_0_is_refused`)
- [x] `[testcontainers]` Reject non-accepted device (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/creating/an_endpoint_for_a_device_not_yet_accepted_is_refused`)
- [x] `[testcontainers]` Create for nonexistent device (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/creating/an_endpoint_for_a_device_the_namespace_does_not_hold_is_refused`)
- [x] `[testcontainers]` Create for nonexistent namespace: refused with 401, because the authenticator rejects a token whose namespace is gone before the service's 404 branch runs (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/creating/an_endpoint_for_a_namespace_that_no_longer_exists_is_refused`)
- [ ] `[testcontainers]` Negative TTL normalizes to -1. Not covered: the API refuses a TTL below -1 with 400 (validator `min=-1`, spec `minimum: -1`), so the model's normalization never runs over HTTP; `TestEnterpriseWebEndpoints/creating/an_endpoint_for_a_TTL_below_-1_is_refused` asserts the refusal (shellhub-io/shellhub#7451)
- [x] `[testcontainers]` Proxy HTTP request to device port (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/proxying/a_request_to_an_endpoint's_address_reaches_the_device's_service_with_its_path_and_the_address_as_Host`)
- [x] `[testcontainers]` Proxy request carries original path (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/proxying/a_request_to_an_endpoint's_address_reaches_the_device's_service_with_its_path_and_the_address_as_Host`)
- [x] `[testcontainers]` Proxy request sets `Host` header (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/proxying/a_request_to_an_endpoint's_address_reaches_the_device's_service_with_its_path_and_the_address_as_Host`)
- [x] `[testcontainers]` Proxy with TLS to device (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/proxying/an_endpoint_over_TLS_that_does_not_verify_reaches_a_self-signed_service,_naming_its_domain_as_Host`)
- [x] `[testcontainers]` Proxy with TLS `verify=false` — self-signed succeeds (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/proxying/an_endpoint_over_TLS_that_does_not_verify_reaches_a_self-signed_service,_naming_its_domain_as_Host`)
- [x] `[testcontainers]` Proxy with TLS `verify=true` — self-signed fails (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/proxying/an_endpoint_over_TLS_that_verifies_refuses_a_self-signed_service`)
- [x] `[testcontainers]` Lookup expired endpoint returns forbidden (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/lifetime/an_endpoint_answers_until_its_TTL_runs_out_and_is_refused_after_it`)
- [x] `[testcontainers]` Endpoint expires after TTL (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/lifetime/an_endpoint_answers_until_its_TTL_runs_out_and_is_refused_after_it`)
- [x] `[testcontainers]` Endpoint TTL renews on access (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/lifetime/an_endpoint_accessed_within_its_TTL_keeps_answering_past_it`)
- [x] `[testcontainers]` Infinite TTL never expires (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/lifetime/an_endpoint_with_a_TTL_of_-1_keeps_answering_after_one_with_a_TTL_has_expired`)
- [x] `[testcontainers]` `ExpiresIn` from `LastAccess` when accessed (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/lifetime/an_accessed_endpoint_expires_its_TTL_after_its_last_access`)
- [x] `[testcontainers]` `ExpiresIn` from `CreatedAt` when never accessed (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/lifetime/an_endpoint_never_accessed_expires_its_TTL_after_its_creation`)
- [x] `[testcontainers]` Delete expired endpoint succeeds (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/lifetime/an_expired_endpoint_can_be_deleted`)
- [ ] `[testcontainers]` Address is MD5 hash. Not covered: the hash mixes in the creation time, so a client cannot recompute it; that it is MD5 is unit-test territory, and no unit test covers it today. `TestEnterpriseWebEndpoints/creating/an_endpoint_answers_under_a_32_hex_digit_address_of_the_web_endpoint_domain` asserts the shape a client sees (shellhub-io/shellhub#7451)
- [x] `[testcontainers]` Same inputs at different times produce different addresses: while the endpoint exists the same device, host and port get it back with its TTL reset; once deleted they get a new address (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/creating/the_same_device,_host_and_port_keep_their_endpoint_and_get_a_new_address_once_it_is_deleted`)
- [x] `[testcontainers]` Resolve middleware extracts address from subdomain (indirect: shellhub-io/shellhub#7451, every `TestEnterpriseWebEndpoints/proxying/*` case reaches its device through the address in the Host)
- [x] `[testcontainers]` Non-matching host passes through: the instance's own domain still reaches the API; any other subdomain of the web endpoint domain now gets the gateway's 404 (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/proxying/a_request_to_the_instance's_own_domain_still_reaches_the_API`, `TestEnterpriseWebEndpoints/proxying/a_subdomain_that_is_not_an_address_is_not_served`)
- [x] `[testcontainers]` API returns 404 when `WebEndpointsFeature` off (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpointsDisabled/the_API_serves_no_web_endpoint_route`)
- [x] `[testcontainers]` Device merge transfers tunnel ownership (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/device_merge`)
- [x] `[testcontainers]` Legacy create tunnel (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/deprecated_tunnel_API`)
- [x] `[testcontainers]` Legacy list tunnels (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/deprecated_tunnel_API`)
- [x] `[testcontainers]` Legacy delete tunnel (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/deprecated_tunnel_API`)

Domain 27, Gateway, Web Endpoint Subdomain Routing:

- [x] `[testcontainers]` Web endpoint subdomain routed (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpoints/proxying/a_request_to_an_endpoint's_address_reaches_the_device's_service_with_its_path_and_the_address_as_Host`)
- [x] `[testcontainers]` Web endpoint internal TLS (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpointsTLS`)
- [ ] `[testcontainers]` Web endpoint DNS-01 challenge. Not covered: issuing a wildcard over DNS-01 needs an ACME CA and a DNS provider the CA can query inside the stack; `TestCaddyfileAdapts` in `gateway/` adapts the `dns` block for each provider (shellhub-io/shellhub#7451)
- [x] `[testcontainers]` No web endpoint site without feature flag (shellhub-io/shellhub#7451, `TestEnterpriseWebEndpointsDisabled/the_gateway_serves_no_site_for_an_endpoint's_address`)

Not duplicated from #7437 or #7429: neither touches web endpoints, tunnels or the gateway's web endpoint site.

Evidence

  • Before, with the cases written against master (the OpenAPI skipper fix applied, cloud and the gateway unchanged):

    proxying/requests_to_two_endpoints_each_reach_their_own_service
      endpoint two -> {"name":"first",...}                                   expected "second"
    proxying/a_request_to_a_deleted_endpoint's_address_is_refused           200 from the device
    proxying/a_request_to_an_address_no_endpoint_has_is_refused             200 from the device
    proxying/an_endpoint_over_TLS_that_verifies_refuses_a_self-signed_service  200 from the device
    lifetime/*                                                               expired endpoints still answered
    lifetime/an_endpoint_never_accessed...      GET /api/web-endpoints 500: /0/device/namespace minimum length 1
    deprecated_tunnel_API                       POST .../tunnels 500: response Content-Type ""
    device_merge (after the proxy fix)          403 web endpoint not found
    proxying/a_subdomain_that_is_not_an_address_is_not_served (after the proxy fix)
      console.localhost/internal/metrics -> 200 "# HELP go_gc_duration_seconds ..."
    server/app TestOpenAPIValidationSkipper//http/proxy                      expected true, actual false
    another_namespace                           DELETE /api/devices/<own device>/tunnels/<their address> 200,
                                                then their endpoint 403 web endpoint not found
    

    Deliberately broken stacks fail the remaining two: TestEnterpriseWebEndpointsDisabled with the feature on (200, 400 and 403 where 404 is expected) and TestEnterpriseWebEndpointsTLS with the gateway choosing DNS-01 instead of internal TLS (remote error: tls: internal error).

    After, every case passes in the test compose runner:

    tests        TestEnterpriseWebEndpoints, TestEnterpriseWebEndpointsDisabled, TestEnterpriseWebEndpointsTLS   PASS
    tests        full suite -skip ^TestEnterprise (32 min)                                                       PASS
    tests        full suite -run ^TestEnterprise                                                                 PASS
    server       go test ./...                                                                                   PASS
    root         go test ./pkg/...                                                                               PASS
    gateway      go test ./...  (TestCaddyfileAdapts and the rest)                                               PASS
    cloud        go test -tags enterprise ./...  (shellhub-io/cloud#2653)                                       PASS
    golangci-lint v2.14.0, uncapped: tests (--build-tags docker), server/app, pkg/webendpoints                   0 issues
    go mod tidy: root, server, tests                                                                             clean
    

Review

Five Codex (gpt-6.1-sol, high effort) rounds on the Standards and Spec axes, over both repos:

  1. On an early spike, before the routing bug was understood: doc contracts missing errors and context, a test doc claiming coverage it did not have yet, a weak suppression reason, CreateWebEndpoint repeating PostWebEndpoint, and the fixture logging "listening" before it bound its listeners. All accepted. Rejected: wrapping the helpers' errors with pkg/errors. The environment helpers return raw errors throughout, and the fixture is a lone main.go built with no module to import from.

  2. Two correctness findings, both accepted:

    • The merge fix dropped the cache inside the accept transaction, so a concurrent lookup could cache the old device again. The store has no commit hook, so LookupWebEndpoint now reads the endpoint again past the cache when its device is gone.
    • The proxy restored forwarding headers that the client's Connection header named, dropped Forwarded, and let the transport add gzip.

    Also accepted: incomplete doc contracts, a duplicated AwaitDeviceOffline, HTTPSAddress reporting port 443 without HTTPS, and a routing unit test that did not prove the connection was reused. On the spec side, the renewal case did not require a request after the first expiry, and its TTLs left CI little slack.

  3. Spec clean. Four doc contracts still incomplete or wrong (CreateTunnel, TunnelUpdateDeviceUID, WebEndpointLookup, and Proxy's error handler discarding a write error). All fixed.

  4. Spec clean. Proxy's contract did not cover http.ErrAbortHandler, which ReverseProxy panics with when copying an answer already started fails. Documented.

  5. Clean on both axes.

The automated review's first pass found two judgement calls here, both accepted and amended into the commits that wrote them:

  • The proxy cases wrote each endpoint request out by hand instead of using the existing builder. deviceService now builds them (plain, secure), and the loopback host is one constant.
  • HTTPSAddress built its address on DockerCompose; it now lives on Stack next to BaseURL and SSHAddress.

It also asked whether cloud saves an endpoint's last access before it answers, which the renewal case relies on. It does: WebEndpointLookup writes it before the proxy dials. billing.spec.ts failed once in e2e (cloud) with a 401 that has nothing to do with web endpoints. The case read the owner's namespaces through a login that could land mid-deletion, and expect.poll does not retry a callback that throws. It now waits for the DELETE's answer first. Pinning the refusal's status turned up a spec gap: the user route's deleteNamespace did not document the 402 billing's refusal answers, so the strict test stack had been turning it into a 500. The spec now documents it, as the admin route already did. The Stripe keys this spec needs are not available to this session locally, so the e2e (cloud) job is its run; ESLint, Prettier and tsc pass on it.

After the passes below came back clean, the two existing problems the review had set aside were fixed here as well, at the user's request: the cross-tenant delete (another namespace above, red then green) and the proxy's anonymous allowance moving into cloud's allowlist.

The second pass found two more, accepted: test cleanups now use context.WithoutCancel(t.Context()), and the require*Refused helpers stop on a wrong status. The third pass found two more, accepted: PostWebEndpoint takes a typed body, and unknownDeviceUID names the zero UID. The cloud PR's findings are listed there. All three web endpoint tests pass again against both amended branches.

Merge Danger

Door: two-way

No migration and no wire format the agent or Connect reads. The agent's side of the HTTP proxy tunnel is unchanged.

Blast Radius: web endpoints

On instances with web endpoints on, a subdomain of the web endpoint domain that is not an address now gets the gateway's 404 instead of reaching the server. Nothing ShellHub serves lives there. The device sees one request per tunnel instead of a kept-alive stream, and the server keeps answering on the gateway connection. Instances with web endpoints off render the same Caddyfile as before. The OpenAPI skip matters only where response validation is on, which production leaves off by default.

A request to a web endpoint's address is rewritten to /http/proxy and answered by the device's own
HTTP service, or by the proxy refusing it. No API schema describes either answer. Strict
validation, which the testcontainers stack runs, replaced the proxy's 403 for an unknown or
expired address with a 500 saying the response does not match the schema.

Cloud registers the route on the core router, so the path moves to pkg/webendpoints, where the
skipper and cloud can both name it.
The site for *.<web endpoint domain> proxied every Host under that domain to the server. A
subdomain that is not an address, such as console.localhost when the web endpoint domain is the
instance's own, went through the server's address resolution untouched and reached any server
route. That includes /internal/metrics, which the main site refuses with 404.

The site now proxies only a Host whose first label is a 32 hex digit address, matched without
regard to case as the server matches it, and answers 404 for any other Host. vars_regexp reads
{http.request.host}, which carries no port.
@otavio
otavio requested review from a team as code owners October 10, 2026 20:52
@otavio otavio changed the title fix(cloud): route every web endpoint request through its own lookup fix(gateway): serve only web endpoint addresses on their domain Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Claude finished @otavio's task in 6m 1s —— View job


Code Review Complete

Reviewed 11 files. 2 inline issues posted. Standards: 0 violations, 2 judgement calls. Spec: not run. Correctness: 0.

Standards

There are no hard violations. Every comment the branch adds is a doc comment on an exported declaration that states its contract. The one nolint gives a real reason. The more useful of the two judgement calls: testWebEndpointProxy writes out WebEndpointCreate{UID, "127.0.0.1", port, -1} by hand instead of using the plain builder that already exists (tests/web_endpoints_test.go:216-236, and again at :413).

Spec

The Spec axis did not run. No issue in this repo is linked. The only reference is Refs: shellhub-io/team#243, and this run cannot read that repo.

Additional notes

  • Correctness: found no defects. What was checked:
    • The Caddy vars_regexp matcher works with the pinned Caddy. {http.request.host} carries no port, and (?i) matches case the same way AddressFromHost does.
    • The OpenAPI skipper matches /http/proxy exactly, so no other route skips validation.
    • Cloud's sibling checkout already uses webendpoints.ProxyPath, so its build doesn't break.
    • The test helpers close every response body and terminate every container.
  • Unverified timing assumption: the "accessed endpoint" lifetime case expects cloud to save the renewed LastAccess before it answers the proxied request. If cloud saves it asynchronously, that case could be flaky.
  • Two lower-value judgement calls were dropped below the bar, so the cap did not apply.

To request another review round, comment /review.

Comment thread tests/web_endpoints_test.go Outdated
Comment thread tests/environment/docker_compose.go
WithAutoSSL adds docker-compose.autossl.yml and publishes HTTPS on a reserved port, so a test can
reach what the gateway serves only over TLS. HTTPSPort reads empty without it, because .env sets
SHELLHUB_HTTPS_PORT=443 for every stack.
@otavio
otavio force-pushed the test/e2e-web-endpoints branch from 9af16f5 to 41d48e6 Compare October 11, 2026 23:10
@otavio

otavio commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Claude finished @otavio's task in 5m 48s —— View job


Code Review Complete

Reviewed 11 files. 2 inline issues posted. Standards: 1 violation, 1 judgement call. Spec: not run. Correctness: 0.

Standards

The violation: CreateWebEndpoint's cleanup at tests/environment/web_endpoint.go:97 calls context.Background(), and so does the DeleteTunnel cleanup at tests/web_endpoints_test.go:388. This breaks go-conventions. Both should use context.WithoutCancel(t.Context()), the way DeleteFirewallRule and DeleteDevice already do.

Spec

The Spec axis did not run. The PR closes no issue in this repo. Its only reference is Refs: shellhub-io/team#243, and this run cannot read that repo.

Additional notes

  • Correctness: no defects found.
    • Caddy matcher: it accepts exactly what AddressFromHost accepts, which is 32 hex digits in any case and no port. It renders the same way across every internal TLS, DNS provider and autossl combination.
    • Main site: it keeps its own exact-host block when the web endpoint domain is the instance's domain.
    • OpenAPI skipper: it matches only /http/proxy, and only response validation is turned off.
    • Cloud: it already uses webendpoints.ProxyPath.
    • Test fixture and helpers: they close every body and container, and their timing margins hold.
  • Dropped judgement calls: none hit the cap. Seven judgement calls fell below the bar and were dropped, including the Tunnel* helper names, which mirror cloud's deprecated route handlers.

To request another review round, comment /review.

Comment thread tests/environment/web_endpoint.go Outdated
Comment thread tests/web_endpoints_test.go Outdated
@otavio
otavio force-pushed the test/e2e-web-endpoints branch from 41d48e6 to 1569026 Compare October 11, 2026 23:26
@otavio

otavio commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Claude finished @otavio's task in 3m 33s —— View job


Code Review Complete

Reviewed 11 files. 2 inline issues posted. Standards: 0 violations, 2 judgement calls. Spec: not run. Correctness: 0.

Standards

There are no hard violations, and all four fixes from the previous round are in place. The more useful of the two judgement calls: PostWebEndpoint takes body any, but every caller passes a WebEndpointCreate, while its sibling PostTunnel is typed (tests/environment/web_endpoint.go:75).

Spec

The Spec axis did not run. The PR closes no issue in this repo. Its only reference is Refs: shellhub-io/team#243, and this run cannot read that repo.

Additional notes

  • Correctness: found no defects. What was checked:
    • Caddy wildcard: *. matches exactly one label, so neither <hex>.x.<domain> nor the bare domain reaches the new matcher. The main site and the dev sites still win by host specificity when the two domains are the same. The TLS and DNS-01 blocks, and the X-Real-IP forwarding, are unchanged.
    • OpenAPI skipper: cloud's Resolve runs as router.Pre, so the skipper sees the rewritten /http/proxy. Cloud's ProxyPath is an alias of the new pkg constant, so its build is unaffected.
    • Routes: no route is added or made anonymous.
  • Below the bar: one judgement call was dropped. It asked the commit message to explain the 30-second retries on the gateway's internal CA and its TLS answer. No cap applied.
  • Possible gap: gateway/caddy_test.go has no rendered-template test for the new 404 on a non-address subdomain. Only the end-to-end case covers it.

To request another review round, comment /review.

Comment thread tests/environment/web_endpoint.go Outdated
Comment thread tests/web_endpoints_test.go Outdated
TestEnterpriseWebEndpoints, TestEnterpriseWebEndpointsDisabled and TestEnterpriseWebEndpointsTLS
cover Domain 22 of shellhub-io/team#243 and the web endpoint items of Domain 27. They run on the
enterprise stack, so the enterprise job runs them against the cloud branch of the same name.

The device side is cmd/device-http, started in the gateway's network namespace like the agent, so
the agent dials it on 127.0.0.1. It answers with its name and the Host, request URI and TLS state
it received, and generates a self-signed certificate for TLS_DOMAIN when it starts. It binds both
listeners before it logs "listening", which is the line the test waits for.

A lookup renews an endpoint's TTL, so the expiry cases wait idle past the TTL and then send one
request.

Refs: shellhub-io/team#243
@otavio
otavio force-pushed the test/e2e-web-endpoints branch from 1569026 to 98546ba Compare October 11, 2026 23:40
@otavio

otavio commented Oct 11, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Claude finished @otavio's task in 2m 1s —— View job


Code Review Complete

Reviewed 11 files on two axes: Standards (repo rules, Go conventions, tests, code smells) and Correctness (security and runtime defects). No new issues found. The code looks good as-is.

The Spec axis did not run. The PR closes no issue in this repo, and its only reference, shellhub-io/team#243, is in a repo this run cannot read.

A user who owns another namespace and an accepted device in it names the
first namespace's endpoint, through the device route and by address. Both
deletes must answer 404 and leave the endpoint answering. The lookup the
case makes first caches the endpoint, which is the state a visit to its URL
leaves behind.

Refs: shellhub-io/team#243
@otavio

otavio commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Claude finished @otavio's task in 2m 21s —— View job


Code Review Complete

I reviewed 12 files on two axes and found no new issues. The code looks good as-is.

  • Standards: repo rules, Go conventions, tests and code smells.
  • Correctness: security and runtime defects.
  • Newest commit (491d472, the cross-namespace delete case): checked closely, nothing to report.

The Spec axis did not run. The PR closes no issue in this repo. Its only reference is Refs: shellhub-io/team#243, and this run cannot read that repo.

submitNamespaceDeletion returned once it clicked, before the DELETE answered, and the case then
polled the owner's namespaces through a fresh login. A login that landed while the DELETE was in
flight got a token naming the namespace, which the DELETE removed before the listing that followed.
The server then cannot resolve the token's role and answers 401. expect.poll calls its callback
outside the try that retries a failed match, so that 401 failed the case instead of being polled
again, as it did in an e2e (cloud) run.

The helper now waits for the DELETE's answer and returns it. The first attempt asserts it failed
and the second that it succeeded. Deleting a namespace is synchronous, so once it has answered the
namespaces can be read once, without polling.
@otavio
otavio requested a review from a team as a code owner October 12, 2026 02:27
@otavio

otavio commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Claude finished @otavio's task in 2m 16s —— View job


Code Review Complete

Reviewed 13 files. 1 inline issue posted. Standards: 1 violation, 0 judgement calls. Spec: not run. Correctness: 0.

Standards

The violation is in the newest commit, ce17f39, at ui/apps/console/e2e/billing.spec.ts:292. It breaks the react-conventions "Known answer" rule. expect(deletion.ok()).toBe(false) passes for any non-2xx answer, so a 401, 404 or 500 satisfies it as well as the billing refusal does. The comment asks to pin the exact status. The error's outer NewErrInvalid points to 400, and the e2e (cloud) run will show which status the server really sends.

Spec

The Spec axis did not run. The PR closes no issue in this repo. Its only reference is Refs: shellhub-io/team#243, and this run cannot read that repo.

Additional notes

  • Correctness: no defects. What was checked:
    • Caddyfile template: it renders correctly with internal TLS, with a DNS provider, without autossl, and when the web endpoint domain is the instance's own domain.
    • OpenAPI skipper: it matches only /http/proxy.
    • Cloud: its ProxyPath alias and anonymous allowlist still compile against the new pkg constant.
    • ce17f39: registering waitForResponse before the click removes the race with the login.
  • Dropped: one judgement call fell below the bar and was not posted. It was the repeated delete-then-require-200 block in tests/web_endpoints_test.go. No cap applied.

To request another review round, comment /review.

Comment thread ui/apps/console/e2e/billing.spec.ts Outdated
…ng refuses

Deleting a namespace whose billing still holds an active subscription is refused with
ErrBillingReportNamespaceDelete, whose code maps to 402 Payment Required. The admin delete route
documents that answer; the user route did not, so a server validating its responses strictly
replaced the 402 with a 500, and a client reading the spec had no 402 to expect.
The billing case asserted only that its first deletion failed and its second succeeded, which any
error answer satisfies, the 401 of the login race fixed two commits back included. It now expects
the 402 that billing's refusal answers, which the spec documents since the previous commit, and the
200.
@otavio

otavio commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Claude finished @otavio's task in 2m 32s —— View job


Code Review Complete

Reviewed 14 files on two axes and found no new issues. The code looks good as-is.

  • Standards: repo rules, Go and React conventions, tests and code smells.
  • Correctness: security and runtime defects.
  • Newest commits: checked closely, nothing to report.
    • ce17f39: the response wait is registered before the click.
    • d969ded: the 402 $ref resolves to the existing responses/402.yaml.
    • b8b49b5: the refusal and the retry are pinned to 402 and 200.

The Spec axis did not run. The PR closes no issue in this repo. Its only reference is Refs: shellhub-io/team#243, and this run cannot read that repo.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant