mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 05:49:50 +00:00
test: drive platform administration with the admin's session, not its API token (BUG-3361)
CI on the first push: 70 E2E specs and TestMCPCapability_UpgradeChangesNothingWhileOff called /api/v1/admin/* or admin account creation with an admin PAT, exactly the use this change refuses. Our own harness was the first operator script the breaking change broke. - E2E: global setup keeps the session token login already returns as fixture.adminSessionToken (padsess_ is not User-Agent bound when sent as a Bearer). The seven admin call sites use it: account creation in permission-walk, workspace-tabs, workspace-access-live, no-commerce and account-delete; the admin user search; and PUT /admin/mcp. Every other call keeps the PAT, including the MCP client config in plan-2310. - mcp_capability_test: the admin drives /admin with a CLI session and keeps its PAT for /mcp, which takes one. Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
This commit is contained in:
@@ -72,14 +72,15 @@ func capRoutes() []capRoute {
|
||||
}
|
||||
|
||||
type capFixture struct {
|
||||
store *store.Store
|
||||
srv *server.Server
|
||||
keyBytes []byte
|
||||
admin *models.User
|
||||
member *models.User
|
||||
ws *models.Workspace
|
||||
adminPAT string
|
||||
memberPAT string
|
||||
store *store.Store
|
||||
srv *server.Server
|
||||
keyBytes []byte
|
||||
admin *models.User
|
||||
member *models.User
|
||||
ws *models.Workspace
|
||||
adminPAT string // for /mcp, which takes a PAT
|
||||
adminSession string // for /api/v1/admin, which refuses one (BUG-3361)
|
||||
memberPAT string
|
||||
// host is the Host header requests carry: the configured origin's,
|
||||
// as a client using the configured URL (or a proxy forwarding it)
|
||||
// sends, so the DR-6 allowlist admits them. "example.com" when no
|
||||
@@ -126,6 +127,11 @@ func newCapFixture(t *testing.T, cfg config.Config, cloud bool, env *bool, setti
|
||||
}
|
||||
*u.dst = tok.Token
|
||||
}
|
||||
// The admin drives /api/v1/admin, which refuses an API token (BUG-3361):
|
||||
// a CLI session, as `pad auth login` gives an operator.
|
||||
if f.adminSession, err = s.CreateSession(f.admin.ID, "cli-browser-auth", "192.0.2.10", "", time.Hour); err != nil {
|
||||
t.Fatalf("CreateSession admin: %v", err)
|
||||
}
|
||||
if setting != "" {
|
||||
if err := s.SetPlatformSetting("mcp_enabled", setting); err != nil {
|
||||
t.Fatalf("SetPlatformSetting: %v", err)
|
||||
@@ -163,7 +169,7 @@ func (f *capFixture) do(t *testing.T, rt capRoute) *httptest.ResponseRecorder {
|
||||
case "member":
|
||||
req.Header.Set("Authorization", "Bearer "+f.memberPAT)
|
||||
case "admin":
|
||||
req.Header.Set("Authorization", "Bearer "+f.adminPAT)
|
||||
req.Header.Set("Authorization", "Bearer "+f.adminSession)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
f.srv.ServeHTTP(rr, req)
|
||||
|
||||
@@ -97,9 +97,9 @@ test.describe('Danger Zone: delete my account', () => {
|
||||
});
|
||||
|
||||
test('password branch removes the account and redirects to /login', async ({ browser }) => {
|
||||
const { baseURL, apiToken } = suiteFixture();
|
||||
const { baseURL, adminSessionToken } = suiteFixture();
|
||||
const identity = throwawayIdentity('pw');
|
||||
await createThrowawayUser(baseURL, apiToken, identity, 'Delete PW');
|
||||
await createThrowawayUser(baseURL, adminSessionToken, identity, 'Delete PW');
|
||||
|
||||
const context = await browser.newContext({ baseURL });
|
||||
try {
|
||||
@@ -129,7 +129,7 @@ test.describe('Danger Zone: delete my account', () => {
|
||||
// (A GET on the generous API limiter, so it adds no /auth rate pressure.)
|
||||
const admin = await playwrightRequest.newContext({
|
||||
baseURL,
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${apiToken}` }
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${adminSessionToken}` }
|
||||
});
|
||||
try {
|
||||
const resp = await admin.get(
|
||||
@@ -145,9 +145,9 @@ test.describe('Danger Zone: delete my account', () => {
|
||||
});
|
||||
|
||||
test('cloud OAuth-only typed-confirm branch redirects to /login', async ({ browser }) => {
|
||||
const { baseURL, apiToken } = suiteFixture();
|
||||
const { baseURL, adminSessionToken } = suiteFixture();
|
||||
const identity = throwawayIdentity('cloud');
|
||||
await createThrowawayUser(baseURL, apiToken, identity, 'Delete Cloud');
|
||||
await createThrowawayUser(baseURL, adminSessionToken, identity, 'Delete Cloud');
|
||||
|
||||
const context = await browser.newContext({ baseURL });
|
||||
try {
|
||||
|
||||
@@ -26,6 +26,10 @@ export interface SuiteFixture {
|
||||
adminEmail: string;
|
||||
adminUsername: string;
|
||||
apiToken: string;
|
||||
// The admin's own session token (padsess_), for platform-administration
|
||||
// calls: /api/v1/admin/* and account creation refuse an API token
|
||||
// (BUG-3361). Sent as a Bearer, it is not bound to a User-Agent.
|
||||
adminSessionToken: string;
|
||||
}
|
||||
|
||||
function loadSuiteFixture(): SuiteFixture {
|
||||
|
||||
@@ -60,6 +60,10 @@ export default async function globalSetup(config: FullConfig) {
|
||||
if (!login.ok()) {
|
||||
throw new Error(`login failed (${login.status()}): ${await login.text()}`);
|
||||
}
|
||||
const loginPayload = (await login.json()) as { token?: string };
|
||||
if (!loginPayload.token) {
|
||||
throw new Error('login succeeded but response missing the session token');
|
||||
}
|
||||
|
||||
// State-changing endpoints require the CSRF double-submit header
|
||||
// matching the pad_csrf cookie issued at login.
|
||||
@@ -128,7 +132,8 @@ export default async function globalSetup(config: FullConfig) {
|
||||
workspaceSlug: resolvedSlug,
|
||||
adminEmail: ADMIN_EMAIL,
|
||||
adminUsername: me.username,
|
||||
apiToken: tokenPayload.token
|
||||
apiToken: tokenPayload.token,
|
||||
adminSessionToken: loginPayload.token
|
||||
},
|
||||
null,
|
||||
2
|
||||
|
||||
@@ -153,10 +153,12 @@ async function createItems(
|
||||
|
||||
/** Seed one walk world. Each call builds a fresh, independent one. */
|
||||
export async function seedPermissionWalk(): Promise<PermissionWalk> {
|
||||
const { baseURL, apiToken } = suiteFixture();
|
||||
const { baseURL, adminSessionToken } = suiteFixture();
|
||||
const tag = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
|
||||
const admin = await bearer(baseURL, apiToken);
|
||||
// Account creation is platform administration: the admin's session, not
|
||||
// its API token (BUG-3361).
|
||||
const admin = await bearer(baseURL, adminSessionToken);
|
||||
const accounts = {} as Record<AccountKey, WalkAccount>;
|
||||
try {
|
||||
for (const key of ACCOUNT_KEYS) {
|
||||
|
||||
@@ -17,11 +17,11 @@ export const NAMES = [
|
||||
export type Box = { x: number; y: number; width: number; height: number };
|
||||
|
||||
export async function asNewUser(page: Page): Promise<{ username: string; slugs: string[] }> {
|
||||
const { baseURL, apiToken } = suiteFixture();
|
||||
const { baseURL, adminSessionToken } = suiteFixture();
|
||||
const suffix = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
||||
const email = `e2e-tabs-${suffix}@example.com`;
|
||||
const username = `e2etabs${suffix}`;
|
||||
const api = await playwrightRequest.newContext({ baseURL, extraHTTPHeaders: { Authorization: `Bearer ${apiToken}` } });
|
||||
const api = await playwrightRequest.newContext({ baseURL, extraHTTPHeaders: { Authorization: `Bearer ${adminSessionToken}` } });
|
||||
try {
|
||||
const r = await api.post('/api/v1/auth/register', { data: { email, username, name: 'Tabs', password: PASSWORD } });
|
||||
if (!r.ok()) throw new Error(`register failed (${r.status()}): ${await r.text()}`);
|
||||
|
||||
@@ -50,7 +50,7 @@ test('http self-host: the modal token config reaches /mcp and lists tools', asyn
|
||||
|
||||
const admin = await playwrightRequest.newContext({
|
||||
baseURL: fixture.baseURL,
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${fixture.apiToken}` }
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${fixture.adminSessionToken}` }
|
||||
});
|
||||
try {
|
||||
// Inside the try: a failed readiness assertion after the PUT
|
||||
|
||||
@@ -92,8 +92,8 @@ for (const leg of [
|
||||
]) {
|
||||
test(`TASK-3293: ${leg.name}`, async ({ browser }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== 'desktop-chromium', 'UA is the variable, not viewport; one project keeps auth rate-limit pressure down.');
|
||||
const { baseURL, apiToken } = suiteFixture();
|
||||
const email = await register(baseURL, apiToken, leg.shell ? 's' : 'b');
|
||||
const { baseURL, adminSessionToken } = suiteFixture();
|
||||
const email = await register(baseURL, adminSessionToken, leg.shell ? 's' : 'b');
|
||||
const context = await browser.newContext({ baseURL, userAgent: leg.ua });
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
|
||||
@@ -32,7 +32,7 @@ async function ok(resp: Awaited<ReturnType<APIRequestContext['get']>>, what: str
|
||||
async function mintAccount(fixture: SuiteFixture, tag: string): Promise<Account> {
|
||||
const admin = await request.newContext({
|
||||
baseURL: fixture.baseURL,
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${fixture.apiToken}` }
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${fixture.adminSessionToken}` }
|
||||
});
|
||||
const session = await request.newContext({ baseURL: fixture.baseURL });
|
||||
try {
|
||||
|
||||
@@ -30,7 +30,7 @@ async function ok(resp: Awaited<ReturnType<APIRequestContext['get']>>, what: str
|
||||
async function mintAccount(fixture: SuiteFixture, tag: string): Promise<Account> {
|
||||
const admin = await request.newContext({
|
||||
baseURL: fixture.baseURL,
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${fixture.apiToken}` }
|
||||
extraHTTPHeaders: { Authorization: `Bearer ${fixture.adminSessionToken}` }
|
||||
});
|
||||
try {
|
||||
const username = `tabs${tag}`;
|
||||
|
||||
Reference in New Issue
Block a user