# Workflow.Redeem (answer a callback URL, as nobody)

> Answers a link minted by `<Slot>.CallbackUrl()` the way the third party holding it would — anonymously, with no principal of any kind. It is the only way to test the half of a callback URL that is the point of one: somebody with no account completing a slot. Refusals arrive as ordinary faults, so `Assert.Throws` reads them.

<!-- id: testing-redeem-callback · area: testing · stability: preview · html: https://osysharp.com/reference/testing/redeem-callback/ -->

## Summary        {#summary}
[`<Slot>.CallbackUrl()`](https://osysharp.com/reference/workflow/callback-url/) exists so that a person with **no account** can complete one slot —
a supplier confirming a delivery, an invitee accepting. Every other driver verb in a `.test.osy` acts *as somebody*,
so without this one an app could assert that a link was **minted** and could never assert that answering it works.

`Workflow.Redeem(url)` answers it. It runs **genuinely anonymous** — it does not pass the test's `[runas]` principal,
or any principal — so what a passing test proves is that no principal was involved.

**The argument is the callback URL, or just its last path segment — the token.** Both are accepted, so a page that
mails `App.BaseUrl + "/accept/" + token` and a test that passes the whole `AcceptLink` are answering the same slot
([signup by invitation (invite, accept link, chase, expire)](https://osysharp.com/reference/security/invitation-signup/) passes the token; the examples below pass the link). Nothing else is parsed out of
the string.

## Signature      {#signature}
```osy syntax
Workflow.Redeem(<url-or-token>);        // an event with no parameters — the whole callback URL, or its last segment
Workflow.Redeem(<url-or-token>, <json>); // the event's arguments, as the JSON a third party would POST
```

`<url>` is the string `CallbackUrl()` returned. `<json>` is text, not a typed argument list — deliberately: the wire
contract is *"POST the event's parameters as a JSON object, by name"*, so the test drives the same bytes a stranger's
`curl` would. A typed form would prove something weaker by skipping the bind that the contract is made of.

## Description    {#description}

### Refusals are faults, so `Assert.Throws` reads them   {#refusals}
A refusal is not a return value to inspect — it arrives the way every other refusal in this surface does:

| what happened | the fault |
|---|---|
| the token is unknown, or was already spent | `NotFoundException` |
| the slot has closed — satisfied, cancelled, or its run finished | `ConflictException` |
| the body does not fit the event's signature | `ValidationException` |
| a `Requires` criterion does not hold | `RequirementsNotMet` |
| a gate the callback still meets refused (a `Pending` slot) | `NotAuthorized` |

⚑ **Unknown and SPENT are the same answer on purpose.** Telling an unauthenticated caller which of the two it hit
tells it that a token exists. A CLOSED slot is distinguishable because the holder needs to know their item was
withdrawn rather than that their link was corrupted.

### What it does around the call   {#settling}
It settles staged writes first — the URL almost always came off a row the test just created, and the run has to exist
before a token can address it — and after a successful deposit it drives the background pump and **drops what the
test's context had already loaded**. The deposit happens on the engine's own context, so without that last step the
next line reads pre-deposit values and a callback that really worked reads as one that silently did nothing.

## Examples       {#examples}
A supplier who is not a user of the app, and cannot become one — `Candidates` admits only staff, so the link is the
only way this run can reach `Confirmed`:

```osy title="the model" test app=testing-redeem-callback
enum OrderState { Awaiting, Confirmed, Refused }

[Principal]
entity Person {
  [Required, MaxLength(100)] string Name;
  bool IsStaff;
  security { allow read, create when IsAuthenticated || IsAnonymous; }
}

entity Order {
  [Required, MaxLength(100)] string Reference;
  OrderState State;      // no default: the workflow autostarts, so `Initial = Awaiting` IS this field's value
  [MaxLength(400)] string? ConfirmLink;
  security { allow read, create, update when IsAuthenticated || IsAnonymous; }
}

workflow OrderFlow {
  Tracks = Order.State; Autostart = true; Initial = Awaiting;

  event Confirm(bool ok);

  state Awaiting {
    subscribe Confirm(bool ok) as SupplierOk { Candidates = u => u.IsStaff; }
    enter { this.Item.ConfirmLink = SupplierOk.CallbackUrl(); }
    on SupplierOk(bool ok) {
      when (ok) { goto Confirmed; }
      default   { goto Refused; }
    }
  }

  terminal success Confirmed { }
  terminal cancel  Refused { }
}
```

```osy title="…and the tests only this verb makes possible" run app=testing-redeem-callback
[TestFixture]
void Seed() {
  new Person { Name = "Sam", IsStaff = true };
}

principal Sam => Person.Single(p => p.Name == "Sam");

// THE FEATURE: a caller with no account completes the slot, while the same deposit made as a signed-in
// NON-candidate is refused. Both halves in one test — a gate that admitted everyone would pass either alone.
[Test(Seed)]
[runas(Sam)]
void a_holder_of_the_link_confirms_with_no_account_at_all() {
  var o = new Order { Reference = "PO-1" };
  Workflow.Settle(o);
  Assert.NotNull(o.ConfirmLink);

  Workflow.Redeem(o.ConfirmLink, "{\"ok\": true}");

  Assert.Equal(OrderState.Confirmed, o.State);
}

// SINGLE-USE — a forwarded link cannot be answered by a second party, which is a different problem from a
// double click and the one that actually bites.
[Test(Seed)]
[runas(Sam)]
void a_forwarded_link_is_dead_once_it_has_been_used() {
  var o = new Order { Reference = "PO-2" };
  Workflow.Settle(o);
  var link = o.ConfirmLink;

  Workflow.Redeem(link, "{\"ok\": true}");
  Assert.Equal(OrderState.Confirmed, o.State);

  Assert.Throws<NotFoundException>(() => Workflow.Redeem(link, "{\"ok\": true}"));
}

// THE PAYLOAD IS CHECKED, NOT TRUSTED — it is bound against the event's own signature, so a property the event
// never declared is refused rather than ignored.
[Test(Seed)]
[runas(Sam)]
void a_body_the_event_does_not_declare_is_refused() {
  var o = new Order { Reference = "PO-3" };
  Workflow.Settle(o);

  Assert.Throws<ValidationException>(() => Workflow.Redeem(o.ConfirmLink, "{\"approved\": true}"));
  Assert.Equal(OrderState.Awaiting, o.State);
}
```

⚠ **Note the `[runas(Sam)]` and the fact that it changes nothing about the redemption.** The attribute governs the
rest of the body — creating the order is Sam's act. Answering the link is nobody's.

And the property most worth pinning, because a reader will not guess it — an event's `[Authorize]` is not evaluated
either, since a predicate takes a principal and a callback has none:

```osy title="an Authorize refuses a stranger — the link is still accepted" syntax
// The event declares [Authorize(u => u.Email == this.Item.Email)] — and a signed-in stranger IS refused by it…
runas (Mallory) { Assert.Throws<NotAuthorized>(() => Onboarding.RaiseAccept(inv)); }
// …while the LINK, held by nobody, is accepted.
Workflow.Redeem(inv.AcceptLink);
```

## See also       {#see-also}
- [Callback URLs — letting an outsider complete one slot](https://osysharp.com/reference/workflow/callback-url/) — minting the link, and the five properties that bound it
- [signup by invitation (invite, accept link, chase, expire)](https://osysharp.com/reference/security/invitation-signup/) — the complete flow these examples are drawn from
- [runas](https://osysharp.com/reference/testing/runas/) — acting AS somebody, which this verb is the absence of
