---
kind: guide
zz: "0.8.0"
updated: 2026-10-10
state: live
rules: https://zriz.io/llms.txt
---
# Test that a password is stored as a hash

Sign up in a zriz pipeline, then read the user row. The test fails if a result holds the password or the database holds it as plain text.

## The problem {#problem}

A password in plain text is the worst data to lose. The fault hides in two places: a result that returns the user record, and the column in the database. No user sees it until the data leaks.

## The test {#test}

The example is a shop API with a MySQL database. The column `password_hash` must hold a bcrypt hash.

Start from a project that `zz init` made: the [quick start](https://zriz.io/docs.md) gives the commands. Add the action `register`, the database value, and the database resource.

`.zriz/resources/target.json`:

```json
{
  "type": "http",
  "description": "Your app under test",
  "base-url": "${env.TARGET_URL}",
  "headers": { "Content-Type": "application/json" },
  "actions": {
    "check": { "method": "GET", "path": "/api/health" },
    "register": { "method": "POST", "path": "/api/auth/register" }
  }
}
```

`.zriz/environments/local.json`:

```json
{
  "values": {
    "TARGET_URL": "http://localhost:9080",
    "SHOP_DB": "user:pass@tcp(localhost:3306)/shop"
  },
  "sensitive": ["SHOP_DB"]
}
```

`.zriz/resources/shopdb.json`:

```json
{
  "type": "sql",
  "connection": "${env.SHOP_DB}",
  "read-only": true,
  "actions": {
    "password-by-email": { "query": "SELECT password_hash FROM users WHERE email = ${ctx.email}" }
  }
}
```

`.zriz/pipelines/password-is-hashed.json`:

```json
{
  "description": "The password is in no reply and is stored as a hash",
  "steps": [
    { "set": { "email": "u-${gen.uuid}@test.com", "password": "secret-${gen.uuid}" } },
    {
      "call": "target/register",
      "body": { "email": "${ctx.email}", "password": "${ctx.password}", "name": "Ann" },
      "expect": [
        ["status", "==", 201],
        ["body.password", "not-exists"],
        ["body.password_hash", "not-exists"]
      ]
    },
    {
      "call": "shopdb/password-by-email",
      "expect": [
        ["row-count", "==", 1],
        ["rows[0].password_hash", "!=", "${ctx.password}"],
        ["rows[0].password_hash", "not-contains", "${ctx.password}"],
        ["rows[0].password_hash", "starts-with", "$2"]
      ]
    }
  ]
}
```

## Run it {#run}

```sh
zz run password-is-hashed
```

```text
"status":"pass"
```

## What it proves {#proves}

- `not-exists` on `body.password` and `body.password_hash`: the result holds no password and no hash.
- `!=` and `not-contains`: the column does not hold the text that the test sent.
- `starts-with $2`: the value is a bcrypt hash. Change the text if your API uses a different hash.

> Note: The test password is not a real secret. A real secret goes in the env file with its name in `sensitive`.

## Next {#next}

- [Test an async API with polling, not sleep](https://zriz.io/learn/test-async-api-with-polling.md)
- [Does the cloud see my database password?](https://zriz.io/docs/security.md#database-password)
