improve io-tests

Tests are now run through unix socket by default allowing
parallel execution. Test setup configuration is done via
environment variables instead of config files.
This commit is contained in:
Remo Rechkemmer
2020-12-24 16:09:25 +01:00
committed by GitHub
parent b5185de706
commit 2425cddaed
12 changed files with 117 additions and 90 deletions
+15 -1
View File
@@ -160,7 +160,21 @@ $ nix-shell --run postgrest-test-spec-all
# Run the tests against a specific version of PostgreSQL (use tab-completion in # Run the tests against a specific version of PostgreSQL (use tab-completion in
# nix-shell to see all available versions): # nix-shell to see all available versions):
$ nix-shell --run postgrest-test-spec-postgresql-9.5 $ nix-shell --run postgrest-test-spec-postgresql-13
```
The io-test that test PostgREST as a black box with inputs and outputs can be
run with `postgrest-test-io`. The test runner under the hood is
[pytest](https://docs.pytest.org/) and you can pass it the usual options:
```bash
# Filter the tests to run by name, including all that contain 'config':
postgrest-test-io -k config
# Run tests in parallel using xdist, specifying the number of processes:
postgrest-test-io -n auto
postgrest-test-io -n 8
``` ```
+3 -2
View File
@@ -90,11 +90,12 @@ let
ioTestPython = ioTestPython =
python3.withPackages (ps: [ python3.withPackages (ps: [
ps.pyjwt
ps.pytest ps.pytest
ps.pytest_xdist
ps.pyyaml
ps.requests ps.requests
ps.requests-unixsocket ps.requests-unixsocket
ps.pyjwt
ps.pyyaml
]); ]);
testIO = testIO =
@@ -1,6 +1,4 @@
db-pool = 1 db-pool = 1
db-pool-timeout = 1 db-pool-timeout = 1
server-host = "127.0.0.1"
server-port = 49421
app.settings.external_api_secret = "0123456789abcdef" app.settings.external_api_secret = "0123456789abcdef"
@@ -1,6 +1,4 @@
db-pool = 1 db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
# Read secret from a file: /dev/stdin (alias for standard input) # Read secret from a file: /dev/stdin (alias for standard input)
jwt-secret = "@/dev/stdin" jwt-secret = "@/dev/stdin"
@@ -1,5 +1,3 @@
db-uri = "@/dev/stdin" db-uri = "@/dev/stdin"
db-pool = 1 db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
jwt-secret = "reallyreallyreallyreallyverysafe" jwt-secret = "reallyreallyreallyreallyverysafe"
@@ -1,5 +1,3 @@
db-pool = 1 db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
jwt-role-claim-key = "$(ROLE_CLAIM_KEY)" jwt-role-claim-key = "$(ROLE_CLAIM_KEY)"
jwt-secret = "reallyreallyreallyreallyverysafe" jwt-secret = "reallyreallyreallyreallyverysafe"
@@ -1,6 +1,4 @@
db-pool = 1 db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
# Read secret from a file: /dev/stdin (alias for standard input) # Read secret from a file: /dev/stdin (alias for standard input)
jwt-secret = "@/dev/stdin" jwt-secret = "@/dev/stdin"
@@ -1,7 +1,5 @@
db-schemas = "test" db-schemas = "test"
db-pool = 1 db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
app.settings.name_var = "John" app.settings.name_var = "John"
jwt-secret = "invalidinvalidinvalidinvalidinvalid" jwt-secret = "invalidinvalidinvalidinvalidinvalid"
-2
View File
@@ -1,4 +1,2 @@
db-pool = 1 db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
jwt-secret = "reallyreallyreallyreallyverysafe" jwt-secret = "reallyreallyreallyreallyverysafe"
+95 -69
View File
@@ -3,11 +3,14 @@
import contextlib import contextlib
import dataclasses import dataclasses
from datetime import datetime from datetime import datetime
import pathlib
import subprocess
from operator import attrgetter from operator import attrgetter
import os import os
import pathlib
import shutil
import signal import signal
import socket
import subprocess
import tempfile
import time import time
import urllib.parse import urllib.parse
@@ -21,7 +24,7 @@ import yaml
BASEDIR = pathlib.Path(os.path.realpath(__file__)).parent BASEDIR = pathlib.Path(os.path.realpath(__file__)).parent
CONFIGSDIR = BASEDIR / "configs" CONFIGSDIR = BASEDIR / "configs"
FIXTURES = yaml.load((BASEDIR / "fixtures.yaml").read_text(), Loader=yaml.Loader) FIXTURES = yaml.load((BASEDIR / "fixtures.yaml").read_text(), Loader=yaml.Loader)
BASEURL = "http://127.0.0.1:49421" POSTGREST_BIN = shutil.which("postgrest")
SECRET = "reallyreallyreallyreallyverysafe" SECRET = "reallyreallyreallyreallyverysafe"
@@ -41,7 +44,9 @@ class PostgrestSession(requests_unixsocket.Session):
self.baseurl = baseurl self.baseurl = baseurl
def request(self, method, url, *args, **kwargs): def request(self, method, url, *args, **kwargs):
fullurl = urllib.parse.urljoin(self.baseurl, url) # Not using urllib.parse.urljoin to compose the url, as it doesn't play
# well with our 'http+unix://' unix domain socket urls.
fullurl = self.baseurl + url
return super(PostgrestSession, self).request(method, fullurl, *args, **kwargs) return super(PostgrestSession, self).request(method, fullurl, *args, **kwargs)
@@ -58,25 +63,27 @@ def dburi():
return os.getenv("PGRST_DB_URI").encode("utf-8") return os.getenv("PGRST_DB_URI").encode("utf-8")
def mkenv(moreenv): @pytest.fixture
""" def defaultenv():
Create env from os.environ and moreenv, while "Default environment for PostgREST."
filtering None values to allow overriding "unset". return {
""" "PGRST_DB_URI": os.environ["PGRST_DB_URI"],
env = {**os.environ, **(moreenv or {})} "PGRST_DB_SCHEMAS": os.environ["PGRST_DB_SCHEMAS"],
return {k: v for k, v in env.items() if v is not None} "PGRST_DB_ANON_ROLE": os.environ["PGRST_DB_ANON_ROLE"],
}
def dumpconfig(configpath=None, moreenv=None, stdin=None): def dumpconfig(configpath=None, env=None, stdin=None):
"Dump the config as parsed by PostgREST." "Dump the config as parsed by PostgREST."
command = [POSTGREST_BIN, "--dump-config"]
command = ["postgrest", "--dump-config"]
if configpath: if configpath:
command += [configpath] command.append(configpath)
process = subprocess.Popen( process = subprocess.Popen(
command, env=mkenv(moreenv), stdin=subprocess.PIPE, stdout=subprocess.PIPE command, env=env or {}, stdin=subprocess.PIPE, stdout=subprocess.PIPE
) )
process.stdin.write(stdin or b"") process.stdin.write(stdin or b"")
result = process.communicate()[0] result = process.communicate()[0]
process.kill() process.kill()
@@ -87,27 +94,44 @@ def dumpconfig(configpath=None, moreenv=None, stdin=None):
@contextlib.contextmanager @contextlib.contextmanager
def run(configpath, stdin=None, moreenv=None, socket=None): def run(configpath=None, stdin=None, env=None, port=None):
"Run PostgREST and yield an endpoint that is ready for connections." "Run PostgREST and yield an endpoint that is ready for connections."
if socket: with tempfile.TemporaryDirectory() as tmpdir:
baseurl = "http+unix://" + urllib.parse.quote_plus(str(socket)) if port:
else: env["PGRST_SERVER_PORT"] = str(port)
baseurl = BASEURL env["PGRST_SERVER_HOST"] = "localhost"
baseurl = f"http://localhost:{port}"
else:
socketfile = pathlib.Path(tmpdir) / "postgrest.sock"
env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile)
baseurl = "http+unix://" + urllib.parse.quote_plus(str(socketfile))
command = ["postgrest", configpath] command = [POSTGREST_BIN]
process = subprocess.Popen(command, stdin=subprocess.PIPE, env=mkenv(moreenv))
try: if configpath:
process.stdin.write(stdin or b"") command.append(configpath)
process.stdin.close()
wait_until_ready(baseurl) process = subprocess.Popen(command, stdin=subprocess.PIPE, env=env or {})
yield PostgrestProcess(process=process, session=PostgrestSession(baseurl)) try:
finally: process.stdin.write(stdin or b"")
process.kill() process.stdin.close()
process.wait()
wait_until_ready(baseurl)
yield PostgrestProcess(process=process, session=PostgrestSession(baseurl))
finally:
process.kill()
process.wait()
def freeport():
"Find a free port on localhost."
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
s.bind(("", 0))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return s.getsockname()[1]
def wait_until_ready(url): def wait_until_ready(url):
@@ -153,8 +177,7 @@ def test_expected_config(expectedconfig):
expected = expectedconfig.read_text() expected = expectedconfig.read_text()
config = CONFIGSDIR / expectedconfig.name config = CONFIGSDIR / expectedconfig.name
unset = {"PGRST_DB_URI": None, "PGRST_DB_ANON_ROLE": None, "PGRST_DB_SCHEMAS": None} assert dumpconfig(config) == expected
assert dumpconfig(config, moreenv=unset) == expected
def test_expected_config_from_environment(): def test_expected_config_from_environment():
@@ -164,7 +187,7 @@ def test_expected_config_from_environment():
env = {k: str(v) for k, v in yaml.load(envfile, Loader=yaml.Loader).items()} env = {k: str(v) for k, v in yaml.load(envfile, Loader=yaml.Loader).items()}
expected = (CONFIGSDIR / "expected" / "no-defaults.config").read_text() expected = (CONFIGSDIR / "expected" / "no-defaults.config").read_text()
assert dumpconfig(moreenv=env) == expected assert dumpconfig(env=env) == expected
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -172,7 +195,7 @@ def test_expected_config_from_environment():
[conf for conf in CONFIGSDIR.iterdir() if conf.suffix == ".config"], [conf for conf in CONFIGSDIR.iterdir() if conf.suffix == ".config"],
ids=attrgetter("name"), ids=attrgetter("name"),
) )
def test_stable_config(tmp_path, config): def test_stable_config(tmp_path, config, defaultenv):
""" """
A dumped, re-read and re-dumped config should match the dumped config. A dumped, re-read and re-dumped config should match the dumped config.
@@ -184,30 +207,27 @@ def test_stable_config(tmp_path, config):
# Set environment variables that some of the configs expect. Using a # Set environment variables that some of the configs expect. Using a
# complex ROLE_CLAIM_KEY to make sure quoting works. # complex ROLE_CLAIM_KEY to make sure quoting works.
env = { env = {
**defaultenv,
"ROLE_CLAIM_KEY": '."https://www.example.com/roles"[0].value', "ROLE_CLAIM_KEY": '."https://www.example.com/roles"[0].value',
"POSTGREST_TEST_SOCKET": "/tmp/postgrest.sock", "POSTGREST_TEST_SOCKET": "/tmp/postgrest.sock",
"POSTGREST_TEST_PORT": "80",
} }
# Some configs expect input from stdin, at least on base64. # Some configs expect input from stdin, at least on base64.
stdin = b"Y29ubmVjdGlvbl9zdHJpbmc=" stdin = b"Y29ubmVjdGlvbl9zdHJpbmc="
dumped = dumpconfig(config, moreenv=env, stdin=stdin) dumped = dumpconfig(config, env=env, stdin=stdin)
tmpconfigpath = tmp_path / "config" tmpconfigpath = tmp_path / "config"
tmpconfigpath.write_text(dumped) tmpconfigpath.write_text(dumped)
redumped = dumpconfig(tmpconfigpath, moreenv=env) redumped = dumpconfig(tmpconfigpath, env=env)
assert dumped == redumped assert dumped == redumped
def test_socket_connection(tmp_path): def test_port_connection(defaultenv):
"Connections via unix domain sockets should work." "Connections via a port on localhost should work."
socket = tmp_path / "postgrest.sock" with run(env=defaultenv, port=freeport()):
env = {
"POSTGREST_TEST_SOCKET": str(socket),
}
with run(CONFIGSDIR / "unix-socket.config", socket=socket, moreenv=env):
pass pass
@@ -216,7 +236,7 @@ def test_socket_connection(tmp_path):
[path for path in (BASEDIR / "secrets").iterdir() if path.suffix != ".jwt"], [path for path in (BASEDIR / "secrets").iterdir() if path.suffix != ".jwt"],
ids=attrgetter("name"), ids=attrgetter("name"),
) )
def test_read_secret_from_file(secretpath): def test_read_secret_from_file(secretpath, defaultenv):
"Authorization should succeed when the secret is read from a file." "Authorization should succeed when the secret is read from a file."
if secretpath.suffix == ".b64": if secretpath.suffix == ".b64":
configfile = CONFIGSDIR / "base64-secret-from-file.config" configfile = CONFIGSDIR / "base64-secret-from-file.config"
@@ -226,53 +246,59 @@ def test_read_secret_from_file(secretpath):
secret = secretpath.read_bytes() secret = secretpath.read_bytes()
headers = authheader(secretpath.with_suffix(".jwt").read_text()) headers = authheader(secretpath.with_suffix(".jwt").read_text())
with run(configfile, stdin=secret) as postgrest: with run(configfile, stdin=secret, env=defaultenv) as postgrest:
response = postgrest.session.get("/authors_only", headers=headers) response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 200 assert response.status_code == 200
def test_read_dburi_from_file_without_eol(dburi): def test_read_dburi_from_file_without_eol(dburi, defaultenv):
"Reading the dburi from a file with a single line should work." "Reading the dburi from a file with a single line should work."
config = CONFIGSDIR / "dburi-from-file.config" config = CONFIGSDIR / "dburi-from-file.config"
unset = {"PGRST_DB_URI": None} env = {key: value for key, value in defaultenv.items() if key != "PGRST_DB_URI"}
with run(config, moreenv=unset, stdin=dburi): with run(config, env=env, stdin=dburi):
pass pass
def test_read_dburi_from_file_with_eol(dburi): def test_read_dburi_from_file_with_eol(dburi, defaultenv):
"Reading the dburi from a file containing a newline should work." "Reading the dburi from a file containing a newline should work."
config = CONFIGSDIR / "dburi-from-file.config" config = CONFIGSDIR / "dburi-from-file.config"
unset = {"PGRST_DB_URI": None} env = {key: value for key, value in defaultenv.items() if key != "PGRST_DB_URI"}
with run(config, moreenv=unset, stdin=dburi + b"\n"): with run(config, env=env, stdin=dburi + b"\n"):
pass pass
@pytest.mark.parametrize( @pytest.mark.parametrize(
"roleclaim", FIXTURES["roleclaims"], ids=lambda claim: claim["key"] "roleclaim", FIXTURES["roleclaims"], ids=lambda claim: claim["key"]
) )
def test_role_claim_key(roleclaim): def test_role_claim_key(roleclaim, defaultenv):
"Authorization should depend on a correct role-claim-key and JWT claim." "Authorization should depend on a correct role-claim-key and JWT claim."
env = {"ROLE_CLAIM_KEY": roleclaim["key"]} env = {
**defaultenv,
"ROLE_CLAIM_KEY": roleclaim["key"],
}
headers = jwtauthheader(roleclaim["data"], SECRET) headers = jwtauthheader(roleclaim["data"], SECRET)
with run(CONFIGSDIR / "role-claim-key.config", moreenv=env) as postgrest: with run(CONFIGSDIR / "role-claim-key.config", env=env) as postgrest:
response = postgrest.session.get("/authors_only", headers=headers) response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == roleclaim["expected_status"] assert response.status_code == roleclaim["expected_status"]
@pytest.mark.parametrize("invalidroleclaimkey", FIXTURES["invalidroleclaimkeys"]) @pytest.mark.parametrize("invalidroleclaimkey", FIXTURES["invalidroleclaimkeys"])
def test_invalid_role_claim_key(invalidroleclaimkey): def test_invalid_role_claim_key(invalidroleclaimkey, defaultenv):
"Given an invalid role-claim-key, Postgrest should exit with a non-zero exit code." "Given an invalid role-claim-key, Postgrest should exit with a non-zero exit code."
env = {"ROLE_CLAIM_KEY": invalidroleclaimkey} env = {
**defaultenv,
"ROLE_CLAIM_KEY": invalidroleclaimkey,
}
with pytest.raises(PostgrestError): with pytest.raises(PostgrestError):
dump = dumpconfig(CONFIGSDIR / "role-claim-key.config", moreenv=env) dump = dumpconfig(CONFIGSDIR / "role-claim-key.config", env=env)
for line in dump.split("\n"): for line in dump.split("\n"):
if line.startswith("jwt-role-claim-key"): if line.startswith("jwt-role-claim-key"):
print(line) print(line)
def test_iat_claim(): def test_iat_claim(defaultenv):
""" """
A claim with an 'iat' (issued at) attribute should be successful. A claim with an 'iat' (issued at) attribute should be successful.
@@ -283,7 +309,7 @@ def test_iat_claim():
claim = {"role": "postgrest_test_author", "iat": datetime.utcnow()} claim = {"role": "postgrest_test_author", "iat": datetime.utcnow()}
headers = jwtauthheader(claim, SECRET) headers = jwtauthheader(claim, SECRET)
with run(CONFIGSDIR / "simple.config") as postgrest: with run(CONFIGSDIR / "simple.config", env=defaultenv) as postgrest:
for _ in range(10): for _ in range(10):
response = postgrest.session.get("/authors_only", headers=headers) response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 200 assert response.status_code == 200
@@ -291,14 +317,14 @@ def test_iat_claim():
time.sleep(0.5) time.sleep(0.5)
def test_app_settings(): def test_app_settings(defaultenv):
""" """
App settings should not reset when the db pool times out. App settings should not reset when the db pool times out.
See: https://github.com/PostgREST/postgrest/issues/1141 See: https://github.com/PostgREST/postgrest/issues/1141
""" """
with run(CONFIGSDIR / "app-settings.config") as postgrest: with run(CONFIGSDIR / "app-settings.config", env=defaultenv) as postgrest:
# Wait for the db pool to time out, set to 1s in config # Wait for the db pool to time out, set to 1s in config
time.sleep(2) time.sleep(2)
@@ -309,14 +335,14 @@ def test_app_settings():
assert response.text == '"0123456789abcdef"' assert response.text == '"0123456789abcdef"'
def test_app_settings_reload(tmp_path): def test_app_settings_reload(tmp_path, defaultenv):
"App settings should be reloaded when PostgREST is sent SIGUSR2." "App settings should be reloaded when PostgREST is sent SIGUSR2."
config = (CONFIGSDIR / "sigusr2-settings.config").read_text() config = (CONFIGSDIR / "sigusr2-settings.config").read_text()
configfile = tmp_path / "test.config" configfile = tmp_path / "test.config"
configfile.write_text(config) configfile.write_text(config)
uri = "/rpc/get_guc_value?name=app.settings.name_var" uri = "/rpc/get_guc_value?name=app.settings.name_var"
with run(configfile) as postgrest: with run(configfile, env=defaultenv) as postgrest:
response = postgrest.session.get(uri) response = postgrest.session.get(uri)
assert response.status_code == 200 assert response.status_code == 200
assert response.text == '"John"' assert response.text == '"John"'
@@ -333,7 +359,7 @@ def test_app_settings_reload(tmp_path):
assert response.text == '"Jane"' assert response.text == '"Jane"'
def test_jwt_secret_reload(tmp_path): def test_jwt_secret_reload(tmp_path, defaultenv):
"JWT secret should be reloaded when PostgREST is sent SIGUSR2." "JWT secret should be reloaded when PostgREST is sent SIGUSR2."
config = (CONFIGSDIR / "sigusr2-settings.config").read_text() config = (CONFIGSDIR / "sigusr2-settings.config").read_text()
configfile = tmp_path / "test.config" configfile = tmp_path / "test.config"
@@ -341,7 +367,7 @@ def test_jwt_secret_reload(tmp_path):
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET) headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
with run(configfile) as postgrest: with run(configfile, env=defaultenv) as postgrest:
response = postgrest.session.get("/authors_only", headers=headers) response = postgrest.session.get("/authors_only", headers=headers)
assert response.status_code == 401 assert response.status_code == 401
@@ -357,16 +383,16 @@ def test_jwt_secret_reload(tmp_path):
assert response.status_code == 200 assert response.status_code == 200
def test_db_schema_reload(tmp_path): def test_db_schema_reload(tmp_path, defaultenv):
"DB schema should be reloaded when PostgREST is sent SIGUSR2." "DB schema should be reloaded when PostgREST is sent SIGUSR2."
config = (CONFIGSDIR / "sigusr2-settings.config").read_text() config = (CONFIGSDIR / "sigusr2-settings.config").read_text()
configfile = tmp_path / "test.config" configfile = tmp_path / "test.config"
configfile.write_text(config) configfile.write_text(config)
headers = {"Accept-Profile": "v1"} headers = {"Accept-Profile": "v1"}
unset = {"PGRST_DB_SCHEMAS": None} env = {key: value for key, value in defaultenv.items() if key != "PGRST_DB_SCHEMAS"}
with run(configfile, moreenv=unset) as postgrest: with run(configfile, env=env) as postgrest:
response = postgrest.session.get("/parents", headers=headers) response = postgrest.session.get("/parents", headers=headers)
assert response.status_code == 404 assert response.status_code == 404
+3 -3
View File
@@ -5,10 +5,12 @@
set -eu set -eu
pgrPort=49421
# PGRST_DB_URI, PGRST_DB_ANON_ROLE and PGRST_DB_SCHEMAS are expected to be set by with_tmp_db # PGRST_DB_URI, PGRST_DB_ANON_ROLE and PGRST_DB_SCHEMAS are expected to be set by with_tmp_db
export PGRST_DB_POOL="1" export PGRST_DB_POOL="1"
export PGRST_SERVER_HOST="127.0.0.1" export PGRST_SERVER_HOST="127.0.0.1"
export PGRST_SERVER_PORT="49421" export PGRST_SERVER_PORT="$pgrPort"
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe" export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
trap "kill 0" int term exit trap "kill 0" int term exit
@@ -19,8 +21,6 @@ result(){ echo "$1 $currentTest $2"; currentTest=$(( $currentTest + 1 )); }
ok(){ result 'ok' "- $1"; } ok(){ result 'ok' "- $1"; }
ko(){ result 'not ok' "- $1"; failedTests=$(( $failedTests + 1 )); } ko(){ result 'not ok' "- $1"; failedTests=$(( $failedTests + 1 )); }
pgrPort=49421
pgrStart(){ postgrest +RTS -p -h > /dev/null & pgrPID="$!"; } pgrStart(){ postgrest +RTS -p -h > /dev/null & pgrPID="$!"; }
pgrStop(){ kill "$pgrPID" 2>/dev/null; } pgrStop(){ kill "$pgrPID" 2>/dev/null; }
+1 -1
View File
@@ -56,7 +56,7 @@ export PGDATA="$tmpdir/db"
export PGHOST="$tmpdir/socket" export PGHOST="$tmpdir/socket"
export PGUSER=postgrest_test_authenticator export PGUSER=postgrest_test_authenticator
export PGDATABASE=postgres export PGDATABASE=postgres
export DB_URI="postgresql://$PGDATABASE?host=$PGHOST&user=$PGUSER" export DB_URI="postgresql:///$PGDATABASE?host=$PGHOST&user=$PGUSER"
export PGRST_DB_URI="$DB_URI" export PGRST_DB_URI="$DB_URI"
export PGRST_DB_SCHEMAS="test" export PGRST_DB_SCHEMAS="test"
export PGRST_DB_ANON_ROLE="postgrest_test_anonymous" export PGRST_DB_ANON_ROLE="postgrest_test_anonymous"