| 118 | // The mint normalizes the name; the free-name guard must compare against |
| 119 | // that same normalized form, so a repeat call resolves to a suffix. |
| 120 | const connect = (requestedName: string) => |
| 121 | Effect.gen(function* () { |
| 122 | const started = yield* client.oauth.start({ |
| 123 | payload: { |
| 124 | client: clientSlug, |
| 125 | clientOwner: "org", |
| 126 | owner: "org", |
| 127 | name: ConnectionName.make(requestedName), |
| 128 | integration: slug, |
| 129 | template: AuthTemplateSlug.make("oauth"), |
| 130 | newConnection: true, |
| 131 | }, |
| 132 | }); |
| 133 | expect(started.status, "oauth.start redirects to the authorization server").toBe( |
| 134 | "redirect", |
| 135 | ); |
| 136 | if (started.status !== "redirect") return yield* Effect.die("no redirect"); |
| 137 | |
| 138 | // Drive the test IdP's consent by hand (authorize -> login -> code). |
| 139 | // Plain fetch with manual redirects is the e2e-proven path: the |
| 140 | // Effect HttpClient's manual-redirect layer is overridden by the |
| 141 | // scenario runtime and silently follows the hop to the login page. |
| 142 | const code = yield* Effect.promise(async () => { |
| 143 | const authorize = await fetch(started.authorizationUrl, { redirect: "manual" }); |
| 144 | const loginUrl = authorize.headers.get("location"); |
| 145 | if (!loginUrl) throw new Error(`authorize did not redirect: ${authorize.status}`); |
| 146 | const login = await fetch(loginUrl, { |
| 147 | method: "POST", |
| 148 | headers: { |
| 149 | authorization: `Basic ${Buffer.from("alice:password").toString("base64")}`, |
| 150 | }, |
| 151 | redirect: "manual", |
| 152 | }); |
| 153 | const callbackUrl = login.headers.get("location"); |
| 154 | if (!callbackUrl) throw new Error(`login did not redirect: ${login.status}`); |
| 155 | const minted = new URL(callbackUrl).searchParams.get("code"); |
| 156 | if (!minted) throw new Error("callback carried no authorization code"); |
| 157 | return minted; |
| 158 | }); |
| 159 | // `complete` returns the minted connection projection directly. |
| 160 | const connection = yield* client.oauth.complete({ |
| 161 | payload: { state: started.state, code }, |
| 162 | }); |
| 163 | return String(connection.name); |
| 164 | }); |
| 165 | |
| 166 | yield* Effect.ensuring( |
| 167 | Effect.gen(function* () { |