mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Merge branch 'dev' into emulator-arm64-kvm-diagnostics
This commit is contained in:
commit
d67df1e469
@ -153,7 +153,7 @@ async function backfillSequenceIds(batchSize: number): Promise<boolean> {
|
||||
WHERE "VerificationCode"."projectId" = changed_teams."projectId"
|
||||
AND "VerificationCode"."branchId" = changed_teams."branchId"
|
||||
AND "VerificationCode"."type" = 'TEAM_INVITATION'
|
||||
AND "VerificationCode"."data"->>'team_id' = changed_teams."teamId"
|
||||
AND "VerificationCode"."data"->>'team_id' = changed_teams."teamId"::text
|
||||
AND "VerificationCode"."shouldUpdateSequenceId" = FALSE
|
||||
`;
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import { ensureUpstashSignature } from "@/lib/upstash";
|
||||
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
|
||||
import { getExternalDbSyncFusebox } from "@/lib/external-db-sync-metadata";
|
||||
import { yupNumber, yupObject, yupString, yupTuple } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { traceSpan } from "@/utils/telemetry";
|
||||
|
||||
export const POST = createSmartRouteHandler({
|
||||
@ -45,7 +44,11 @@ export const POST = createSmartRouteHandler({
|
||||
});
|
||||
if (!tenancy) {
|
||||
console.warn(`[sync-engine] Tenancy ${tenancyId} in queue but not found, assuming it was deleted.`);
|
||||
throw new StatusError(400, `Tenancy ${tenancyId} not found.`);
|
||||
span.setAttribute("stack.external-db-sync.ignored-missing-tenancy", true);
|
||||
return {
|
||||
statusCode: 200,
|
||||
bodyType: "success",
|
||||
};
|
||||
}
|
||||
|
||||
const needsResync = await traceSpan("external-db-sync.sync-engine.syncExternalDatabases", async (syncSpan) => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { wait } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { afterAll, beforeAll, describe, expect } from 'vitest';
|
||||
import { test } from '../../../../helpers';
|
||||
import { niceFetch, STACK_BACKEND_BASE_URL, test } from '../../../../helpers';
|
||||
import { withPortPrefix } from '../../../../helpers/ports';
|
||||
import { Auth, backendContext, InternalApiKey, Project, User, niceBackendFetch } from '../../../backend-helpers';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
@ -1173,6 +1173,83 @@ describe.sequential('External DB Sync - Basic Tests', () => {
|
||||
await waitForSyncedTeamInvitationDeletion(client, invitationId);
|
||||
}, TEST_TIMEOUT);
|
||||
|
||||
/**
|
||||
* What it does:
|
||||
* - Sends a team invitation, renames the team, and verifies team_display_name updates externally.
|
||||
*
|
||||
* Why it matters:
|
||||
* - Covers the sequencer cascade that marks TEAM_INVITATION rows for re-sync on team updates.
|
||||
*/
|
||||
test('TeamInvitation sync updates display name after team rename (Postgres)', async () => {
|
||||
const dbName = 'team_invitation_team_rename_test';
|
||||
const connectionString = await dbManager.createDatabase(dbName);
|
||||
|
||||
await createProjectWithExternalDb({
|
||||
main: {
|
||||
type: 'postgres',
|
||||
connectionString,
|
||||
}
|
||||
}, { display_name: 'Invitation Rename Test Project' });
|
||||
|
||||
const client = dbManager.getClient(dbName);
|
||||
const initialTeamName = 'Invitation Team Before Rename';
|
||||
const updatedTeamName = 'Invitation Team After Rename';
|
||||
const invitedEmail = `team-rename-${randomUUID()}@example.com`;
|
||||
|
||||
const createTeamResponse = await niceBackendFetch('/api/v1/teams', {
|
||||
accessType: 'admin',
|
||||
method: 'POST',
|
||||
body: { display_name: initialTeamName },
|
||||
});
|
||||
expect(createTeamResponse.status).toBe(201);
|
||||
const teamId = createTeamResponse.body.id;
|
||||
|
||||
const inviteResponse = await niceBackendFetch('/api/v1/team-invitations/send-code', {
|
||||
accessType: 'admin',
|
||||
method: 'POST',
|
||||
body: { team_id: teamId, email: invitedEmail, callback_url: 'http://localhost:12345/callback' },
|
||||
});
|
||||
expect(inviteResponse.status).toBe(200);
|
||||
|
||||
await waitForSyncedTeamInvitation(client, invitedEmail);
|
||||
|
||||
const initialInvitation = await client.query(
|
||||
`SELECT "team_display_name" FROM "team_invitations" WHERE "recipient_email" = $1`,
|
||||
[invitedEmail],
|
||||
);
|
||||
expect(initialInvitation.rows.length).toBe(1);
|
||||
expect(initialInvitation.rows[0].team_display_name).toBe(initialTeamName);
|
||||
|
||||
const updateTeamResponse = await niceBackendFetch(`/api/v1/teams/${teamId}`, {
|
||||
accessType: 'admin',
|
||||
method: 'PATCH',
|
||||
body: { display_name: updatedTeamName },
|
||||
});
|
||||
expect(updateTeamResponse.status).toBe(200);
|
||||
|
||||
await waitForCondition(
|
||||
async () => {
|
||||
const updatedInvitation = await client.query(
|
||||
`SELECT "team_display_name" FROM "team_invitations" WHERE "recipient_email" = $1`,
|
||||
[invitedEmail],
|
||||
);
|
||||
return updatedInvitation.rows.length === 1 && updatedInvitation.rows[0].team_display_name === updatedTeamName;
|
||||
},
|
||||
{
|
||||
timeoutMs: 180_000,
|
||||
intervalMs: 500,
|
||||
description: `team invitation for ${invitedEmail} to reflect renamed team`,
|
||||
},
|
||||
);
|
||||
|
||||
const finalInvitation = await client.query(
|
||||
`SELECT "team_display_name" FROM "team_invitations" WHERE "recipient_email" = $1`,
|
||||
[invitedEmail],
|
||||
);
|
||||
expect(finalInvitation.rows.length).toBe(1);
|
||||
expect(finalInvitation.rows[0].team_display_name).toBe(updatedTeamName);
|
||||
}, TEST_TIMEOUT);
|
||||
|
||||
/**
|
||||
* What it does:
|
||||
* - Sends a team invitation, queries ClickHouse analytics API to verify.
|
||||
@ -1524,6 +1601,21 @@ describe.sequential('External DB Sync - Basic Tests', () => {
|
||||
});
|
||||
}, TEST_TIMEOUT);
|
||||
|
||||
test('Sync engine ignores missing tenancy queue items', async () => {
|
||||
const response = await niceFetch(new URL('/api/latest/internal/external-db-sync/sync-engine', STACK_BACKEND_BASE_URL), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'upstash-signature': 'test-bypass',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenancyId: randomUUID(),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
}, TEST_TIMEOUT);
|
||||
|
||||
/**
|
||||
* What it does:
|
||||
* - Signs up a user (which creates a refresh token), waits for it to sync to the external DB.
|
||||
|
||||
@ -170,3 +170,6 @@ A: In `apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/vercel/pag
|
||||
|
||||
Q: Why can restricted users appear logged out on auth handler pages even with a valid session?
|
||||
A: `useUser()` filters out restricted users by default. In `packages/template/src/components-page/auth-page.tsx`, use `useUser({ includeRestricted: true })` and explicitly redirect restricted users to onboarding when `automaticRedirect` is enabled.
|
||||
|
||||
Q: Why can external-db-sync sequencer throw `operator does not exist: text = uuid` on team updates?
|
||||
A: In `apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts`, the TEAM_INVITATION cascade compares JSON text (`"VerificationCode"."data"->>'team_id'`) against `"Team"."teamId"` (`uuid`). Cast the UUID side to text (`changed_teams."teamId"::text`) in the WHERE clause so Postgres type resolution succeeds and team-invitation re-sync marking works.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user