b934b396b1
* feat(api): support creation of oauth based AtpAgents * oauth: misc fixes for confidential clients * fix(xprc): remove ReadableStream.from polyfill * OAuth docs tweaks (#2679) * OAuth: clarification about client_name being shown * OAuth: re-write handle resolution privacy concern * avoid relying on ReadableStream.from in xrpc-server tests * feat(oauth-types): expose "ALLOW_UNSECURE_ORIGINS" constant * feat(handle-resolver): expose "AtprotoIdentityDidMethods" type * fix(oauth-client): ensure that the oauth metadata document contains client_id_metadata_document_supported * fix(oauth-types): prevent unknown query string in loopback client id * fix(identity-resolver): check that handle is in did doc's "alsoKnownAs" * feat(oauth-client:oauth-resolver): allow logging in using either the PDS URL or Entryway URL * fix(oauth-client): return better error in case of invalid "oauth-protected-resource" status code * refactor(did): group atproto specific checks in own * feat(api): relax typing of "appLabelers" and "labelers" AtpClient properties * allow any did as labeller (for tests mainly) * fix(api): allow to override "atproto-proxy" on a per-request basis * remove release candidate versions from changelog * update changeset for api and xrpc packages * Add missing changeset * revert RC versions * Proper wording in OAUTH.md api example * remove "pre" changeset file * xrpc: restore original behavior of setHEader and unsetHeader * docs: add comment for XrpcClient 's constructor arg * feat(api): expose "schemas" publicly * feat(api): allow customizing the whatwg fetch function of the AtpAgent * docs(api): improve migration docs * docs: change reference to BskyAgent to AtpAgent * docs: mention the breaking change regarding setSessionPersistHandler * fix(api): better split AtpClient concerns * fix(xrpc): remove unused import * refactor(api): simplify class hierarchu by removeing AtpClient * fix(api): mock proper method for facets detection * restore ability to restore session asynchronously * feat(api): allow instantiating Agent with same argument as super class * docs(api): properly extend Agent class * style(xrpc): var name * docs(api): remove "async" to header getter --------- Co-authored-by: Devin Ivy <devinivy@gmail.com> Co-authored-by: bnewbold <bnewbold@robocracy.org> Co-authored-by: Hailey <me@haileyok.com>
252 lines
7.9 KiB
TypeScript
252 lines
7.9 KiB
TypeScript
import { TestNetworkNoAppView, SeedClient } from '@atproto/dev-env'
|
|
import { AtpAgent } from '@atproto/api'
|
|
import { randomStr } from '@atproto/crypto'
|
|
|
|
describe('pds admin invite views', () => {
|
|
let network: TestNetworkNoAppView
|
|
let agent: AtpAgent
|
|
let sc: SeedClient
|
|
|
|
beforeAll(async () => {
|
|
network = await TestNetworkNoAppView.create({
|
|
dbPostgresSchema: 'views_admin_invites',
|
|
pds: {
|
|
inviteRequired: true,
|
|
inviteInterval: 1,
|
|
},
|
|
})
|
|
agent = network.pds.getClient()
|
|
sc = network.getSeedClient()
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await network.close()
|
|
})
|
|
|
|
let alice: string
|
|
let bob: string
|
|
let carol: string
|
|
|
|
beforeAll(async () => {
|
|
const adminCode = await agent.api.com.atproto.server.createInviteCode(
|
|
{ useCount: 10 },
|
|
{ encoding: 'application/json', headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
|
|
await sc.createAccount('alice', {
|
|
handle: 'alice.test',
|
|
email: 'alice@test.com',
|
|
password: 'alice',
|
|
inviteCode: adminCode.data.code,
|
|
})
|
|
await sc.createAccount('bob', {
|
|
handle: 'bob.test',
|
|
email: 'bob@test.com',
|
|
password: 'bob',
|
|
inviteCode: adminCode.data.code,
|
|
})
|
|
await sc.createAccount('carol', {
|
|
handle: 'carol.test',
|
|
email: 'carol@test.com',
|
|
password: 'carol',
|
|
inviteCode: adminCode.data.code,
|
|
})
|
|
|
|
alice = sc.dids.alice
|
|
bob = sc.dids.bob
|
|
carol = sc.dids.carol
|
|
|
|
const aliceCodes = await agent.api.com.atproto.server.getAccountInviteCodes(
|
|
{},
|
|
{ headers: sc.getHeaders(alice) },
|
|
)
|
|
await agent.api.com.atproto.server.getAccountInviteCodes(
|
|
{},
|
|
{ headers: sc.getHeaders(bob) },
|
|
)
|
|
await agent.api.com.atproto.server.createInviteCode(
|
|
{ useCount: 5, forAccount: alice },
|
|
{ encoding: 'application/json', headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
await agent.api.com.atproto.admin.disableInviteCodes(
|
|
{ codes: [adminCode.data.code], accounts: [bob] },
|
|
{ encoding: 'application/json', headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
|
|
const useCode = async (code: string) => {
|
|
const name = randomStr(8, 'base32')
|
|
await agent.api.com.atproto.server.createAccount({
|
|
handle: `${name}.test`,
|
|
email: `${name}@test.com`,
|
|
password: name,
|
|
inviteCode: code,
|
|
})
|
|
}
|
|
|
|
await useCode(aliceCodes.data.codes[0].code)
|
|
await useCode(aliceCodes.data.codes[1].code)
|
|
})
|
|
|
|
it('gets a list of invite codes by recency', async () => {
|
|
const result = await agent.api.com.atproto.admin.getInviteCodes(
|
|
{},
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
let lastDate = result.data.codes[0].createdAt
|
|
for (const code of result.data.codes) {
|
|
expect(code.createdAt <= lastDate).toBeTruthy()
|
|
lastDate = code.createdAt
|
|
}
|
|
expect(result.data.codes.length).toBe(12)
|
|
expect(result.data.codes[0]).toMatchObject({
|
|
available: 5,
|
|
disabled: false,
|
|
forAccount: alice,
|
|
createdBy: 'admin',
|
|
})
|
|
expect(result.data.codes[0].uses.length).toBe(0)
|
|
expect(result.data.codes.at(-1)).toMatchObject({
|
|
available: 10,
|
|
disabled: true,
|
|
forAccount: 'admin',
|
|
createdBy: 'admin',
|
|
})
|
|
expect(result.data.codes.at(-1)?.uses.length).toBe(3)
|
|
})
|
|
|
|
it('paginates by recency', async () => {
|
|
const full = await agent.api.com.atproto.admin.getInviteCodes(
|
|
{},
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
const first = await agent.api.com.atproto.admin.getInviteCodes(
|
|
{ limit: 5 },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
const second = await agent.api.com.atproto.admin.getInviteCodes(
|
|
{ cursor: first.data.cursor },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
const combined = [...first.data.codes, ...second.data.codes]
|
|
expect(combined).toEqual(full.data.codes)
|
|
})
|
|
|
|
it('gets a list of invite codes by usage', async () => {
|
|
const result = await agent.api.com.atproto.admin.getInviteCodes(
|
|
{ sort: 'usage' },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
let lastUseCount = result.data.codes[0].uses.length
|
|
for (const code of result.data.codes) {
|
|
expect(code.uses.length).toBeLessThanOrEqual(lastUseCount)
|
|
lastUseCount = code.uses.length
|
|
}
|
|
expect(result.data.codes[0]).toMatchObject({
|
|
available: 10,
|
|
disabled: true,
|
|
forAccount: 'admin',
|
|
createdBy: 'admin',
|
|
})
|
|
expect(result.data.codes[0].uses.length).toBe(3)
|
|
})
|
|
|
|
it('paginates by usage', async () => {
|
|
const full = await agent.api.com.atproto.admin.getInviteCodes(
|
|
{ sort: 'usage' },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
const first = await agent.api.com.atproto.admin.getInviteCodes(
|
|
{ sort: 'usage', limit: 5 },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
const second = await agent.api.com.atproto.admin.getInviteCodes(
|
|
{ sort: 'usage', cursor: first.data.cursor },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
const combined = [...first.data.codes, ...second.data.codes]
|
|
expect(combined).toEqual(full.data.codes)
|
|
})
|
|
|
|
it('hydrates invites into admin.getAccountInfo', async () => {
|
|
const aliceView = await agent.api.com.atproto.admin.getAccountInfo(
|
|
{ did: alice },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
expect(aliceView.data.invitedBy?.available).toBe(10)
|
|
expect(aliceView.data.invitedBy?.uses.length).toBe(3)
|
|
expect(aliceView.data.invites?.length).toBe(6)
|
|
})
|
|
|
|
it('disables an account from getting additional invite codes', async () => {
|
|
await agent.api.com.atproto.admin.disableAccountInvites(
|
|
{ account: carol },
|
|
{ encoding: 'application/json', headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
|
|
const repoRes = await agent.api.com.atproto.admin.getAccountInfo(
|
|
{ did: carol },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
expect(repoRes.data.invitesDisabled).toBe(true)
|
|
|
|
const invRes = await agent.api.com.atproto.server.getAccountInviteCodes(
|
|
{},
|
|
{ headers: sc.getHeaders(carol) },
|
|
)
|
|
expect(invRes.data.codes.length).toBe(0)
|
|
})
|
|
|
|
it('allows setting reason when enabling and disabling invite codes', async () => {
|
|
await agent.api.com.atproto.admin.enableAccountInvites(
|
|
{ account: carol },
|
|
{ encoding: 'application/json', headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
|
|
const afterEnable = await agent.api.com.atproto.admin.getAccountInfo(
|
|
{ did: carol },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
expect(afterEnable.data.invitesDisabled).toBe(false)
|
|
|
|
await agent.api.com.atproto.admin.disableAccountInvites(
|
|
{ account: carol },
|
|
{ encoding: 'application/json', headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
|
|
const afterDisable = await agent.api.com.atproto.admin.getAccountInfo(
|
|
{ did: carol },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
expect(afterDisable.data.invitesDisabled).toBe(true)
|
|
})
|
|
|
|
it('creates codes in the background but disables them', async () => {
|
|
const res = await network.pds.ctx.accountManager.db.db
|
|
.selectFrom('invite_code')
|
|
.where('forAccount', '=', carol)
|
|
.selectAll()
|
|
.execute()
|
|
expect(res.length).toBe(5)
|
|
expect(res.every((row) => row.disabled === 1))
|
|
})
|
|
|
|
it('re-enables an accounts invites', async () => {
|
|
await agent.api.com.atproto.admin.enableAccountInvites(
|
|
{ account: carol },
|
|
{ encoding: 'application/json', headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
|
|
const repoRes = await agent.api.com.atproto.admin.getAccountInfo(
|
|
{ did: carol },
|
|
{ headers: network.pds.adminAuthHeaders() },
|
|
)
|
|
expect(repoRes.data.invitesDisabled).toBe(false)
|
|
|
|
const invRes = await agent.api.com.atproto.server.getAccountInviteCodes(
|
|
{},
|
|
{ headers: sc.getHeaders(carol) },
|
|
)
|
|
expect(invRes.data.codes.length).toBeGreaterThan(0)
|
|
})
|
|
})
|