> ## Documentation Index
> Fetch the complete documentation index at: https://e2e.tester.army/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Signing in

> Sign in once, reuse the session across tests, and pass secrets without exposing them to the model.

A setup test signs in one time and saves a named session. Other tests
restore that session and start signed in. Passwords and other secrets are
handles: the runner types the value and shows `<secret:name>` in its place
in model input and reports, with
[some limits](https://e2e.tester.army/docs/reference/security#where-redaction-stops).

## Sign in once in a setup test

Save the session in a `test.setup` body. Restore it with the `session`
option. The runner runs the setup a test needs, even when you select only
the test:

<Tabs>
  <Tab title="tests/auth.setup.e2e.ts">

```ts

test.setup('authenticate as admin', { sessions: ['admin'] }, async ({ app, screen, session, browser }) => {
  const admin = credentials.user('admin');

  await app.open('/login');
  await screen.getByLabel('Username').fill(admin.username);
  await screen.getByLabel('Password').fill(admin.password);
  await screen.getByRole('button', 'Sign in').tap();

  await expect(browser).toHaveURL('/dashboard');
  await expect(screen.getByRole('status', 'Greeting')).toContainText('admin');

  await session.save('admin');
});
```

  </Tab>
  <Tab title="tests/dashboard.e2e.ts">

```ts

test('the dashboard opens directly', { session: 'admin' }, async ({ app, screen, browser }) => {
  await app.open('/dashboard');

  await expect(browser).toHaveURL('/dashboard');
  await expect(screen.getByRole('status', 'Greeting')).toHaveText(
    'Welcome back, admin!',
  );
});
```

  </Tab>
</Tabs>

<Note>
`session.save()` doesn't check that sign-in succeeded. Assert the URL or a
signed-in element before you save.
</Note>

## Session rules

| Rule | If you break it |
| --- | --- |
| Setup tests are top-level, never inside `describe` | collection error |
| Exactly one setup declares a given session name | collection error |
| Every name in `sessions` is saved by the body | `SESSION_CONTRACT` failure |
| Only declared names are saved | `SESSION_CONTRACT` failure |
| The target's engine saves and restores state | collection error, before any test runs; a hosted browser leased per attempt has none |

When a setup fails, the runner skips its dependents with cause
`setup-failed`. Other tests keep running.

On a browser target, a session holds cookies, local storage, and IndexedDB,
not server or database state. A restore leaves no page open, so each test
calls `app.open()` first.

A session lasts one run. The runner encrypts it with a key held only in
memory and deletes it at cleanup. See [Sessions](https://e2e.tester.army/docs/reference/security#sessions).

## Credentials

Declare each account in config. Read the password from the environment:

```ts title="e2e.config.ts"

export default {
  targets: [{ engine: web(), app: { url: 'http://127.0.0.1:3000' } }],
  credentials: {
    admin: {
      username: 'admin@example.test',
      password: process.env.ADMIN_PASSWORD ?? '',
    },
  },
} satisfies E2EConfig;
```

The config load checks each password. While `ADMIN_PASSWORD` is unset or
shorter than 6 characters, every command fails with `INVALID_CONFIG`,
`e2e list` included. A valid [override](#override-a-value-per-run) replaces
it.

<Accordion title="Keep the value out of the environment">

Any process the runner starts can read a string from `process.env`.
`password` also accepts a function, `username` doesn't. The runner calls the
function only for an authorized fill, in the runner process, so the value
can come from a vault or be a fresh one-time code:

```ts title="e2e.config.ts"

export default {
  targets: [{ engine: web(), app: { url: 'http://127.0.0.1:3000' } }],
  credentials: {
    admin: {
      username: 'admin@example.test',
      password: () => vault.read('e2e/admin-password'),
    },
  },
} satisfies E2EConfig;
```

The value is redacted only from its first fill on. If the app can show the
value before a test types it, use a string. The same applies to a `secrets`
entry.

</Accordion>

## The password is a handle

`credentials.user(name)` returns the username as a plain string and the
password as a `Secret` named `<name>.password`. A `Secret` has no `.value`.
Pass it to a `fill` or to the `params` of an agent step:

```ts
await agent.act('Sign in', {
  params: { username: admin.username, password: admin.password },
});
```

The model sees only the secret's name and purpose. When the model fills a
password, the runner requires a password field and types the value. See the
[secret fill rules](https://e2e.tester.army/docs/reference/security#secret-fills). The agent can't read
a secure field, so assert that sign-in succeeded instead.

Screenshots mask the password field. A value the app shows anywhere else
reaches the model. See
[screenshots after a secret fill](https://e2e.tester.army/docs/reference/security#screenshots-after-a-secret-fill).

<Warning>
Redaction matches strings. A transformed value, such as the last four
characters of a key, can still appear. Keep secret entry in setup tests, and
assert the result of the submit. See
[where redaction stops](https://e2e.tester.army/docs/reference/security#where-redaction-stops).
</Warning>

## Secrets that are not passwords

Use `secrets` for API keys, webhook tokens, and other values a test types:

```ts title="e2e.config.ts"

export default {
  targets: [{ engine: web(), app: { url: 'http://127.0.0.1:3000' } }],
  secrets: {
    'stripe-key': process.env.STRIPE_TEST_KEY ?? '',
    'webhook-token': process.env.WEBHOOK_TOKEN ?? '',
  },
} satisfies E2EConfig;
```

`secrets.get()` returns a handle. Use it like a password:

```ts

await screen.getByLabel('Secret key').fill(secrets.get('stripe-key'));
await agent.act('Connect Stripe with the given key', {
  params: { apiKey: secrets.get('stripe-key') },
});
```

A `secrets` entry can fill any editable input. An unconfigured name throws
`SECRET_UNAVAILABLE`. A plain input isn't masked, so screenshots show the
value while the input holds it, the model's included. `secrets.get()`
returns `secrets` entries only, so
`credentials.admin` and `secrets.admin` can both exist.

In `e2e.config.ts`, `secrets.get()` can pass a secret to an engine option
that declares it, such as [`basicAuth`](https://e2e.tester.army/docs/web#protected-previews). The config
load checks the name. Screenshots stay on, so the app's pixels aren't masked.
See [Secrets in engine options](https://e2e.tester.army/docs/reference/security#secrets-in-engine-options).

## Override a value per run

An environment variable replaces a config value for one run:

| Entry | Variable |
| --- | --- |
| `credentials.admin.username` | `E2E_USER_ADMIN_USERNAME` |
| `credentials.admin.password` | `E2E_USER_ADMIN_PASSWORD` |
| `secrets['stripe-key']` | `E2E_SECRET_STRIPE_KEY` |

The variable holds the entry name in upper case, with each character
outside `A-Z` and `0-9` as `_`. When two entries map to one variable, such
as `stripe-key` and `stripe_key`, the config load fails with
`INVALID_CONFIG`.

<CodeGroup>
```bash npm
E2E_USER_ADMIN_PASSWORD=... E2E_SECRET_STRIPE_KEY=sk_test_... npx e2e run
```

```bash pnpm
E2E_USER_ADMIN_PASSWORD=... E2E_SECRET_STRIPE_KEY=sk_test_... pnpm exec e2e run
```

```bash bun
E2E_USER_ADMIN_PASSWORD=... E2E_SECRET_STRIPE_KEY=sk_test_... bunx e2e run
```
</CodeGroup>

## Several signed-in users

Save one session per user. Then run the same tests with each session and
[agent](https://e2e.tester.army/docs/agents):

<Tabs>
  <Tab title="tests/personas.setup.e2e.ts">

```ts

const PERSONAS = ['buyer', 'admin'] as const;

test.setup('sign in each persona', { sessions: [...PERSONAS] }, async ({ app, screen, session, browser }) => {
  for (const name of PERSONAS) {
    const user = credentials.user(name);
    await app.open('/login');
    await screen.getByLabel('Username').fill(user.username);
    await screen.getByLabel('Password').fill(user.password);
    await screen.getByRole('button', 'Sign in').tap();
    await expect(browser).toHaveURL('/dashboard');
    await session.save(name);
    await app.open('/logout');
  }
});
```

  </Tab>
  <Tab title="tests/checkout.e2e.ts">

```ts

for (const persona of ['buyer', 'admin'] as const) {
  describe(`checkout as ${persona}`, { agent: persona, session: persona, tags: [persona] }, () => {
    test('pays for one item', async ({ app, agent }) => {
      await app.open('/cart');
      await agent.act('pay for the item in the cart');
      await agent.assert('the order confirmation is shown');
    });
  });
}
```

  </Tab>
</Tabs>

Each name refers to an `agents` entry, a `credentials` entry, and a saved
session. When the personas don't need separate sessions,
[run one flow as several agents](https://e2e.tester.army/docs/agents#one-flow-several-personas) instead.

## Next

<CardGroup cols={2}>
  <Card title="test reference" icon="flask" href="/reference/test">
    `test.setup`, `sessions`, and the `session` option.
  </Card>
  <Card title="Config reference" icon="sliders" href="/reference/config#credentials">
    The `credentials` and `secrets` blocks.
  </Card>
</CardGroup>
