Repository navigation
test(lk): run lk from scripts against a fake LiveKit Cloud - #1069
Conversation
6e47027 to
81c5844
Compare
81c5844 to
7d625e4
Compare
Each script in cmd/lk/testdata/script runs lk the way a user types it: real args and env, a crafted CLI config under a temporary $HOME, and the stdout, stderr and exit code the run produces. testscript (rogpeppe/go-internal) re-executes the test binary as lk in a process per case, since lk's flags are package-level and cli/v3 keeps their parse state. A fake LiveKit Cloud, one httptest server set as lk's HTTP proxy, answers any http://NAME.test host with canned responses and records the credential and project each request carried, so a case can assert on what reached the server as well as on the output. Anything else lk sends through the proxy is refused and logged, so it shows up in the request log. UPDATE_LK_SCRIPTS=1 regenerates the expected files. A canned Public API body has to be what the spec says its route sends. The fake decodes it strictly into the type the client is generated with, or into a google.rpc.Status for an error, so a script can't drift from the spec, and a regeneration that changes a shape fails the scripts still sending the old one. The 401 and 500 bodies the auth middleware and panic recovery send are described only in prose, so they get a small type of their own. The first row: with no stored session, a session-only command says to sign in and exits non-zero.
An expired session is refused before any call, and a session the Public API rejects fails as Unauthenticated; both say how to sign in again. A session-only command in API-key mode, and a command not yet available under --user-auth, each say which mode they need. --experimental-auth still selects user auth, and passing both mode flags is an error. Three rows check what holds today and skip at the check that doesn't: with API keys in the environment lk doesn't say --user-auth ignores them, the 401 the Public API sends is shown as raw JSON, and the error for a default user that no longer resolves doesn't say so.
A PermissionDenied from the Public API, with the body it really sends, fails with a non-zero exit and carries cloudAPIError's hint. The check that the message also names the project is skipped as a known bug. How each refusal reads (the sign-in hint on Unauthenticated, why and what to do instead on PermissionDenied, NotFound passed through) is a unit test of cloudAPIError, since that is one function and needs no process.
API-key mode: --project beats credentials in the environment, --subdomain and a livekit.toml each pick their project over the default, the default is used when nothing else names one, and no default is a clear error in non-interactive mode. User auth: the default project is used, a user who can reach only one project acts on it, and one email signed in to staging and to prod keeps both sessions, with each command sending the session for the server it talks to. Two users on the same Public API with no default is a refusal; --user picks one. Two rows skip as known bugs: a --project that names no configured project falls back to the only project instead of failing, and the two-users refusal says no user is signed in instead of asking for --user.
A project ADMIN lists, creates and deletes the project's API keys. A member below ADMIN listing them gets the refusal with cloudAPIError's hint and a non-zero exit. lk never sees the role, only the Public API's refusal, which is the same for READ and WRITE, so one refused identity covers both.
7d625e4 to
e07a424
Compare
| ) | ||
|
|
||
| func TestMain(m *testing.M) { | ||
| testscript.Main(m, map[string]func(){"lk": main}) |
There was a problem hiding this comment.
testscript.Main copies the whole test binary to a temp bin/lk and puts it first on PATH for every test in cmd/lk. That includes the Windows runs in windows.yaml, where TestScripts always skips, so the copy is never used there, and any test in the package that runs lk from PATH gets the test binary. Skipping the setup where the scripts don't run:
| testscript.Main(m, map[string]func(){"lk": main}) | |
| if runtime.GOOS == "windows" { | |
| os.Exit(m.Run()) | |
| } | |
| testscript.Main(m, map[string]func(){"lk": main}) |
There was a problem hiding this comment.
good catch for this pr on its own, but i'd rather keep it: #1077 drops the windows skip so the scripts run there too (windows ci is green on it), and #1072's property test finds lk with exec.LookPath("lk"), so it needs testscript.Main to install it on windows as well. skipping the setup here would just get reverted two prs up. agree it means anything in cmd/lk that runs lk from PATH gets the test binary. right now that's only the property test, which wants exactly that
mistyped-project skipped before running lk, so it checked nothing. It now records today's fallback before the skip: lk says it's using project one and sends one's key, so the fix shows up as a diff there. The fake: - keys a canned response the way it looks one up, without the query, as the respond docs say; - rejects a Public API body with anything after its JSON value, such as an extra closing brace, which dec.More let through; - recognises the middleware's 401/500 body by its "error" field rather than by the bytes it starts with; - keeps the caller's GORACE and appends atexit_sleep_ms=0 to it; - answers a missing Twirp response with a Twirp error, so lk shows which response the script didn't give.
Why
lkhas two auth modes now, and nothing tested the CLI the way a user types it: root flags, env vars, the config file and the stored session together deciding which credential and which project a command uses. These scripts pin that behaviour so a change to the resolver shows up as a failing script, not a user acting on the wrong project.What changed
cmd/lk/script_test.go: a testscript (rogpeppe/go-internal) harness. Eachcmd/lk/testdata/script/*.txtarrunslkas its own process, with real args and env and a crafted CLI config under a temporary$HOME, and checks stdout, stderr and the exit code. One process per case is needed because lk's flags are package-level and cli/v3 keeps their parse state.http://NAME.testhost with canned responses (respond) and records the credential and project of every request (requests). Anything else lk sends through the proxy is refused and logged, so it shows up in the request log.UPDATE_LK_SCRIPTS=1regenerates expected files.google.rpc.Statusfor an error, so a script can't drift from the spec, and a regeneration that changes a shape fails the scripts still sending the old one. The 401 and 500 bodies from the auth middleware and panic recovery are described only in prose, so they get a small type of their own until they become agoogle.rpc.Status.--experimental-authalias and both mode flags at once;--projectover env credentials,--subdomain, alivekit.toml, the default project, no default, and a mistyped--project;--user, and a user's only project picked automatically;TestCloudAPIError: how each refusal reads (the sign-in hint on Unauthenticated, why and what to do on PermissionDenied, NotFound passed through) is one function, so it's a unit test. The scripts only check that commands attach it.lkrun per case: real args, env, a config file under a temporary$HOME, the requests lk sends, and the exit code, each in its own process since lk's flags keep parse state. testscript gives that with one readable file per case, and follows the VRT convention for regenerating (UPDATE_LK_SCRIPTS).--user;--projectfalls back to the only project;USERPROFILE, and a Windows checkout gives the.txtarfiles CRLF endings.Testing
go build ./...,go vet ./...,go test -count=1 ./...,go test -race ./cmd/lkand golangci-lint all pass, and each commit builds and passes on its own.-racetime forcmd/lkgoes from 29.7s to 32.0s.Stacked on this