refactor(account): tighten effect-based account flows (#17072)

This commit is contained in:
Kit Langton
2026-03-11 14:18:58 -04:00
committed by GitHub
parent 2aae0d3493
commit 981c7b9e37
9 changed files with 390 additions and 354 deletions

View File

@@ -2,7 +2,7 @@ import { expect } from "bun:test"
import { Effect, Layer, Option } from "effect"
import { AccountRepo } from "../../src/account/repo"
import { AccountID, OrgID } from "../../src/account/schema"
import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema"
import { Database } from "../../src/storage/db"
import { testEffect } from "../fixture/effect"
@@ -41,8 +41,8 @@ it.effect(
id,
email: "test@example.com",
url: "https://control.example.com",
accessToken: "at_123",
refreshToken: "rt_456",
accessToken: AccessToken.make("at_123"),
refreshToken: RefreshToken.make("rt_456"),
expiry: Date.now() + 3600_000,
orgID: Option.some(OrgID.make("org-1")),
}),
@@ -51,7 +51,7 @@ it.effect(
const row = yield* AccountRepo.use((r) => r.getRow(id))
expect(Option.isSome(row)).toBe(true)
const value = Option.getOrThrow(row)
expect(value.id).toBe("user-1")
expect(value.id).toBe(AccountID.make("user-1"))
expect(value.email).toBe("test@example.com")
const active = yield* AccountRepo.use((r) => r.active())
@@ -70,8 +70,8 @@ it.effect(
id: id1,
email: "first@example.com",
url: "https://control.example.com",
accessToken: "at_1",
refreshToken: "rt_1",
accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"),
expiry: Date.now() + 3600_000,
orgID: Option.some(OrgID.make("org-1")),
}),
@@ -82,8 +82,8 @@ it.effect(
id: id2,
email: "second@example.com",
url: "https://control.example.com",
accessToken: "at_2",
refreshToken: "rt_2",
accessToken: AccessToken.make("at_2"),
refreshToken: RefreshToken.make("rt_2"),
expiry: Date.now() + 3600_000,
orgID: Option.some(OrgID.make("org-2")),
}),
@@ -108,8 +108,8 @@ it.effect(
id: id1,
email: "a@example.com",
url: "https://control.example.com",
accessToken: "at_1",
refreshToken: "rt_1",
accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"),
expiry: Date.now() + 3600_000,
orgID: Option.none(),
}),
@@ -120,8 +120,8 @@ it.effect(
id: id2,
email: "b@example.com",
url: "https://control.example.com",
accessToken: "at_2",
refreshToken: "rt_2",
accessToken: AccessToken.make("at_2"),
refreshToken: RefreshToken.make("rt_2"),
expiry: Date.now() + 3600_000,
orgID: Option.some(OrgID.make("org-1")),
}),
@@ -143,8 +143,8 @@ it.effect(
id,
email: "test@example.com",
url: "https://control.example.com",
accessToken: "at_1",
refreshToken: "rt_1",
accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"),
expiry: Date.now() + 3600_000,
orgID: Option.none(),
}),
@@ -168,8 +168,8 @@ it.effect(
id: id1,
email: "first@example.com",
url: "https://control.example.com",
accessToken: "at_1",
refreshToken: "rt_1",
accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"),
expiry: Date.now() + 3600_000,
orgID: Option.none(),
}),
@@ -180,8 +180,8 @@ it.effect(
id: id2,
email: "second@example.com",
url: "https://control.example.com",
accessToken: "at_2",
refreshToken: "rt_2",
accessToken: AccessToken.make("at_2"),
refreshToken: RefreshToken.make("rt_2"),
expiry: Date.now() + 3600_000,
orgID: Option.none(),
}),
@@ -208,8 +208,8 @@ it.effect(
id,
email: "test@example.com",
url: "https://control.example.com",
accessToken: "old_token",
refreshToken: "old_refresh",
accessToken: AccessToken.make("old_token"),
refreshToken: RefreshToken.make("old_refresh"),
expiry: 1000,
orgID: Option.none(),
}),
@@ -219,16 +219,16 @@ it.effect(
yield* AccountRepo.use((r) =>
r.persistToken({
accountID: id,
accessToken: "new_token",
refreshToken: "new_refresh",
accessToken: AccessToken.make("new_token"),
refreshToken: RefreshToken.make("new_refresh"),
expiry: Option.some(expiry),
}),
)
const row = yield* AccountRepo.use((r) => r.getRow(id))
const value = Option.getOrThrow(row)
expect(value.access_token).toBe("new_token")
expect(value.refresh_token).toBe("new_refresh")
expect(value.access_token).toBe(AccessToken.make("new_token"))
expect(value.refresh_token).toBe(RefreshToken.make("new_refresh"))
expect(value.token_expiry).toBe(expiry)
}),
)
@@ -243,8 +243,8 @@ it.effect(
id,
email: "test@example.com",
url: "https://control.example.com",
accessToken: "old_token",
refreshToken: "old_refresh",
accessToken: AccessToken.make("old_token"),
refreshToken: RefreshToken.make("old_refresh"),
expiry: 1000,
orgID: Option.none(),
}),
@@ -253,8 +253,8 @@ it.effect(
yield* AccountRepo.use((r) =>
r.persistToken({
accountID: id,
accessToken: "new_token",
refreshToken: "new_refresh",
accessToken: AccessToken.make("new_token"),
refreshToken: RefreshToken.make("new_refresh"),
expiry: Option.none(),
}),
)
@@ -274,8 +274,8 @@ it.effect(
id,
email: "test@example.com",
url: "https://control.example.com",
accessToken: "at_v1",
refreshToken: "rt_v1",
accessToken: AccessToken.make("at_v1"),
refreshToken: RefreshToken.make("rt_v1"),
expiry: 1000,
orgID: Option.some(OrgID.make("org-1")),
}),
@@ -286,8 +286,8 @@ it.effect(
id,
email: "test@example.com",
url: "https://control.example.com",
accessToken: "at_v2",
refreshToken: "rt_v2",
accessToken: AccessToken.make("at_v2"),
refreshToken: RefreshToken.make("rt_v2"),
expiry: 2000,
orgID: Option.some(OrgID.make("org-2")),
}),
@@ -298,7 +298,7 @@ it.effect(
const row = yield* AccountRepo.use((r) => r.getRow(id))
const value = Option.getOrThrow(row)
expect(value.access_token).toBe("at_v2")
expect(value.access_token).toBe(AccessToken.make("at_v2"))
const active = yield* AccountRepo.use((r) => r.active())
expect(Option.getOrThrow(active).active_org_id).toBe(OrgID.make("org-2"))
@@ -315,8 +315,8 @@ it.effect(
id,
email: "test@example.com",
url: "https://control.example.com",
accessToken: "at_1",
refreshToken: "rt_1",
accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"),
expiry: Date.now() + 3600_000,
orgID: Option.some(OrgID.make("org-1")),
}),

View File

@@ -1,10 +1,10 @@
import { expect } from "bun:test"
import { Effect, Layer, Option, Ref, Schema } from "effect"
import { Duration, Effect, Layer, Option, Ref, Schema } from "effect"
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
import { AccountRepo } from "../../src/account/repo"
import { AccountService } from "../../src/account/service"
import { AccountID, Login, Org, OrgID } from "../../src/account/schema"
import { AccessToken, AccountID, DeviceCode, Login, Org, OrgID, RefreshToken, UserCode } from "../../src/account/schema"
import { Database } from "../../src/storage/db"
import { testEffect } from "../fixture/effect"
@@ -42,8 +42,8 @@ it.effect(
id: AccountID.make("user-1"),
email: "one@example.com",
url: "https://one.example.com",
accessToken: "at_1",
refreshToken: "rt_1",
accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"),
expiry: Date.now() + 60_000,
orgID: Option.none(),
}),
@@ -54,8 +54,8 @@ it.effect(
id: AccountID.make("user-2"),
email: "two@example.com",
url: "https://two.example.com",
accessToken: "at_2",
refreshToken: "rt_2",
accessToken: AccessToken.make("at_2"),
refreshToken: RefreshToken.make("rt_2"),
expiry: Date.now() + 60_000,
orgID: Option.none(),
}),
@@ -101,8 +101,8 @@ it.effect(
id,
email: "user@example.com",
url: "https://one.example.com",
accessToken: "at_old",
refreshToken: "rt_old",
accessToken: AccessToken.make("at_old"),
refreshToken: RefreshToken.make("rt_old"),
expiry: Date.now() - 1_000,
orgID: Option.none(),
}),
@@ -110,7 +110,7 @@ it.effect(
const client = HttpClient.make((req) =>
Effect.succeed(
req.url === "https://one.example.com/oauth/token"
req.url === "https://one.example.com/auth/device/token"
? json(req, {
access_token: "at_new",
refresh_token: "rt_new",
@@ -127,8 +127,8 @@ it.effect(
const row = yield* AccountRepo.use((r) => r.getRow(id))
const value = Option.getOrThrow(row)
expect(value.access_token).toBe("at_new")
expect(value.refresh_token).toBe("rt_new")
expect(value.access_token).toBe(AccessToken.make("at_new"))
expect(value.refresh_token).toBe(RefreshToken.make("rt_new"))
expect(value.token_expiry).toBeGreaterThan(Date.now())
}),
)
@@ -143,8 +143,8 @@ it.effect(
id,
email: "user@example.com",
url: "https://one.example.com",
accessToken: "at_1",
refreshToken: "rt_1",
accessToken: AccessToken.make("at_1"),
refreshToken: RefreshToken.make("rt_1"),
expiry: Date.now() + 60_000,
orgID: Option.none(),
}),
@@ -180,12 +180,12 @@ it.effect(
"poll stores the account and first org on success",
Effect.gen(function* () {
const login = new Login({
code: "device-code",
user: "user-code",
code: DeviceCode.make("device-code"),
user: UserCode.make("user-code"),
url: "https://one.example.com/verify",
server: "https://one.example.com",
expiry: 600,
interval: 5,
expiry: Duration.seconds(600),
interval: Duration.seconds(5),
})
const client = HttpClient.make((req) =>
@@ -194,6 +194,7 @@ it.effect(
? json(req, {
access_token: "at_1",
refresh_token: "rt_1",
token_type: "Bearer",
expires_in: 60,
})
: req.url === "https://one.example.com/api/user"