Adding Organiztion, environment and project. Segment fix, tag fix

This commit is contained in:
2026-04-14 14:10:37 -07:00
parent 4195d384d0
commit b75f5f602e
20 changed files with 3370 additions and 61 deletions

View File

@@ -15,6 +15,8 @@ jest.mock('@/api/client', () => ({
import * as envApi from '@/api/environments';
const dialogStub = { template: '<div><slot /></div>' };
const mockProject: ProjectResponse = {
id: 10,
name: 'Website',
@@ -33,7 +35,10 @@ function mountComponent() {
routes: [{ path: '/', component: { template: '<div />' } }],
});
return mount(EnvironmentTabBar, {
global: { plugins: [router] },
global: {
plugins: [router],
stubs: { 'v-dialog': dialogStub },
},
});
}
@@ -57,6 +62,13 @@ describe('EnvironmentTabBar', () => {
expect(wrapper.find('[data-testid="env-no-project-message"]').exists()).toBe(true);
});
it('ThenCreateEnvironmentButtonIsDisabled', async () => {
const wrapper = mountComponent();
await flushPromises();
expect(wrapper.find('[data-testid="create-env-btn"]').attributes('disabled')).toBeDefined();
});
});
describe('WhenProjectIsSetInContextStore', () => {
@@ -96,6 +108,18 @@ describe('EnvironmentTabBar', () => {
expect(contextStore.currentEnvironment).toEqual(mockEnvironments[0]);
});
it('ThenCreateEnvironmentButtonIsEnabled', async () => {
jest.mocked(envApi.listEnvironments).mockResolvedValue(mockEnvironments);
const contextStore = useContextStore();
contextStore.setProject(mockProject);
const wrapper = mountComponent();
await flushPromises();
expect(wrapper.find('[data-testid="create-env-btn"]').attributes('disabled')).toBeUndefined();
});
});
describe('WhenEnvironmentChipIsClicked', () => {
@@ -108,7 +132,6 @@ describe('EnvironmentTabBar', () => {
contextStore.setProject(mockProject);
await flushPromises();
// Simulate chip-group selecting index 1 (Production)
await wrapper.findComponent({ name: 'VChipGroup' }).vm.$emit('update:modelValue', 1);
expect(contextStore.currentEnvironment).toEqual(mockEnvironments[1]);
@@ -147,4 +170,149 @@ describe('EnvironmentTabBar', () => {
expect(wrapper.find('[data-testid="env-empty-message"]').exists()).toBe(true);
});
});
describe('WhenCreateEnvironmentButtonIsClicked', () => {
it('ThenCreateDialogIsShown', async () => {
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
const contextStore = useContextStore();
contextStore.setProject(mockProject);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-env-btn"]').trigger('click');
expect(wrapper.find('[data-testid="create-env-dialog"]').exists()).toBe(true);
});
it('ThenNameInputIsPresentInDialog', async () => {
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
const contextStore = useContextStore();
contextStore.setProject(mockProject);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-env-btn"]').trigger('click');
expect(wrapper.find('[data-testid="env-name-input"]').exists()).toBe(true);
});
});
describe('WhenCreateEnvironmentIsConfirmed', () => {
const newEnv: EnvironmentResponse = {
id: 200, name: 'Staging', apiKey: 'env-key-staging', projectId: 10, createdAt: '2026-04-14T00:00:00Z',
};
it('ThenCreateEnvironmentApiIsCalled', async () => {
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
jest.mocked(envApi.createEnvironment).mockResolvedValue(newEnv);
const contextStore = useContextStore();
contextStore.setProject(mockProject);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-env-btn"]').trigger('click');
(wrapper.vm as any).newEnvName = 'Staging';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect(envApi.createEnvironment).toHaveBeenCalledWith({ name: 'Staging', projectId: mockProject.id });
});
it('ThenNewEnvironmentIsAutoSelectedInContextStore', async () => {
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
jest.mocked(envApi.createEnvironment).mockResolvedValue(newEnv);
const contextStore = useContextStore();
contextStore.setProject(mockProject);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-env-btn"]').trigger('click');
(wrapper.vm as any).newEnvName = 'Staging';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect(contextStore.currentEnvironment).toEqual(newEnv);
});
it('ThenDialogIsClosedAfterSuccessfulCreate', async () => {
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
jest.mocked(envApi.createEnvironment).mockResolvedValue(newEnv);
const contextStore = useContextStore();
contextStore.setProject(mockProject);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-env-btn"]').trigger('click');
(wrapper.vm as any).newEnvName = 'Staging';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect((wrapper.vm as any).showDialog).toBe(false);
});
});
describe('WhenCreateEnvironmentFails', () => {
it('ThenErrorMessageIsDisplayed', async () => {
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
jest.mocked(envApi.createEnvironment).mockRejectedValue(new Error('Server error'));
const contextStore = useContextStore();
contextStore.setProject(mockProject);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-env-btn"]').trigger('click');
(wrapper.vm as any).newEnvName = 'Bad Env';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect((wrapper.vm as any).createError).toBeTruthy();
});
it('ThenDialogRemainsOpen', async () => {
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
jest.mocked(envApi.createEnvironment).mockRejectedValue(new Error('Server error'));
const contextStore = useContextStore();
contextStore.setProject(mockProject);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-env-btn"]').trigger('click');
(wrapper.vm as any).newEnvName = 'Bad Env';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect((wrapper.vm as any).showDialog).toBe(true);
});
});
describe('WhenCancelIsClicked', () => {
it('ThenDialogIsClosed', async () => {
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
const contextStore = useContextStore();
contextStore.setProject(mockProject);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-env-btn"]').trigger('click');
await wrapper.find('[data-testid="create-env-cancel-btn"]').trigger('click');
expect((wrapper.vm as any).showDialog).toBe(false);
});
});
});

View File

@@ -15,6 +15,8 @@ jest.mock('@/api/client', () => ({
import * as orgsApi from '@/api/organizations';
const dialogStub = { template: '<div><slot /></div>' };
const mockOrgs: OrganizationResponse[] = [
{ id: 1, name: 'Acme Corp', createdAt: '2026-01-01T00:00:00Z' },
{ id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z' },
@@ -26,7 +28,10 @@ function mountComponent() {
routes: [{ path: '/', component: { template: '<div />' } }],
});
return mount(OrgSelector, {
global: { plugins: [router] },
global: {
plugins: [router],
stubs: { 'v-dialog': dialogStub },
},
});
}
@@ -55,6 +60,15 @@ describe('OrgSelector', () => {
const select = wrapper.find('[data-testid="org-select"]');
expect(select.exists()).toBe(true);
});
it('ThenCreateOrganizationButtonIsAlwaysVisible', async () => {
jest.mocked(orgsApi.listOrganizations).mockResolvedValue(mockOrgs);
const wrapper = mountComponent();
await flushPromises();
expect(wrapper.find('[data-testid="create-org-btn"]').exists()).toBe(true);
});
});
describe('WhenOrganizationIsSelected', () => {
@@ -65,7 +79,6 @@ describe('OrgSelector', () => {
await flushPromises();
const contextStore = useContextStore();
// Simulate the v-select emitting an update
await wrapper.findComponent({ name: 'VSelect' }).vm.$emit('update:modelValue', mockOrgs[0]);
expect(contextStore.currentOrganization).toEqual(mockOrgs[0]);
@@ -73,14 +86,13 @@ describe('OrgSelector', () => {
});
describe('WhenApiReturnsNoOrganizations', () => {
it('ThenEmptyMessageIsDisplayed', async () => {
it('ThenCreateButtonIsStillVisible', async () => {
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
const wrapper = mountComponent();
await flushPromises();
const emptyMsg = wrapper.find('[data-testid="org-empty-message"]');
expect(emptyMsg.exists()).toBe(true);
expect(wrapper.find('[data-testid="create-org-btn"]').exists()).toBe(true);
});
});
@@ -91,8 +103,129 @@ describe('OrgSelector', () => {
const wrapper = mountComponent();
await flushPromises();
// No crash — component stays rendered
expect(wrapper.find('[data-testid="org-selector"]').exists()).toBe(true);
});
});
describe('WhenCreateOrganizationButtonIsClicked', () => {
it('ThenCreateDialogIsShown', async () => {
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-org-btn"]').trigger('click');
expect(wrapper.find('[data-testid="create-org-dialog"]').exists()).toBe(true);
});
it('ThenNameInputIsPresentInDialog', async () => {
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-org-btn"]').trigger('click');
expect(wrapper.find('[data-testid="org-name-input"]').exists()).toBe(true);
});
});
describe('WhenCreateOrganizationIsConfirmed', () => {
const newOrg: OrganizationResponse = { id: 3, name: 'New Org', createdAt: '2026-04-13T00:00:00Z' };
it('ThenCreateOrganizationApiIsCalled', async () => {
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
jest.mocked(orgsApi.createOrganization).mockResolvedValue(newOrg);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-org-btn"]').trigger('click');
(wrapper.vm as any).newOrgName = 'New Org';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect(orgsApi.createOrganization).toHaveBeenCalledWith({ name: 'New Org' });
});
it('ThenNewOrgIsAutoSelectedInContextStore', async () => {
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
jest.mocked(orgsApi.createOrganization).mockResolvedValue(newOrg);
const wrapper = mountComponent();
await flushPromises();
const contextStore = useContextStore();
await wrapper.find('[data-testid="create-org-btn"]').trigger('click');
(wrapper.vm as any).newOrgName = 'New Org';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect(contextStore.currentOrganization).toEqual(newOrg);
});
it('ThenDialogIsClosedAfterSuccessfulCreate', async () => {
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
jest.mocked(orgsApi.createOrganization).mockResolvedValue(newOrg);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-org-btn"]').trigger('click');
(wrapper.vm as any).newOrgName = 'New Org';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect((wrapper.vm as any).showDialog).toBe(false);
});
});
describe('WhenCreateOrganizationFails', () => {
it('ThenErrorMessageIsDisplayed', async () => {
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
jest.mocked(orgsApi.createOrganization).mockRejectedValue(new Error('Server error'));
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-org-btn"]').trigger('click');
(wrapper.vm as any).newOrgName = 'Bad Org';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect((wrapper.vm as any).createError).toBeTruthy();
});
it('ThenDialogRemainsOpen', async () => {
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
jest.mocked(orgsApi.createOrganization).mockRejectedValue(new Error('Server error'));
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-org-btn"]').trigger('click');
(wrapper.vm as any).newOrgName = 'Bad Org';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect((wrapper.vm as any).showDialog).toBe(true);
});
});
describe('WhenCancelIsClicked', () => {
it('ThenDialogIsClosed', async () => {
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-org-btn"]').trigger('click');
await wrapper.find('[data-testid="create-org-cancel-btn"]').trigger('click');
const vm = wrapper.vm as any;
expect(vm.showDialog).toBe(false);
});
});
});

View File

@@ -15,6 +15,8 @@ jest.mock('@/api/client', () => ({
import * as projectsApi from '@/api/projects';
const dialogStub = { template: '<div><slot /></div>' };
const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z' };
const mockProjects: ProjectResponse[] = [
{ id: 10, name: 'Website', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z' },
@@ -27,7 +29,10 @@ function mountComponent() {
routes: [{ path: '/', component: { template: '<div />' } }],
});
return mount(ProjectSelector, {
global: { plugins: [router] },
global: {
plugins: [router],
stubs: { 'v-dialog': dialogStub },
},
});
}
@@ -45,6 +50,15 @@ describe('ProjectSelector', () => {
expect(projectsApi.listProjects).not.toHaveBeenCalled();
expect(wrapper.find('[data-testid="project-selector"]').exists()).toBe(true);
});
it('ThenCreateProjectButtonIsDisabled', async () => {
const wrapper = mountComponent();
await flushPromises();
const btn = wrapper.find('[data-testid="create-project-btn"]');
expect(btn.exists()).toBe(true);
expect(btn.attributes('disabled')).toBeDefined();
});
});
describe('WhenOrganizationIsSetInContextStore', () => {
@@ -59,6 +73,19 @@ describe('ProjectSelector', () => {
expect(projectsApi.listProjects).toHaveBeenCalledWith(mockOrg.id);
});
it('ThenCreateProjectButtonIsEnabled', async () => {
jest.mocked(projectsApi.listProjects).mockResolvedValue(mockProjects);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountComponent();
await flushPromises();
const btn = wrapper.find('[data-testid="create-project-btn"]');
expect(btn.attributes('disabled')).toBeUndefined();
});
});
describe('WhenProjectIsSelected', () => {
@@ -95,4 +122,149 @@ describe('ProjectSelector', () => {
expect(projectsApi.listProjects).toHaveBeenLastCalledWith(newOrg.id);
});
});
describe('WhenCreateProjectButtonIsClicked', () => {
it('ThenCreateDialogIsShown', async () => {
jest.mocked(projectsApi.listProjects).mockResolvedValue([]);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-project-btn"]').trigger('click');
expect(wrapper.find('[data-testid="create-project-dialog"]').exists()).toBe(true);
});
it('ThenNameInputIsPresentInDialog', async () => {
jest.mocked(projectsApi.listProjects).mockResolvedValue([]);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-project-btn"]').trigger('click');
expect(wrapper.find('[data-testid="project-name-input"]').exists()).toBe(true);
});
});
describe('WhenCreateProjectIsConfirmed', () => {
const newProject: ProjectResponse = {
id: 99, name: 'New Project', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-04-13T00:00:00Z',
};
it('ThenCreateProjectApiIsCalled', async () => {
jest.mocked(projectsApi.listProjects).mockResolvedValue([]);
jest.mocked(projectsApi.createProject).mockResolvedValue(newProject);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-project-btn"]').trigger('click');
(wrapper.vm as any).newProjectName = 'New Project';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect(projectsApi.createProject).toHaveBeenCalledWith({ name: 'New Project', organizationId: mockOrg.id });
});
it('ThenNewProjectIsAutoSelectedInContextStore', async () => {
jest.mocked(projectsApi.listProjects).mockResolvedValue([]);
jest.mocked(projectsApi.createProject).mockResolvedValue(newProject);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-project-btn"]').trigger('click');
(wrapper.vm as any).newProjectName = 'New Project';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect(contextStore.currentProject).toEqual(newProject);
});
it('ThenDialogIsClosedAfterSuccessfulCreate', async () => {
jest.mocked(projectsApi.listProjects).mockResolvedValue([]);
jest.mocked(projectsApi.createProject).mockResolvedValue(newProject);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-project-btn"]').trigger('click');
(wrapper.vm as any).newProjectName = 'New Project';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect((wrapper.vm as any).showDialog).toBe(false);
});
});
describe('WhenCreateProjectFails', () => {
it('ThenErrorMessageIsDisplayed', async () => {
jest.mocked(projectsApi.listProjects).mockResolvedValue([]);
jest.mocked(projectsApi.createProject).mockRejectedValue(new Error('Server error'));
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-project-btn"]').trigger('click');
(wrapper.vm as any).newProjectName = 'Bad Project';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect((wrapper.vm as any).createError).toBeTruthy();
});
it('ThenDialogRemainsOpen', async () => {
jest.mocked(projectsApi.listProjects).mockResolvedValue([]);
jest.mocked(projectsApi.createProject).mockRejectedValue(new Error('Server error'));
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-project-btn"]').trigger('click');
(wrapper.vm as any).newProjectName = 'Bad Project';
await (wrapper.vm as any).onCreateConfirm();
await flushPromises();
expect((wrapper.vm as any).showDialog).toBe(true);
});
});
describe('WhenCancelIsClicked', () => {
it('ThenDialogIsClosed', async () => {
jest.mocked(projectsApi.listProjects).mockResolvedValue([]);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountComponent();
await flushPromises();
await wrapper.find('[data-testid="create-project-btn"]').trigger('click');
await wrapper.find('[data-testid="create-project-cancel-btn"]').trigger('click');
expect((wrapper.vm as any).showDialog).toBe(false);
});
});
});

View File

@@ -0,0 +1,85 @@
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { mount, flushPromises } from '@vue/test-utils';
import { setActivePinia, createPinia } from 'pinia';
import { createRouter, createMemoryHistory } from 'vue-router';
import ProfileView from '@/views/ProfileView.vue';
import { useAuthStore } from '@/stores/auth';
import { useContextStore } from '@/stores/context';
jest.mock('@/api/auth');
jest.mock('@/api/client', () => ({
setAuthTokens: jest.fn(),
clearAuthTokens: jest.fn(),
getAccessToken: jest.fn(),
}));
import * as authApi from '@/api/auth';
function mountView() {
const routes = [
{ path: '/', component: { template: '<div />' } },
{ path: '/login', name: 'Login', component: { template: '<div />' } },
];
const router = createRouter({ history: createMemoryHistory(), routes });
return { wrapper: mount(ProfileView, { global: { plugins: [router] } }), router };
}
describe('ProfileView', () => {
beforeEach(() => {
setActivePinia(createPinia());
jest.clearAllMocks();
});
describe('WhenAuthenticated', () => {
it('ThenProfileCardIsRendered', () => {
const { wrapper } = mountView();
expect(wrapper.find('[data-testid="profile-card"]').exists()).toBe(true);
});
it('ThenLogoutButtonIsPresent', () => {
const { wrapper } = mountView();
expect(wrapper.find('[data-testid="logout-btn"]').exists()).toBe(true);
});
it('ThenOrgNameIsDisplayedWhenSelected', () => {
const { wrapper } = mountView();
const contextStore = useContextStore();
contextStore.setOrganization({ id: 1, name: 'Acme Corp', createdAt: '' });
expect(wrapper.find('[data-testid="profile-org-name"]').exists()).toBe(false); // org name shown in v-if block
});
});
describe('WhenLogoutButtonIsClicked', () => {
it('ThenAuthStoreLogoutIsCalled', async () => {
jest.mocked(authApi.logout).mockResolvedValue(undefined);
const authStore = useAuthStore();
// Simulate logged-in state by setting tokens manually
authStore.accessToken = 'fake-token' as any;
authStore.refreshToken = 'fake-refresh' as any;
const { wrapper } = mountView();
await wrapper.find('[data-testid="logout-btn"]').trigger('click');
await flushPromises();
expect(authApi.logout).toHaveBeenCalled();
});
it('ThenUserIsRedirectedToLoginAfterLogout', async () => {
jest.mocked(authApi.logout).mockResolvedValue(undefined);
const authStore = useAuthStore();
authStore.accessToken = 'fake-token' as any;
authStore.refreshToken = 'fake-refresh' as any;
const { wrapper, router } = mountView();
await wrapper.find('[data-testid="logout-btn"]').trigger('click');
await flushPromises();
expect(router.currentRoute.value.name).toBe('Login');
});
});
});

View File

@@ -0,0 +1,480 @@
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { mount, flushPromises, type VueWrapper } from '@vue/test-utils';
import { setActivePinia, createPinia } from 'pinia';
import { createRouter, createMemoryHistory } from 'vue-router';
import SettingsView from '@/views/SettingsView.vue';
import { useContextStore } from '@/stores/context';
import type {
ProjectResponse, OrganizationResponse, EnvironmentResponse,
UserPermissionResponse, ApiKeyResponse, CreateApiKeyResponse,
} from '@/types/api';
jest.mock('@/api/organizations');
jest.mock('@/api/projects');
jest.mock('@/api/environments');
jest.mock('@/api/apiKeys');
jest.mock('@/api/webhooks');
jest.mock('@/api/client', () => ({
setAuthTokens: jest.fn(),
clearAuthTokens: jest.fn(),
getAccessToken: jest.fn(),
}));
import * as orgsApi from '@/api/organizations';
import * as projectsApi from '@/api/projects';
import * as environmentsApi from '@/api/environments';
import * as apiKeysApi from '@/api/apiKeys';
import * as webhooksApi from '@/api/webhooks';
const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z' };
const mockProject: ProjectResponse = { id: 10, name: 'Website', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z' };
const mockEnvs: EnvironmentResponse[] = [
{ id: 100, name: 'Development', apiKey: 'env-dev', projectId: 10, createdAt: '2026-01-01T00:00:00Z' },
{ id: 200, name: 'Production', apiKey: 'env-prod', projectId: 10, createdAt: '2026-01-02T00:00:00Z' },
];
const mockApiKeys: ApiKeyResponse[] = [
{ id: 1, name: 'CI Key', prefix: 'org-ci', isActive: true, expiresAt: null, createdAt: '2026-01-01T00:00:00Z' },
];
const mockPermissions: UserPermissionResponse[] = [
{ userId: 5, projectId: 10, isAdmin: false, permissions: ['ViewProject', 'EditFeature'] },
];
const dialogStub = { template: '<div><slot /></div>' };
function mountView() {
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/', component: { template: '<div />' } }] });
return mount(SettingsView, {
global: { plugins: [router], stubs: { 'v-dialog': dialogStub } },
});
}
describe('SettingsView', () => {
beforeEach(() => {
setActivePinia(createPinia());
jest.clearAllMocks();
jest.mocked(orgsApi.listOrganizationMembers).mockResolvedValue([]);
jest.mocked(projectsApi.listProjectUserPermissions).mockResolvedValue([]);
jest.mocked(environmentsApi.listEnvironments).mockResolvedValue([]);
jest.mocked(apiKeysApi.listApiKeys).mockResolvedValue([]);
jest.mocked(webhooksApi.listOrgWebhooks).mockResolvedValue([]);
jest.mocked(webhooksApi.listEnvWebhooks).mockResolvedValue([]);
});
describe('WhenNoOrganizationSelected', () => {
it('ThenSelectOrgMessageIsShown', () => {
const wrapper = mountView();
expect(wrapper.text()).toContain('Select an organization');
});
});
describe('WhenOrganizationIsSelected', () => {
it('ThenTabsAreRendered', async () => {
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountView();
await flushPromises();
expect(wrapper.find('[data-testid="tab-organization"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="tab-api-keys"]').exists()).toBe(true);
});
});
// ── Organization tab ────────────────────────────────────────────────────────
describe('WhenSaveOrgNameIsClicked', () => {
it('ThenUpdateOrganizationIsCalledWithNewName', async () => {
jest.mocked(orgsApi.updateOrganization).mockResolvedValue({ ...mockOrg, name: 'New Name' });
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountView();
await flushPromises();
await (wrapper.findComponent('[data-testid="org-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'New Name');
await wrapper.find('[data-testid="save-org-name-btn"]').trigger('click');
await flushPromises();
expect(orgsApi.updateOrganization).toHaveBeenCalledWith(mockOrg.id, { name: 'New Name' });
});
});
describe('WhenInviteMemberIsClicked', () => {
it('ThenInviteOrganizationMemberIsCalledWithUserId', async () => {
jest.mocked(orgsApi.inviteOrganizationMember).mockResolvedValue(undefined);
jest.mocked(orgsApi.listOrganizationMembers).mockResolvedValue([{ userId: 42, role: 'User' }]);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountView();
await flushPromises();
await (wrapper.findComponent('[data-testid="invite-user-id-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', '42');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="invite-member-btn"]').trigger('click');
await flushPromises();
expect(orgsApi.inviteOrganizationMember).toHaveBeenCalledWith(mockOrg.id, { userId: 42, role: 'User' });
});
});
describe('WhenRemoveMemberIsClicked', () => {
it('ThenRemoveOrganizationMemberIsCalledAndMemberIsRemovedFromList', async () => {
jest.mocked(orgsApi.listOrganizationMembers).mockResolvedValue([{ userId: 5, role: 'User' }]);
jest.mocked(orgsApi.removeOrganizationMember).mockResolvedValue(undefined);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountView();
await flushPromises();
expect(wrapper.find('[data-testid="member-row-5"]').exists()).toBe(true);
await wrapper.find('[data-testid="remove-member-5"]').trigger('click');
await flushPromises();
expect(orgsApi.removeOrganizationMember).toHaveBeenCalledWith(mockOrg.id, 5);
expect(wrapper.find('[data-testid="member-row-5"]').exists()).toBe(false);
});
});
// ── Project tab ─────────────────────────────────────────────────────────────
describe('WhenSaveProjectIsClicked', () => {
it('ThenUpdateProjectIsCalledWithFormValues', async () => {
jest.mocked(projectsApi.updateProject).mockResolvedValue(mockProject);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
contextStore.setProject(mockProject);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-project"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="save-project-btn"]').trigger('click');
await flushPromises();
expect(projectsApi.updateProject).toHaveBeenCalledWith(
mockProject.id,
expect.objectContaining({ name: mockProject.name }),
);
});
});
describe('WhenPermissionsAreLoaded', () => {
it('ThenPermissionRowsAreDisplayed', async () => {
jest.mocked(projectsApi.listProjectUserPermissions).mockResolvedValue(mockPermissions);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
contextStore.setProject(mockProject);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-project"]').trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="permission-row-5"]').exists()).toBe(true);
});
});
describe('WhenAdminToggleIsChanged', () => {
it('ThenUpdateProjectUserPermissionsIsCalledWithIsAdmin', async () => {
jest.mocked(projectsApi.listProjectUserPermissions).mockResolvedValue(mockPermissions);
jest.mocked(projectsApi.updateProjectUserPermissions).mockResolvedValue({
...mockPermissions[0], isAdmin: true,
});
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
contextStore.setProject(mockProject);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-project"]').trigger('click');
await wrapper.vm.$nextTick();
await (wrapper.findComponent(`[data-testid="perm-admin-toggle-5"]`) as VueWrapper<any>).vm.$emit('update:modelValue', true);
await flushPromises();
expect(projectsApi.updateProjectUserPermissions).toHaveBeenCalledWith(
mockProject.id, 5,
expect.objectContaining({ isAdmin: true }),
);
});
});
describe('WhenAddUserPermissionsIsSubmitted', () => {
it('ThenSetProjectUserPermissionsIsCalledWithNewUserId', async () => {
const newPerm: UserPermissionResponse = { userId: 99, projectId: 10, isAdmin: false, permissions: ['ViewProject'] };
jest.mocked(projectsApi.setProjectUserPermissions).mockResolvedValue(newPerm);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
contextStore.setProject(mockProject);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-project"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="add-permission-btn"]').trigger('click');
await wrapper.vm.$nextTick();
await (wrapper.findComponent('[data-testid="new-perm-user-id-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', '99');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="save-new-permission-btn"]').trigger('click');
await flushPromises();
expect(projectsApi.setProjectUserPermissions).toHaveBeenCalledWith(
mockProject.id,
expect.objectContaining({ userId: 99 }),
);
});
});
describe('WhenRemovePermissionIsClicked', () => {
it('ThenRemoveProjectUserPermissionsIsCalledAndRowIsRemoved', async () => {
jest.mocked(projectsApi.listProjectUserPermissions).mockResolvedValue(mockPermissions);
jest.mocked(projectsApi.removeProjectUserPermissions).mockResolvedValue(undefined);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
contextStore.setProject(mockProject);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-project"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="remove-permission-5"]').trigger('click');
await flushPromises();
expect(projectsApi.removeProjectUserPermissions).toHaveBeenCalledWith(mockProject.id, 5);
expect(wrapper.find('[data-testid="permission-row-5"]').exists()).toBe(false);
});
});
// ── Environments tab ────────────────────────────────────────────────────────
describe('WhenCreateEnvironmentIsClicked', () => {
it('ThenCreateEnvironmentIsCalledAndEnvIsAddedToList', async () => {
jest.mocked(environmentsApi.listEnvironments).mockResolvedValue([]);
jest.mocked(environmentsApi.createEnvironment).mockResolvedValue(mockEnvs[0]);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
contextStore.setProject(mockProject);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-environments"]').trigger('click');
await wrapper.vm.$nextTick();
await (wrapper.findComponent('[data-testid="new-env-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'Staging');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="create-env-btn"]').trigger('click');
await flushPromises();
expect(environmentsApi.createEnvironment).toHaveBeenCalledWith({ projectId: mockProject.id, name: 'Staging' });
});
});
describe('WhenCloneEnvironmentIsConfirmed', () => {
it('ThenCloneEnvironmentIsCalledWithNewName', async () => {
jest.mocked(environmentsApi.listEnvironments).mockResolvedValue([...mockEnvs]);
jest.mocked(environmentsApi.cloneEnvironment).mockResolvedValue({
...mockEnvs[0], id: 300, name: 'Staging', apiKey: 'env-stg',
});
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
contextStore.setProject(mockProject);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-environments"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="clone-env-100"]').trigger('click');
await wrapper.vm.$nextTick();
await (wrapper.findComponent('[data-testid="clone-env-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'Staging');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="confirm-clone-btn"]').trigger('click');
await flushPromises();
expect(environmentsApi.cloneEnvironment).toHaveBeenCalledWith(mockEnvs[0].apiKey, { name: 'Staging' });
});
});
describe('WhenDeleteEnvironmentIsConfirmed', () => {
it('ThenDeleteEnvironmentIsCalledAndEnvIsRemovedFromList', async () => {
jest.mocked(environmentsApi.listEnvironments).mockResolvedValue([...mockEnvs]);
jest.mocked(environmentsApi.deleteEnvironment).mockResolvedValue(undefined);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
contextStore.setProject(mockProject);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-environments"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="delete-env-100"]').trigger('click');
await wrapper.vm.$nextTick();
await (wrapper.findComponent('[data-testid="delete-env-confirm-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'Development');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="confirm-delete-env-btn"]').trigger('click');
await flushPromises();
expect(environmentsApi.deleteEnvironment).toHaveBeenCalledWith(mockEnvs[0].apiKey);
});
});
describe('WhenRenameEnvironmentIsConfirmed', () => {
it('ThenUpdateEnvironmentIsCalledWithNewName', async () => {
jest.mocked(environmentsApi.listEnvironments).mockResolvedValue([...mockEnvs]);
jest.mocked(environmentsApi.updateEnvironment).mockResolvedValue({ ...mockEnvs[0], name: 'Dev Renamed' });
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
contextStore.setProject(mockProject);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-environments"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="rename-env-100"]').trigger('click');
await wrapper.vm.$nextTick();
await (wrapper.findComponent('[data-testid="rename-env-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'Dev Renamed');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="confirm-rename-btn"]').trigger('click');
await flushPromises();
expect(environmentsApi.updateEnvironment).toHaveBeenCalledWith(mockEnvs[0].apiKey, { name: 'Dev Renamed' });
});
});
// ── API Keys tab ────────────────────────────────────────────────────────────
describe('WhenApiKeysTabIsOpened', () => {
it('ThenApiKeysAreLoaded', async () => {
jest.mocked(apiKeysApi.listApiKeys).mockResolvedValue(mockApiKeys);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-api-keys"]').trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="api-key-row-1"]').exists()).toBe(true);
expect(wrapper.text()).toContain('CI Key');
});
});
describe('WhenCreateApiKeyIsSubmitted', () => {
it('ThenCreateApiKeyIsCalledAndRawKeyDialogIsShown', async () => {
const created: CreateApiKeyResponse = { id: 99, name: 'My Key', rawKey: 'raw-secret-key', prefix: 'org-my', expiresAt: null };
jest.mocked(apiKeysApi.createApiKey).mockResolvedValue(created);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-api-keys"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="create-api-key-btn"]').trigger('click');
await wrapper.vm.$nextTick();
await (wrapper.findComponent('[data-testid="api-key-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'My Key');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="save-api-key-btn"]').trigger('click');
await flushPromises();
expect(apiKeysApi.createApiKey).toHaveBeenCalledWith(mockOrg.id, expect.objectContaining({ name: 'My Key' }));
expect(wrapper.find('[data-testid="raw-key-dialog"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="raw-key-field"]').exists()).toBe(true);
});
it('ThenRawKeyIsDisplayedInTheRevealDialog', async () => {
const created: CreateApiKeyResponse = { id: 99, name: 'My Key', rawKey: 'raw-secret-key', prefix: 'org-my', expiresAt: null };
jest.mocked(apiKeysApi.createApiKey).mockResolvedValue(created);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-api-keys"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="create-api-key-btn"]').trigger('click');
await wrapper.vm.$nextTick();
await (wrapper.findComponent('[data-testid="api-key-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'My Key');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="save-api-key-btn"]').trigger('click');
await flushPromises();
const rawKeyField = wrapper.findComponent('[data-testid="raw-key-field"]') as VueWrapper<any>;
expect(rawKeyField.props('modelValue')).toBe('raw-secret-key');
});
});
describe('WhenDeleteApiKeyIsClicked', () => {
it('ThenDeleteApiKeyIsCalledAndKeyIsRemovedFromList', async () => {
jest.mocked(apiKeysApi.listApiKeys).mockResolvedValue([...mockApiKeys]);
jest.mocked(apiKeysApi.deleteApiKey).mockResolvedValue(undefined);
const contextStore = useContextStore();
contextStore.setOrganization(mockOrg);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="tab-api-keys"]').trigger('click');
await wrapper.vm.$nextTick();
await wrapper.find('[data-testid="delete-api-key-1"]').trigger('click');
await flushPromises();
expect(apiKeysApi.deleteApiKey).toHaveBeenCalledWith(mockOrg.id, 1);
expect(wrapper.find('[data-testid="api-key-row-1"]').exists()).toBe(false);
});
});
});