Skip to content

Commit 0cd6bf8

Browse files
committed
fix: skip env config write on metadata-only project updates
- updateProject: don't call overrideEnvironmentConfigOverride when there are no env-config keys to set, so metadata-only PATCHes (e.g. onboarding_state) on local-emulator projects no longer trip the LOCAL_EMULATOR_ENV_CONFIG_BLOCKED guard. - Drop the "rejects non-existent config files" test: the local-emulator POST endpoint now auto-creates the file (#1413), and that path is already covered by "writes default config for empty files". - failed-emails-digest test: poll until both the verify-email and test-email failures have been recorded instead of relying on a fixed 10s wait, and restore the original snapshot.
1 parent 35fd07b commit 0cd6bf8

3 files changed

Lines changed: 40 additions & 39 deletions

File tree

apps/backend/src/lib/projects.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -314,11 +314,13 @@ export async function createOrUpdateProjectWithLegacyConfig(
314314
configOverrideOverride['apps.installed.authentication.enabled'] ??= true;
315315
configOverrideOverride['apps.installed.emails.enabled'] ??= true;
316316
}
317-
await overrideEnvironmentConfigOverride({
318-
projectId: projectId,
319-
branchId: branchId,
320-
environmentConfigOverrideOverride: configOverrideOverride,
321-
});
317+
if (Object.keys(configOverrideOverride).length > 0) {
318+
await overrideEnvironmentConfigOverride({
319+
projectId: projectId,
320+
branchId: branchId,
321+
environmentConfigOverrideOverride: configOverrideOverride,
322+
});
323+
}
322324
const result = await getProject(projectId);
323325
if (!result) {
324326
throw new StackAssertionError("Project not found after creation/update", { projectId });

apps/e2e/tests/backend/endpoints/api/v1/internal/failed-emails-digest.test.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -102,24 +102,31 @@ describe("with valid credentials", () => {
102102
}
103103
`);
104104

105-
await wait(10_000);
106-
107-
const response = await niceBackendFetch("/api/v1/internal/failed-emails-digest", {
108-
method: "POST",
109-
headers: { "Authorization": "Bearer mock_cron_secret" },
110-
query: {
111-
dry_run: `${isDryRun}`,
112-
},
113-
});
114-
expect(response.status).toBe(200);
115-
116-
const failedEmailsByTenancy = response.body.failed_emails_by_tenancy;
117-
const mockProjectFailedEmails = failedEmailsByTenancy.filter(
118-
(batch: any) => batch.tenant_owner_emails.includes(backendContext.value.mailbox.emailAddress)
119-
).map((batch: any) => ({
120-
...batch,
121-
emails: [...batch.emails].sort((a, b) => stringCompare(a.subject, b.subject)),
122-
}));
105+
// Failed emails are written asynchronously; poll until both the test
106+
// email and the verification email have been recorded for our tenancy.
107+
let mockProjectFailedEmails: any[] = [];
108+
for (let i = 0; i < 30; i++) {
109+
const response = await niceBackendFetch("/api/v1/internal/failed-emails-digest", {
110+
method: "POST",
111+
headers: { "Authorization": "Bearer mock_cron_secret" },
112+
query: {
113+
dry_run: `${isDryRun}`,
114+
},
115+
});
116+
expect(response.status).toBe(200);
117+
const failedEmailsByTenancy = response.body.failed_emails_by_tenancy;
118+
mockProjectFailedEmails = failedEmailsByTenancy.filter(
119+
(batch: any) => batch.tenant_owner_emails.includes(backendContext.value.mailbox.emailAddress)
120+
).map((batch: any) => ({
121+
...batch,
122+
emails: [...batch.emails].sort((a, b) => stringCompare(a.subject, b.subject)),
123+
tenant_owner_emails: [...batch.tenant_owner_emails].sort(),
124+
}));
125+
if (mockProjectFailedEmails[0]?.emails?.length >= 2) {
126+
break;
127+
}
128+
await wait(1_000);
129+
}
123130

124131
if (process.env.STACK_TEST_SOURCE_OF_TRUTH === "true") {
125132
expect(mockProjectFailedEmails).toMatchInlineSnapshot(`[]`);
@@ -128,14 +135,21 @@ describe("with valid credentials", () => {
128135
[
129136
{
130137
"emails": [
138+
{
139+
"subject": "",
140+
"to": ["User ID: <stripped UUID>"],
141+
},
131142
{
132143
"subject": "Verify your email at Test Failed Emails Project",
133144
"to": ["default-mailbox--<stripped UUID>@stack-generated.example.com"],
134145
},
135146
],
136147
"project_id": "<stripped UUID>",
137148
"tenancy_id": "<stripped UUID>",
138-
"tenant_owner_emails": ["default-mailbox--<stripped UUID>@stack-generated.example.com"],
149+
"tenant_owner_emails": [
150+
"default-mailbox--<stripped UUID>@stack-generated.example.com",
151+
"default-mailbox--<stripped UUID>@stack-generated.example.com",
152+
],
139153
},
140154
]
141155
`);

apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,6 @@ describe("local emulator project endpoint", () => {
5353
}
5454
});
5555

56-
it.runIf(isLocalEmulator)("rejects non-existent config files", async ({ expect }) => {
57-
const nonExistentPath = `/tmp/${randomUUID()}/stack.config.ts`;
58-
59-
const response = await niceBackendFetch(LOCAL_EMULATOR_PROJECT_ENDPOINT, {
60-
accessType: "admin",
61-
method: "POST",
62-
body: {
63-
absolute_file_path: nonExistentPath,
64-
},
65-
});
66-
67-
expect(response.status).toBe(400);
68-
expect(response.body).toContain("Config file not found");
69-
});
70-
7156
it.runIf(isLocalEmulator)("writes default config for empty files", async ({ expect }) => {
7257
const filePath = `/tmp/${randomUUID()}/stack.config.ts`;
7358
await fs.mkdir(path.dirname(filePath), { recursive: true });

0 commit comments

Comments
 (0)