stack/apps/e2e/tests/js/config.test.ts
BilalG1 609579abab
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
feat(hexclave): PR 3 — native @hexclave/* source rename + delete dual-publish wiring (#1482)
2026-05-29 15:21:59 -07:00

650 lines
21 KiB
TypeScript

import { pick } from "@hexclave/shared/dist/utils/objects";
import { describe } from "vitest";
import { it } from "../helpers";
import { createApp } from "./js-helpers";
describe("access control", () => {
it("serverApp project does not have config methods", async ({ expect }) => {
const { serverApp } = await createApp();
const project = await serverApp.getProject();
// Server apps only get basic Project type, not AdminProject
// So config methods should not exist
expect((project as any).updateConfig).toBeUndefined();
expect((project as any).pushConfig).toBeUndefined();
expect((project as any).updatePushedConfig).toBeUndefined();
expect((project as any).getConfig).toBeUndefined();
});
it("clientApp project does not have config methods", async ({ expect }) => {
const { clientApp } = await createApp();
const project = await clientApp.getProject();
// Client apps only get basic Project type, not AdminProject
// So config methods should not exist
expect((project as any).updateConfig).toBeUndefined();
expect((project as any).pushConfig).toBeUndefined();
expect((project as any).updatePushedConfig).toBeUndefined();
expect((project as any).getConfig).toBeUndefined();
});
it("only adminApp project has config methods", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// AdminApp gets AdminProject which has config methods
expect(typeof project.updateConfig).toBe('function');
expect(typeof project.pushConfig).toBe('function');
expect(typeof project.updatePushedConfig).toBe('function');
expect(typeof project.getConfig).toBe('function');
});
});
describe("error handling", () => {
it("updateConfig rejects non-existent config fields", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
await expect(project.updateConfig({
'nonExistentField.value': true,
} as any)).rejects.toThrow(/nonExistentField/);
});
it("pushConfig rejects non-existent config fields", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
await expect(project.pushConfig({
'nonExistentField.value': true,
} as any, { source: { type: "unlinked" } })).rejects.toThrow(/nonExistentField/);
});
it("updateConfig rejects invalid oauth provider type", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
await expect(project.updateConfig({
'auth.oauth.providers.invalid': {
type: 'not-a-real-provider',
isShared: false,
clientId: 'test',
clientSecret: 'test',
allowSignIn: true,
allowConnectedAccounts: true,
},
} as any)).rejects.toThrow(/type must be one of the following values/);
});
it("pushConfig rejects environment-only fields at branch level", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// pushConfig uses branch level, which doesn't allow environment-only fields
// like clientId, clientSecret, isShared
await expect(project.pushConfig({
'auth.oauth.providers.google': {
type: 'google',
isShared: false,
clientId: 'test-client-id',
clientSecret: 'test-secret',
allowSignIn: true,
allowConnectedAccounts: true,
},
} as any, { source: { type: "unlinked" } })).rejects.toThrow(/auth\.oauth\.providers/);
});
it("pushConfig allows branch-level oauth fields", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Branch-level fields only (no secrets)
await project.pushConfig({
'auth.oauth.providers.my_provider': {
type: 'google',
allowSignIn: true,
allowConnectedAccounts: true,
},
} as any, { source: { type: "unlinked" } });
const config = await project.getConfig();
expect(config.auth.oauth.providers['my_provider']).toBeDefined();
});
});
describe("getConfig", () => {
it("gets config", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
const config = await project.getConfig();
expect(pick(config, ["auth", "users", "teams"])).toMatchInlineSnapshot(`
{
"auth": {
"allowSignUp": true,
"oauth": {
"accountMergeStrategy": "link_method",
"providers": {},
},
"otp": { "allowSignIn": false },
"passkey": { "allowSignIn": false },
"password": { "allowSignIn": true },
"signUpRules": {},
"signUpRulesDefaultAction": "allow",
},
"teams": {
"allowClientTeamCreation": false,
"createPersonalTeamOnSignUp": false,
},
"users": { "allowClientUserDeletion": false },
}
`);
});
});
describe("updateConfig", () => {
it("updates config at environment level", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
const config = await project.getConfig();
expect(config['auth']).toMatchInlineSnapshot(`
{
"allowSignUp": true,
"oauth": {
"accountMergeStrategy": "link_method",
"providers": {},
},
"otp": { "allowSignIn": false },
"passkey": { "allowSignIn": false },
"password": { "allowSignIn": true },
"signUpRules": {},
"signUpRulesDefaultAction": "allow",
}
`);
await project.updateConfig({
'auth.allowSignUp': false,
});
const config2 = await project.getConfig();
expect(config2['auth']).toMatchInlineSnapshot(`
{
"allowSignUp": false,
"oauth": {
"accountMergeStrategy": "link_method",
"providers": {},
},
"otp": { "allowSignIn": false },
"passkey": { "allowSignIn": false },
"password": { "allowSignIn": true },
"signUpRules": {},
"signUpRulesDefaultAction": "allow",
}
`);
});
it("updateConfig merges with existing config", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Set first value
await project.updateConfig({
'teams.allowClientTeamCreation': true,
});
// Set second value
await project.updateConfig({
'users.allowClientUserDeletion': true,
});
// Both should be set
const config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(true);
expect(config.users.allowClientUserDeletion).toBe(true);
});
});
describe("pushConfig", () => {
it("pushConfig sets branch-level config", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Push config
await project.pushConfig({
'teams.allowClientTeamCreation': true,
'teams.createPersonalTeamOnSignUp': true,
}, { source: { type: "unlinked" } });
// Verify config is applied
const config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(true);
expect(config.teams.createPersonalTeamOnSignUp).toBe(true);
});
it("pushConfig replaces previous pushConfig", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Push first config
await project.pushConfig({
'teams.allowClientTeamCreation': true,
'teams.createPersonalTeamOnSignUp': true,
}, { source: { type: "unlinked" } });
// Verify first config is applied
let config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(true);
expect(config.teams.createPersonalTeamOnSignUp).toBe(true);
// Push second config (completely replaces first)
await project.pushConfig({
'auth.passkey.allowSignIn': true,
}, { source: { type: "unlinked" } });
// Verify old values are gone (back to defaults) and new value is set
config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(false); // back to default
expect(config.teams.createPersonalTeamOnSignUp).toBe(false); // back to default
expect(config.auth.passkey.allowSignIn).toBe(true); // new value
});
it("updateConfig takes precedence over pushConfig", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Push config first
await project.pushConfig({
'teams.allowClientTeamCreation': true,
}, { source: { type: "unlinked" } });
// Verify push is applied
let config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(true);
// updateConfig overrides the same value
await project.updateConfig({
'teams.allowClientTeamCreation': false,
});
// Environment-level (updateConfig) takes precedence
config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(false);
});
it("pushConfig does not affect updateConfig values", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// updateConfig sets environment-level value
await project.updateConfig({
'users.allowClientUserDeletion': true,
});
// pushConfig sets branch-level values
await project.pushConfig({
'teams.allowClientTeamCreation': true,
}, { source: { type: "unlinked" } });
// Both should be present
let config = await project.getConfig();
expect(config.users.allowClientUserDeletion).toBe(true); // from updateConfig
expect(config.teams.allowClientTeamCreation).toBe(true); // from pushConfig
// Push new config (replaces branch but not environment)
await project.pushConfig({
'auth.passkey.allowSignIn': true,
}, { source: { type: "unlinked" } });
// updateConfig value should still be there
config = await project.getConfig();
expect(config.users.allowClientUserDeletion).toBe(true); // still from updateConfig
expect(config.teams.allowClientTeamCreation).toBe(false); // back to default (old push gone)
expect(config.auth.passkey.allowSignIn).toBe(true); // from new push
});
});
describe("updatePushedConfig", () => {
it("updatePushedConfig merges into pushed config", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Push initial config
await project.pushConfig({
'teams.allowClientTeamCreation': true,
}, { source: { type: "unlinked" } });
// updatePushedConfig merges into the pushed config
await project.updatePushedConfig({
'teams.createPersonalTeamOnSignUp': true,
});
// Both values should be set
const config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(true);
expect(config.teams.createPersonalTeamOnSignUp).toBe(true);
});
it("pushConfig replaces updatePushedConfig changes", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Push initial config
await project.pushConfig({
'teams.allowClientTeamCreation': true,
}, { source: { type: "unlinked" } });
// updatePushedConfig adds a value
await project.updatePushedConfig({
'teams.createPersonalTeamOnSignUp': true,
});
// Verify both values are present
let config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(true);
expect(config.teams.createPersonalTeamOnSignUp).toBe(true);
// pushConfig replaces everything including updatePushedConfig changes
await project.pushConfig({
'auth.passkey.allowSignIn': true,
}, { source: { type: "unlinked" } });
// Old values should be gone
config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(false); // back to default
expect(config.teams.createPersonalTeamOnSignUp).toBe(false); // back to default
expect(config.auth.passkey.allowSignIn).toBe(true); // new push value
});
it("updateConfig takes precedence over updatePushedConfig", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// updatePushedConfig sets a value at branch level
await project.updatePushedConfig({
'teams.allowClientTeamCreation': true,
});
// Verify value is applied
let config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(true);
// updateConfig overrides at environment level
await project.updateConfig({
'teams.allowClientTeamCreation': false,
});
// Environment-level takes precedence
config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(false);
});
it("updatePushedConfig rejects environment-only fields", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// updatePushedConfig uses branch level, so environment-only fields should be rejected
await expect(project.updatePushedConfig({
'auth.oauth.providers.google': {
type: 'google',
isShared: false,
clientId: 'test-client-id',
clientSecret: 'test-secret',
allowSignIn: true,
allowConnectedAccounts: true,
},
} as any)).rejects.toThrow(/auth\.oauth\.providers/);
});
});
describe("resetConfigOverrideKeys", () => {
it("resetConfigOverrideKeys removes keys from environment override", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Set branch config
await project.updatePushedConfig({
'teams.allowClientTeamCreation': true,
});
// Set environment config that overrides branch
await project.updateConfig({
'teams.allowClientTeamCreation': false,
});
// Verify environment takes precedence
let config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(false);
// Reset the key from environment level
await project.resetConfigOverrideKeys("environment", ["teams.allowClientTeamCreation"]);
// Now branch config should win
config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(true);
});
it("resetConfigOverrideKeys with parent key removes children", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Set environment config with multiple team settings
await project.updateConfig({
'teams.allowClientTeamCreation': true,
'teams.createPersonalTeamOnSignUp': true,
'users.allowClientUserDeletion': true,
});
// Reset the parent "teams" key
await project.resetConfigOverrideKeys("environment", ["teams"]);
// Verify teams keys are gone but users key remains
const envOverride = await project.getConfigOverride("environment");
expect(envOverride["teams.allowClientTeamCreation"]).toBeUndefined();
expect(envOverride["teams.createPersonalTeamOnSignUp"]).toBeUndefined();
expect(envOverride["users.allowClientUserDeletion"]).toBe(true);
});
it("resetConfigOverrideKeys with non-existent keys is a no-op", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Set environment config
await project.updateConfig({
'teams.allowClientTeamCreation': true,
});
// Reset a non-existent key
await project.resetConfigOverrideKeys("environment", ["nonExistent.key"]);
// Config should be unchanged
const envOverride = await project.getConfigOverride("environment");
expect(envOverride["teams.allowClientTeamCreation"]).toBe(true);
});
it("resetConfigOverrideKeys on branch level works", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Set branch config
await project.updatePushedConfig({
'teams.allowClientTeamCreation': true,
'users.allowClientUserDeletion': true,
});
// Reset one branch key
await project.resetConfigOverrideKeys("branch", ["teams.allowClientTeamCreation"]);
const branchOverride = await project.getConfigOverride("branch");
expect(branchOverride["teams.allowClientTeamCreation"]).toBeUndefined();
expect(branchOverride["users.allowClientUserDeletion"]).toBe(true);
});
});
describe("pushedConfigSource", () => {
it("getPushedConfigSource returns unlinked by default", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
const source = await project.getPushedConfigSource();
expect(source.type).toBe("unlinked");
});
it("pushConfig with source sets the source", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
await project.pushConfig({
'teams.allowClientTeamCreation': true,
}, {
source: {
type: "pushed-from-github",
owner: "myorg",
repo: "myrepo",
branch: "main",
commitHash: "abc123",
configFilePath: "stack.config.ts",
},
});
const source = await project.getPushedConfigSource();
expect(source.type).toBe("pushed-from-github");
if (source.type === "pushed-from-github") {
expect(source.owner).toBe("myorg");
expect(source.repo).toBe("myrepo");
expect(source.branch).toBe("main");
expect(source.commitHash).toBe("abc123");
expect(source.configFilePath).toBe("stack.config.ts");
}
});
it("pushConfig without source defaults to unlinked", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// First push with source
await project.pushConfig({
'teams.allowClientTeamCreation': true,
}, {
source: {
type: "pushed-from-github",
owner: "myorg",
repo: "myrepo",
branch: "main",
commitHash: "abc123",
configFilePath: "stack.config.ts",
},
});
// Then push without source (should default to unlinked)
await project.pushConfig({
'teams.createPersonalTeamOnSignUp': true,
}, { source: { type: "unlinked" } });
const source = await project.getPushedConfigSource();
expect(source.type).toBe("unlinked");
});
it("updatePushedConfig preserves the source", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Push with GitHub source
await project.pushConfig({
'teams.allowClientTeamCreation': true,
}, {
source: {
type: "pushed-from-github",
owner: "myorg",
repo: "myrepo",
branch: "main",
commitHash: "abc123",
configFilePath: "stack.config.ts",
},
});
// Update pushed config
await project.updatePushedConfig({
'teams.createPersonalTeamOnSignUp': true,
});
// Source should still be GitHub
const source = await project.getPushedConfigSource();
expect(source.type).toBe("pushed-from-github");
});
it("unlinkPushedConfigSource sets source to unlinked", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Push with GitHub source
await project.pushConfig({
'teams.allowClientTeamCreation': true,
}, {
source: {
type: "pushed-from-github",
owner: "myorg",
repo: "myrepo",
branch: "main",
commitHash: "abc123",
configFilePath: "stack.config.ts",
},
});
// Verify source is GitHub
expect((await project.getPushedConfigSource()).type).toBe("pushed-from-github");
// Unlink
await project.unlinkPushedConfigSource();
// Verify source is now unlinked
const source = await project.getPushedConfigSource();
expect(source.type).toBe("unlinked");
});
it("unlinkPushedConfigSource preserves the config", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
// Push config with source
await project.pushConfig({
'teams.allowClientTeamCreation': true,
}, {
source: {
type: "pushed-from-github",
owner: "myorg",
repo: "myrepo",
branch: "main",
commitHash: "abc123",
configFilePath: "stack.config.ts",
},
});
// Unlink
await project.unlinkPushedConfigSource();
// Config should still be there
const config = await project.getConfig();
expect(config.teams.allowClientTeamCreation).toBe(true);
});
it("can set pushed-from-unknown source", async ({ expect }) => {
const { adminApp } = await createApp();
const project = await adminApp.getProject();
await project.pushConfig({
'teams.allowClientTeamCreation': true,
}, {
source: { type: "pushed-from-unknown" },
});
const source = await project.getPushedConfigSource();
expect(source.type).toBe("pushed-from-unknown");
});
});