mirror of
https://github.com/Kong/insomnia.git
synced 2026-11-02 11:05:33 -05:00
## Background Workspace `hasUncommittedChanges` / `hasUnpushedChanges` flags are written only by cloud sync. When a project stopped being a cloud project, those flags were never cleared, so the project overview cards kept showing "Uncommitted changes". Repro: 1. Open a cloud project whose workspaces have uncommitted changes. 2. Remove the project from the remote. 3. Open the project overview. The cards still show "Uncommitted changes" although the project is now local. A project converted from cloud to git carries the same residual flags, because the values come from its former cloud state. ## Changes - Clear both workspace sync flags when a project's `remoteId` becomes `null` in the project service. Remote deletion, conversion to local, cross-organization move, and conversion to git all update the project through this service. - Reuse the cleanup in the startup database repair to fix existing stale records. Only dirty workspace metas are written, and `modified` is preserved. - Add service and repair tests. ## Testing - `npm test -w insomnia-data` (538 tests) - `npm run type-check -w insomnia-data` - `npm run lint -w insomnia-data` ## References - INS-3899
937 lines
27 KiB
TypeScript
937 lines
27 KiB
TypeScript
// @ts-nocheck
|
|
import type { BaseModel } from 'insomnia-data';
|
|
import { models, services } from 'insomnia-data';
|
|
import { afterEach, assert, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { ChangeBufferEvent } from '../..';
|
|
import { database as db } from '../..';
|
|
import { createNedbDatabase } from './database-nedb';
|
|
import * as workspaceInitModel from './init-model/workspace';
|
|
import { repairDatabase } from './repair-database';
|
|
|
|
describe('init()', () => {
|
|
it('handles being initialized twice', async () => {
|
|
await db.init({
|
|
inMemoryOnly: true,
|
|
});
|
|
await db.init({
|
|
inMemoryOnly: true,
|
|
});
|
|
expect((await db.find(models.request.type)).length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('createNedbDatabase()', () => {
|
|
it('keeps documents and change listeners separate between instances', async () => {
|
|
await db.init({ inMemoryOnly: true }, true);
|
|
const appChanges: ChangeBufferEvent<BaseModel>[][] = [];
|
|
db.onChange(changes => {
|
|
appChanges.push(changes);
|
|
});
|
|
await db.update({ _id: 'req_app', type: models.request.type, parentId: 'wrk_app' });
|
|
appChanges.length = 0;
|
|
|
|
const scratchDb = createNedbDatabase();
|
|
await scratchDb.init({ inMemoryOnly: true });
|
|
const scratchChanges: ChangeBufferEvent<BaseModel>[][] = [];
|
|
scratchDb.onChange(changes => {
|
|
scratchChanges.push(changes);
|
|
});
|
|
await scratchDb.update({ _id: 'req_scratch', type: models.request.type, parentId: 'wrk_scratch' });
|
|
|
|
expect((await db.find(models.request.type)).map(doc => doc._id)).toEqual(['req_app']);
|
|
expect((await scratchDb.find(models.request.type)).map(doc => doc._id)).toEqual(['req_scratch']);
|
|
expect(appChanges).toEqual([]);
|
|
expect(scratchChanges).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('onChange()', () => {
|
|
beforeEach(async () => {
|
|
await db.init({ inMemoryOnly: true }, true);
|
|
});
|
|
it('handles change listeners', async () => {
|
|
const doc = {
|
|
type: models.request.type,
|
|
parentId: 'nothing',
|
|
name: 'foo',
|
|
};
|
|
const changesSeen: ChangeBufferEvent<BaseModel>[][] = [];
|
|
|
|
const callback = (change: ChangeBufferEvent<BaseModel>[]) => {
|
|
changesSeen.push(change);
|
|
};
|
|
|
|
db.onChange(callback);
|
|
const newDoc = await services.request.create(doc);
|
|
const updatedDoc = await services.request.update(newDoc, {
|
|
name: 'bar',
|
|
});
|
|
expect(changesSeen).toEqual([[['insert', newDoc, []]], [['update', updatedDoc, [{ name: 'bar' }]]]]);
|
|
});
|
|
});
|
|
|
|
describe('bufferChanges()', () => {
|
|
it('properly buffers changes', async () => {
|
|
const doc = {
|
|
type: models.request.type,
|
|
parentId: 'n/a',
|
|
name: 'foo',
|
|
};
|
|
const changesSeen: ChangeBufferEvent<BaseModel>[][] = [];
|
|
|
|
const callback = (change: ChangeBufferEvent<BaseModel>[]) => {
|
|
changesSeen.push(change);
|
|
};
|
|
|
|
db.onChange(callback);
|
|
await db.bufferChanges();
|
|
const newDoc = await services.request.create(doc);
|
|
// @ts-expect-error -- TSCONVERSION appears to be genuine
|
|
const updatedDoc = await services.request.update(newDoc);
|
|
// Assert no change seen before flush
|
|
expect(changesSeen.length).toBe(0);
|
|
// Assert changes seen after flush
|
|
await db.flushChanges();
|
|
expect(changesSeen).toEqual([
|
|
[
|
|
['insert', newDoc, []],
|
|
['update', updatedDoc, [undefined]],
|
|
],
|
|
]);
|
|
// Assert no more changes seen after flush again
|
|
await db.flushChanges();
|
|
expect(changesSeen).toEqual([
|
|
[
|
|
['insert', newDoc, []],
|
|
['update', updatedDoc, [undefined]],
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('should auto flush after a default wait', async () => {
|
|
const doc = {
|
|
type: models.request.type,
|
|
parentId: 'n/a',
|
|
name: 'foo',
|
|
};
|
|
const changesSeen: ChangeBufferEvent<BaseModel>[][] = [];
|
|
|
|
const callback = (change: ChangeBufferEvent<BaseModel>[]) => {
|
|
changesSeen.push(change);
|
|
};
|
|
|
|
db.onChange(callback);
|
|
await db.bufferChanges();
|
|
const newDoc = await services.request.create(doc);
|
|
// @ts-expect-error -- TSCONVERSION appears to be genuine
|
|
const updatedDoc = await services.request.update(newDoc);
|
|
// Default flush timeout is 1000ms after starting buffering
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
expect(changesSeen).toEqual([
|
|
[
|
|
['insert', newDoc, []],
|
|
['update', updatedDoc, [undefined]],
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('should auto flush after a specified wait', async () => {
|
|
const doc = {
|
|
type: models.request.type,
|
|
parentId: 'n/a',
|
|
name: 'foo',
|
|
};
|
|
const changesSeen: ChangeBufferEvent<BaseModel>[][] = [];
|
|
|
|
const callback = (change: ChangeBufferEvent<BaseModel>[]) => {
|
|
changesSeen.push(change);
|
|
};
|
|
|
|
db.onChange(callback);
|
|
await db.bufferChanges(500);
|
|
const newDoc = await services.request.create(doc);
|
|
// @ts-expect-error -- TSCONVERSION appears to be genuine
|
|
const updatedDoc = await services.request.update(newDoc);
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
expect(changesSeen).toEqual([
|
|
[
|
|
['insert', newDoc, []],
|
|
['update', updatedDoc, [undefined]],
|
|
],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('bufferChangesIndefinitely()', () => {
|
|
it('should not auto flush', async () => {
|
|
const doc = {
|
|
type: models.request.type,
|
|
parentId: 'n/a',
|
|
name: 'foo',
|
|
};
|
|
const changesSeen: ChangeBufferEvent<BaseModel>[][] = [];
|
|
|
|
const callback = (change: ChangeBufferEvent<BaseModel>[]) => {
|
|
changesSeen.push(change);
|
|
};
|
|
|
|
db.onChange(callback);
|
|
await db.bufferChangesIndefinitely();
|
|
const newDoc = await services.request.create(doc);
|
|
// @ts-expect-error -- TSCONVERSION appears to be genuine
|
|
const updatedDoc = await services.request.update(newDoc);
|
|
// Default flush timeout is 1000ms after starting buffering
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
// Assert no change seen before flush
|
|
expect(changesSeen.length).toBe(0);
|
|
// Assert changes seen after flush
|
|
await db.flushChanges();
|
|
expect(changesSeen).toEqual([
|
|
[
|
|
['insert', newDoc, []],
|
|
['update', updatedDoc, [undefined]],
|
|
],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('requestCreate()', () => {
|
|
it('creates a valid request', async () => {
|
|
const now = Date.now();
|
|
const patch = {
|
|
name: 'My Request',
|
|
parentId: 'wrk_123',
|
|
};
|
|
const r = await services.request.create(patch);
|
|
expect(Object.keys(r).length).toBe(24);
|
|
expect(r._id).toMatch(/^req_[a-zA-Z0-9]{32}$/);
|
|
expect(r.created).toBeGreaterThanOrEqual(now);
|
|
expect(r.modified).toBeGreaterThanOrEqual(now);
|
|
expect(r.type).toBe('Request');
|
|
expect(r.name).toBe('My Request');
|
|
expect(r.url).toBe('');
|
|
expect(r.method).toBe('GET');
|
|
expect(r.body).toEqual({});
|
|
expect(r.parameters).toEqual([]);
|
|
expect(r.headers).toEqual([]);
|
|
expect(r.authentication).toEqual({});
|
|
expect(r.metaSortKey).toBeLessThanOrEqual(-1 * now);
|
|
expect(r.parentId).toBe('wrk_123');
|
|
});
|
|
|
|
it('throws when missing parentID', () => {
|
|
const fn = () =>
|
|
services.request.create({
|
|
name: 'My Request',
|
|
});
|
|
|
|
expect(fn).toThrowError('New Requests missing `parentId`');
|
|
});
|
|
});
|
|
|
|
describe('_repairDatabase()', async () => {
|
|
beforeEach(async () => {
|
|
await db.init({ inMemoryOnly: true }, true);
|
|
});
|
|
|
|
it('fixes duplicate environments', async () => {
|
|
// Create Workspace with no children
|
|
const project = await services.project.create();
|
|
const workspace = await services.workspace.create({
|
|
_id: 'w1',
|
|
parentId: project._id,
|
|
});
|
|
// Create one set of sub environments
|
|
await services.environment.create({
|
|
_id: 'b1',
|
|
parentId: 'w1',
|
|
data: {
|
|
foo: 'b1',
|
|
b1: true,
|
|
},
|
|
});
|
|
await services.environment.create({
|
|
_id: 'b1_sub1',
|
|
parentId: 'b1',
|
|
data: {
|
|
foo: '1',
|
|
},
|
|
});
|
|
await services.environment.create({
|
|
_id: 'b1_sub2',
|
|
parentId: 'b1',
|
|
data: {
|
|
foo: '2',
|
|
},
|
|
});
|
|
// Create second set of sub environments
|
|
await services.environment.create({
|
|
_id: 'b2',
|
|
parentId: 'w1',
|
|
data: {
|
|
foo: 'b2',
|
|
b2: true,
|
|
},
|
|
});
|
|
await services.environment.create({
|
|
_id: 'b2_sub1',
|
|
parentId: 'b2',
|
|
data: {
|
|
foo: '3',
|
|
},
|
|
});
|
|
await services.environment.create({
|
|
_id: 'b2_sub2',
|
|
parentId: 'b2',
|
|
data: {
|
|
foo: '4',
|
|
},
|
|
});
|
|
// Make sure we have 6 environments and one workspace
|
|
expect((await db.getWithDescendants(workspace)).length).toBe(7);
|
|
const descendants = (await db.getWithDescendants(workspace)).map(d => ({
|
|
_id: d._id,
|
|
parentId: d.parentId,
|
|
data: d.data || null,
|
|
}));
|
|
expect(descendants).toEqual([
|
|
{
|
|
_id: 'w1',
|
|
data: null,
|
|
parentId: workspace.parentId,
|
|
},
|
|
{
|
|
_id: 'b1',
|
|
data: {
|
|
foo: 'b1',
|
|
b1: true,
|
|
},
|
|
parentId: 'w1',
|
|
},
|
|
{
|
|
_id: 'b2',
|
|
data: {
|
|
foo: 'b2',
|
|
b2: true,
|
|
},
|
|
parentId: 'w1',
|
|
},
|
|
{
|
|
_id: 'b1_sub1',
|
|
data: {
|
|
foo: '1',
|
|
},
|
|
parentId: 'b1',
|
|
},
|
|
{
|
|
_id: 'b1_sub2',
|
|
data: {
|
|
foo: '2',
|
|
},
|
|
parentId: 'b1',
|
|
},
|
|
{
|
|
_id: 'b2_sub1',
|
|
data: {
|
|
foo: '3',
|
|
},
|
|
parentId: 'b2',
|
|
},
|
|
{
|
|
_id: 'b2_sub2',
|
|
data: {
|
|
foo: '4',
|
|
},
|
|
parentId: 'b2',
|
|
},
|
|
]);
|
|
|
|
// Run the fix algorithm
|
|
await repairDatabase();
|
|
|
|
// Make sure things get adjusted
|
|
const descendants2 = (await db.getWithDescendants(workspace)).map(d => ({
|
|
_id: d._id,
|
|
parentId: d.parentId,
|
|
data: d.data || null,
|
|
}));
|
|
expect(descendants2).toEqual([
|
|
{
|
|
_id: 'w1',
|
|
data: null,
|
|
parentId: workspace.parentId,
|
|
},
|
|
{
|
|
_id: 'b1',
|
|
data: {
|
|
foo: 'b1',
|
|
b1: true,
|
|
b2: true,
|
|
},
|
|
parentId: 'w1',
|
|
},
|
|
// Extra base environments should have been deleted
|
|
// {_id: 'b2', data: {foo: 'bar'}, parentId: 'w1'},
|
|
// Sub environments should have been moved to new "master" base environment
|
|
{
|
|
_id: 'b1_sub1',
|
|
data: {
|
|
foo: '1',
|
|
},
|
|
parentId: 'b1',
|
|
},
|
|
{
|
|
_id: 'b1_sub2',
|
|
data: {
|
|
foo: '2',
|
|
},
|
|
parentId: 'b1',
|
|
},
|
|
{
|
|
_id: 'b2_sub1',
|
|
data: {
|
|
foo: '3',
|
|
},
|
|
parentId: 'b1',
|
|
},
|
|
{
|
|
_id: 'b2_sub2',
|
|
data: {
|
|
foo: '4',
|
|
},
|
|
parentId: 'b1',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('fixes duplicate cookie jars', async () => {
|
|
// Create Workspace with no children
|
|
const project = await services.project.create();
|
|
const workspace = await services.workspace.create({
|
|
_id: 'w1',
|
|
parentId: project._id,
|
|
});
|
|
expect((await db.getWithDescendants(workspace)).length).toBe(1);
|
|
// Create one set of sub environments
|
|
await services.cookieJar.create({
|
|
_id: 'j1',
|
|
parentId: 'w1',
|
|
cookies: [
|
|
{
|
|
id: '1',
|
|
key: 'foo',
|
|
value: '1',
|
|
},
|
|
{
|
|
id: 'j1_1',
|
|
key: 'j1',
|
|
value: '1',
|
|
},
|
|
],
|
|
});
|
|
await services.cookieJar.create({
|
|
_id: 'j2',
|
|
parentId: 'w1',
|
|
cookies: [
|
|
{
|
|
id: '1',
|
|
key: 'foo',
|
|
value: '2',
|
|
},
|
|
{
|
|
id: 'j2_1',
|
|
key: 'j2',
|
|
value: '2',
|
|
},
|
|
],
|
|
});
|
|
// Make sure we have 2 cookie jars and one workspace
|
|
expect((await db.getWithDescendants(workspace)).length).toBe(3);
|
|
const descendants = (await db.getWithDescendants(workspace)).map(d => ({
|
|
_id: d._id,
|
|
cookies: d.cookies || null,
|
|
parentId: d.parentId,
|
|
}));
|
|
expect(descendants).toEqual([
|
|
{
|
|
_id: 'w1',
|
|
cookies: null,
|
|
parentId: workspace.parentId,
|
|
},
|
|
{
|
|
_id: 'j1',
|
|
parentId: 'w1',
|
|
cookies: [
|
|
{
|
|
id: '1',
|
|
key: 'foo',
|
|
value: '1',
|
|
source: 'manual',
|
|
},
|
|
{
|
|
id: 'j1_1',
|
|
key: 'j1',
|
|
value: '1',
|
|
source: 'manual',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
_id: 'j2',
|
|
parentId: 'w1',
|
|
cookies: [
|
|
{
|
|
id: '1',
|
|
key: 'foo',
|
|
value: '2',
|
|
source: 'manual',
|
|
},
|
|
{
|
|
id: 'j2_1',
|
|
key: 'j2',
|
|
value: '2',
|
|
source: 'manual',
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
// Run the fix algorithm
|
|
await repairDatabase();
|
|
// Make sure things get adjusted
|
|
const descendants2 = (await db.getWithDescendants(workspace)).map(d => ({
|
|
_id: d._id,
|
|
cookies: d.cookies || null,
|
|
parentId: d.parentId,
|
|
}));
|
|
expect(descendants2).toEqual([
|
|
{
|
|
_id: 'w1',
|
|
cookies: null,
|
|
parentId: workspace.parentId,
|
|
},
|
|
{
|
|
_id: 'j1',
|
|
parentId: 'w1',
|
|
cookies: [
|
|
{
|
|
id: '1',
|
|
key: 'foo',
|
|
value: '1',
|
|
source: 'manual',
|
|
},
|
|
{
|
|
id: 'j1_1',
|
|
key: 'j1',
|
|
value: '1',
|
|
source: 'manual',
|
|
},
|
|
{
|
|
id: 'j2_1',
|
|
key: 'j2',
|
|
value: '2',
|
|
source: 'manual',
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('fixes the filename on an apiSpec', async () => {
|
|
// Create Workspace with apiSpec child (migration in workspace will automatically create this as it is not mocked)
|
|
const w1 = await services.workspace.create({
|
|
_id: 'w1',
|
|
name: 'Workspace 1',
|
|
});
|
|
const w2 = await services.workspace.create({
|
|
_id: 'w2',
|
|
name: 'Workspace 2',
|
|
});
|
|
const w3 = await services.workspace.create({
|
|
_id: 'w3',
|
|
name: 'Workspace 3',
|
|
});
|
|
const w4 = await services.workspace.create({
|
|
_id: 'w4',
|
|
name: models.apiSpec.init().fileName,
|
|
});
|
|
await services.apiSpec.updateOrCreateForParentId(w1._id, {
|
|
fileName: '',
|
|
});
|
|
await services.apiSpec.updateOrCreateForParentId(w4._id, {
|
|
fileName: models.apiSpec.init().fileName,
|
|
});
|
|
const modifiedBefore = (await services.apiSpec.getByParentId(w4._id))?.modified;
|
|
await services.apiSpec.updateOrCreateForParentId(w2._id, {
|
|
fileName: models.apiSpec.init().fileName,
|
|
});
|
|
await services.apiSpec.updateOrCreateForParentId(w3._id, {
|
|
fileName: 'Unique name',
|
|
});
|
|
// Make sure we have everything
|
|
expect((await services.apiSpec.getByParentId(w1._id))?.fileName).toBe('');
|
|
expect((await services.apiSpec.getByParentId(w2._id))?.fileName).toBe('New API Collection');
|
|
expect((await services.apiSpec.getByParentId(w3._id))?.fileName).toBe('Unique name');
|
|
// Run the fix algorithm
|
|
await repairDatabase();
|
|
// Make sure things get adjusted
|
|
expect((await services.apiSpec.getByParentId(w1._id))?.fileName).toBe('Workspace 1'); // Should fix
|
|
expect((await services.apiSpec.getByParentId(w2._id))?.fileName).toBe('Workspace 2'); // Should fix
|
|
expect((await services.apiSpec.getByParentId(w3._id))?.fileName).toBe('Unique name'); // should not fix
|
|
expect((await services.apiSpec.getByParentId(w4._id))?.fileName).toBe(models.apiSpec.init().fileName); // backfill is a no-op, should skip
|
|
expect((await services.apiSpec.getByParentId(w4._id))?.modified).toBe(modifiedBefore); // should not restamp
|
|
});
|
|
|
|
it('fixes old git uris', async () => {
|
|
const oldRepoWithSuffix = await services.gitRepository.create({
|
|
uri: 'https://github.com/foo/bar.git',
|
|
uriNeedsMigration: true,
|
|
});
|
|
const oldRepoWithoutSuffix = await services.gitRepository.create({
|
|
uri: 'https://github.com/foo/bar',
|
|
uriNeedsMigration: true,
|
|
});
|
|
const newRepoWithSuffix = await services.gitRepository.create({
|
|
uri: 'https://github.com/foo/bar.git',
|
|
});
|
|
const newRepoWithoutSuffix = await services.gitRepository.create({
|
|
uri: 'https://github.com/foo/bar',
|
|
});
|
|
await repairDatabase();
|
|
expect(await db.findOne(models.gitRepository.type, { _id: oldRepoWithSuffix._id })).toEqual(
|
|
expect.objectContaining({
|
|
uri: 'https://github.com/foo/bar.git',
|
|
uriNeedsMigration: false,
|
|
}),
|
|
);
|
|
expect(await db.findOne(models.gitRepository.type, { _id: oldRepoWithoutSuffix._id })).toEqual(
|
|
expect.objectContaining({
|
|
uri: 'https://github.com/foo/bar.git',
|
|
uriNeedsMigration: false,
|
|
}),
|
|
);
|
|
expect(await db.findOne(models.gitRepository.type, { _id: newRepoWithSuffix._id })).toEqual(
|
|
expect.objectContaining({
|
|
uri: 'https://github.com/foo/bar.git',
|
|
uriNeedsMigration: false,
|
|
}),
|
|
);
|
|
expect(await db.findOne(models.gitRepository.type, { _id: newRepoWithoutSuffix._id })).toEqual(
|
|
expect.objectContaining({
|
|
uri: 'https://github.com/foo/bar',
|
|
uriNeedsMigration: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('clears stale workspace sync flags for non-remote projects only', async () => {
|
|
const localProject = await services.project.create({ name: 'local', remoteId: null });
|
|
const localWorkspace = await services.workspace.create({ name: 'local-ws', parentId: localProject._id });
|
|
const localMeta = await services.workspaceMeta.create({
|
|
parentId: localWorkspace._id,
|
|
hasUncommittedChanges: true,
|
|
hasUnpushedChanges: true,
|
|
});
|
|
|
|
const gitProject = await services.project.create({
|
|
name: 'git',
|
|
remoteId: null,
|
|
gitRepositoryId: 'git_test',
|
|
});
|
|
const gitWorkspace = await services.workspace.create({ name: 'git-ws', parentId: gitProject._id });
|
|
const gitMeta = await services.workspaceMeta.create({
|
|
parentId: gitWorkspace._id,
|
|
hasUncommittedChanges: true,
|
|
hasUnpushedChanges: true,
|
|
});
|
|
|
|
const cloudProject = await services.project.create({ name: 'cloud', remoteId: 'proj_remote' });
|
|
const cloudWorkspace = await services.workspace.create({ name: 'cloud-ws', parentId: cloudProject._id });
|
|
const cloudMeta = await services.workspaceMeta.create({
|
|
parentId: cloudWorkspace._id,
|
|
hasUncommittedChanges: true,
|
|
hasUnpushedChanges: true,
|
|
});
|
|
|
|
await repairDatabase();
|
|
|
|
for (const [workspace, meta] of [
|
|
[localWorkspace, localMeta],
|
|
[gitWorkspace, gitMeta],
|
|
] as const) {
|
|
expect(await services.workspaceMeta.getByParentId(workspace._id)).toEqual(
|
|
expect.objectContaining({
|
|
hasUncommittedChanges: false,
|
|
hasUnpushedChanges: false,
|
|
modified: meta.modified,
|
|
}),
|
|
);
|
|
}
|
|
expect(await services.workspaceMeta.getByParentId(cloudWorkspace._id)).toEqual(
|
|
expect.objectContaining({
|
|
hasUncommittedChanges: true,
|
|
hasUnpushedChanges: true,
|
|
modified: cloudMeta.modified,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('duplicate()', () => {
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
it('should overwrite appropriate fields on the parent when duplicating', async () => {
|
|
const date = 1_478_795_580_200;
|
|
Date.now = vi.fn().mockReturnValue(date);
|
|
const workspace = await services.workspace.create({
|
|
name: 'Test Workspace',
|
|
});
|
|
const newDescription = 'test';
|
|
const duplicated = await db.duplicate(workspace, {
|
|
description: newDescription,
|
|
});
|
|
expect(duplicated._id).not.toEqual(workspace._id);
|
|
expect(duplicated._id).toMatch(/^wrk_[a-z0-9]{32}$/);
|
|
delete workspace._id;
|
|
delete duplicated._id;
|
|
expect(duplicated).toEqual({
|
|
...workspace,
|
|
description: newDescription,
|
|
modified: date,
|
|
created: date,
|
|
type: models.workspace.type,
|
|
});
|
|
});
|
|
|
|
it('should should not call migrate when duplicating', async () => {
|
|
const workspace = await services.workspace.create({
|
|
name: 'Test Workspace',
|
|
});
|
|
const spy = vi.spyOn(workspaceInitModel, 'migrate');
|
|
await db.duplicate(workspace);
|
|
expect(spy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should emit insert changes when duplicating', async () => {
|
|
const workspace = await services.workspace.create({ name: 'Workspace' });
|
|
const folder = await services.requestGroup.create({ parentId: workspace._id, name: 'Folder' });
|
|
await services.request.create({ parentId: folder._id, name: 'Request' });
|
|
const changesSeen: ChangeBufferEvent<BaseModel>[][] = [];
|
|
db.onChange(changes => changesSeen.push(changes));
|
|
|
|
const duplicatedFolder = await db.duplicate(folder);
|
|
|
|
expect(changesSeen).toHaveLength(1);
|
|
expect(changesSeen[0]).toEqual([
|
|
['insert', duplicatedFolder, []],
|
|
['insert', expect.objectContaining({ parentId: duplicatedFolder._id, type: models.request.type }), []],
|
|
]);
|
|
});
|
|
|
|
it('should rewrite chained request references when duplicating a folder', async () => {
|
|
const workspace = await services.workspace.create({ name: 'Workspace' });
|
|
const folder = await services.requestGroup.create({ parentId: workspace._id, name: 'Folder' });
|
|
const req1 = await services.request.create({
|
|
parentId: folder._id,
|
|
name: 'Request 1',
|
|
url: 'https://example.com/first',
|
|
});
|
|
const req2 = await services.request.create({
|
|
parentId: folder._id,
|
|
name: 'Request 2',
|
|
url: `https://example.com/{% response 'body', '${req1._id}', 'b64::JC5pZA==::46b', 'never', 60 %}`,
|
|
});
|
|
|
|
const duplicatedFolder = await db.duplicate(folder);
|
|
|
|
// Find duplicated requests
|
|
const duplicatedRequests = await db.find<typeof req1>(models.request.type, {
|
|
parentId: duplicatedFolder._id,
|
|
});
|
|
expect(duplicatedRequests).toHaveLength(2);
|
|
|
|
const dupReq1 = duplicatedRequests.find(r => r.name === 'Request 1');
|
|
const dupReq2 = duplicatedRequests.find(r => r.name === 'Request 2');
|
|
expect(dupReq1).toBeDefined();
|
|
expect(dupReq2).toBeDefined();
|
|
|
|
// The duplicated req2 should reference the duplicated req1, not the original
|
|
expect(dupReq2!.url).not.toContain(req1._id);
|
|
expect(dupReq2!.url).toContain(dupReq1!._id);
|
|
expect(dupReq2!.url).toContain("{% response 'body',");
|
|
|
|
// Original should be unchanged
|
|
const originalReq2 = await db.findOne(models.request.type, { _id: req2._id });
|
|
expect(originalReq2!.url).toContain(req1._id);
|
|
});
|
|
});
|
|
|
|
describe('docCreate()', () => {
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
it('should call migrate when creating', async () => {
|
|
const spy = vi.spyOn(workspaceInitModel, 'migrate');
|
|
await db.docCreate(models.workspace.type, {
|
|
name: 'Test Workspace',
|
|
});
|
|
// TODO: This is actually called twice, not once - we should avoid the double model.init() call.
|
|
expect(spy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('withAncestors()', () => {
|
|
it('should return itself and all parents but exclude siblings', async () => {
|
|
const spc = await services.project.create();
|
|
const wrk = await services.workspace.create({
|
|
parentId: spc._id,
|
|
});
|
|
const wrkReq = await services.request.create({
|
|
parentId: wrk._id,
|
|
});
|
|
const wrkGrpcReq = await services.grpcRequest.create({
|
|
parentId: wrk._id,
|
|
});
|
|
const grp = await services.requestGroup.create({
|
|
parentId: wrk._id,
|
|
});
|
|
const grpReq = await services.request.create({
|
|
parentId: grp._id,
|
|
});
|
|
const grpGrpcReq = await services.grpcRequest.create({
|
|
parentId: grp._id,
|
|
});
|
|
// Workspace child searching for ancestors
|
|
await expect(db.withAncestors(wrk)).resolves.toStrictEqual([wrk, spc]);
|
|
await expect(db.withAncestors(wrkReq)).resolves.toStrictEqual([wrkReq, wrk, spc]);
|
|
await expect(db.withAncestors(wrkGrpcReq)).resolves.toStrictEqual([wrkGrpcReq, wrk, spc]);
|
|
// Group searching for ancestors
|
|
await expect(db.withAncestors(grp)).resolves.toStrictEqual([grp, wrk, spc]);
|
|
// Group child searching for ancestors
|
|
await expect(db.withAncestors(grpReq)).resolves.toStrictEqual([grpReq, grp, wrk, spc]);
|
|
await expect(db.withAncestors(grpGrpcReq)).resolves.toStrictEqual([grpGrpcReq, grp, wrk, spc]);
|
|
// Group child searching for ancestors with filters
|
|
await expect(db.withAncestors(grpGrpcReq, [models.requestGroup.type])).resolves.toStrictEqual([grpGrpcReq, grp]);
|
|
await expect(
|
|
db.withAncestors(grpGrpcReq, [models.requestGroup.type, models.workspace.type]),
|
|
).resolves.toStrictEqual([grpGrpcReq, grp, wrk]);
|
|
// Group child searching for ancestors but excluding groups will not find the workspace
|
|
await expect(db.withAncestors(grpGrpcReq, [models.workspace.type])).resolves.toStrictEqual([grpGrpcReq]);
|
|
});
|
|
});
|
|
|
|
describe('getWithDescendants()', () => {
|
|
it('should return specified model and all children', async () => {
|
|
const project = await services.project.create();
|
|
const workspace = await services.workspace.create({
|
|
_id: 'w1',
|
|
parentId: project._id,
|
|
});
|
|
const cookieJar1 = await services.cookieJar.create({
|
|
_id: 'j1',
|
|
parentId: workspace._id,
|
|
cookies: [
|
|
{
|
|
id: '1',
|
|
key: 'foo',
|
|
value: '1',
|
|
},
|
|
{
|
|
id: 'j1_1',
|
|
key: 'j1',
|
|
value: '1',
|
|
},
|
|
],
|
|
});
|
|
const cookieJar2 = await services.cookieJar.create({
|
|
_id: 'j2',
|
|
parentId: workspace._id,
|
|
cookies: [
|
|
{
|
|
id: '1',
|
|
key: 'foo',
|
|
value: '2',
|
|
},
|
|
{
|
|
id: 'j2_1',
|
|
key: 'j2',
|
|
value: '2',
|
|
},
|
|
],
|
|
});
|
|
const folder1 = await services.requestGroup.create({
|
|
_id: 'grp1',
|
|
parentId: workspace._id,
|
|
});
|
|
const folder2 = await services.requestGroup.create({
|
|
_id: 'grp2',
|
|
parentId: folder1._id,
|
|
});
|
|
const request1 = await services.request.create({
|
|
_id: 'req1',
|
|
parentId: workspace._id,
|
|
});
|
|
const request2 = await services.request.create({
|
|
_id: 'req2',
|
|
parentId: folder1._id,
|
|
});
|
|
const grpcRequest1 = await services.grpcRequest.create({
|
|
_id: 'grpc1',
|
|
parentId: workspace._id,
|
|
});
|
|
const websocketRequest1 = await services.webSocketRequest.create({
|
|
_id: 'ws1',
|
|
parentId: workspace._id,
|
|
});
|
|
const socketIORequest1 = await services.socketIORequest.create({
|
|
_id: 'socket1',
|
|
parentId: workspace._id,
|
|
});
|
|
|
|
const environment1 = await services.environment.create({
|
|
_id: 'env1',
|
|
parentId: workspace._id,
|
|
});
|
|
|
|
const environment2 = await services.environment.create({
|
|
_id: 'env2',
|
|
parentId: environment1._id,
|
|
});
|
|
|
|
assert.sameDeepMembers(
|
|
await db.getWithDescendants(workspace),
|
|
[
|
|
workspace,
|
|
folder1,
|
|
folder2,
|
|
request1,
|
|
request2,
|
|
grpcRequest1,
|
|
websocketRequest1,
|
|
socketIORequest1,
|
|
cookieJar1,
|
|
cookieJar2,
|
|
environment1,
|
|
environment2,
|
|
],
|
|
'Should return workspace with all descendants',
|
|
);
|
|
|
|
assert.sameDeepMembers(
|
|
await db.getWithDescendants(workspace, [models.requestGroup.type]),
|
|
[workspace, folder1, folder2],
|
|
'Should return workspace with all request groups',
|
|
);
|
|
|
|
assert.sameDeepMembers(
|
|
await db.getWithDescendants(workspace, [
|
|
models.request.type,
|
|
models.grpcRequest.type,
|
|
models.webSocketRequest.type,
|
|
models.socketIORequest.type,
|
|
]),
|
|
[workspace, folder1, request1, folder2, request2, grpcRequest1, websocketRequest1, socketIORequest1],
|
|
'Should return workspace with all request groups and requests',
|
|
);
|
|
});
|
|
});
|