b7f52be4c9
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
303 lines
8.2 KiB
TypeScript
303 lines
8.2 KiB
TypeScript
import { platform } from 'os';
|
|
import { sep } from 'path';
|
|
|
|
import { setupWebSocketListener, submitMessage } from '../../support/testUtils';
|
|
|
|
// Constants
|
|
const SELECTORS = {
|
|
EMAIL_INPUT: '#email',
|
|
PASSWORD_INPUT: '#password',
|
|
AI_MESSAGE: "[data-step-type='assistant_message']",
|
|
CHAT_SUBMIT: '#chat-submit',
|
|
POSITIVE_FEEDBACK: '.positive-feedback-off',
|
|
NEGATIVE_FEEDBACK: '.negative-feedback-off',
|
|
SUBMIT_FEEDBACK: '#submit-feedback',
|
|
POSITIVE_FEEDBACK_ACTIVE: '.positive-feedback-on',
|
|
STEP: '.step',
|
|
THREAD_HISTORY: '#thread-history',
|
|
THREAD_TEST1: '#thread-test1',
|
|
THREAD_TEST2: '#thread-test2',
|
|
THREAD_OPTIONS: '#thread-options',
|
|
DELETE_THREAD: '#delete-thread',
|
|
CONFIRM_BUTTON: "[role='alertdialog'] button.bg-primary",
|
|
NEW_CHAT_BUTTON: '#new-chat-button',
|
|
CONFIRM_NEW: '#confirm',
|
|
LOADER: '.lucide-loader'
|
|
} as const;
|
|
|
|
// Utility functions
|
|
|
|
const login = (username: string = 'user1', password: string = 'user1') => {
|
|
cy.step('Verify login');
|
|
|
|
cy.location('pathname').should('eq', '/login');
|
|
|
|
cy.get(SELECTORS.EMAIL_INPUT).should('be.visible').type(username);
|
|
cy.get(SELECTORS.PASSWORD_INPUT)
|
|
.should('be.visible')
|
|
.type(`${password}{enter}`);
|
|
};
|
|
|
|
const startConversation = () => {
|
|
cy.step('Start conversation');
|
|
|
|
cy.location('pathname').should('eq', '/');
|
|
|
|
cy.get(SELECTORS.AI_MESSAGE)
|
|
.should('exist')
|
|
.and('be.visible')
|
|
.and('have.length', 2);
|
|
|
|
submitMessage('Hello');
|
|
|
|
cy.location('pathname').should('match', /^\/thread\//);
|
|
|
|
cy.get(SELECTORS.AI_MESSAGE).should('exist').and('be.visible');
|
|
};
|
|
|
|
const verifyFeedback = () => {
|
|
cy.step('Verify feedback');
|
|
|
|
cy.get(SELECTORS.NEGATIVE_FEEDBACK).should('have.length', 1);
|
|
cy.get(SELECTORS.POSITIVE_FEEDBACK).should('have.length', 1).first().click();
|
|
cy.get(SELECTORS.SUBMIT_FEEDBACK).should('be.visible').click();
|
|
cy.get(SELECTORS.POSITIVE_FEEDBACK_ACTIVE).should('have.length', 1);
|
|
};
|
|
|
|
const verifyThreadQueue = () => {
|
|
cy.step('Verify thread queue');
|
|
|
|
cy.get(SELECTORS.STEP).eq(1).should('contain.text', 'Create step counter: 0');
|
|
cy.get(SELECTORS.STEP).eq(3).should('contain.text', 'Create step counter: 5');
|
|
cy.get(SELECTORS.STEP).eq(6).should('contain.text', 'Create step counter: 8');
|
|
};
|
|
|
|
const verifyThreadList = () => {
|
|
cy.step('Verify thread list');
|
|
|
|
cy.get(SELECTORS.THREAD_TEST1).should('contain.text', 'thread 1');
|
|
cy.get(SELECTORS.THREAD_TEST2).should('contain.text', 'thread 2');
|
|
|
|
cy.step('Verify thread page');
|
|
|
|
cy.get(SELECTORS.THREAD_TEST1).click();
|
|
cy.get(SELECTORS.STEP).should('have.length', 2);
|
|
cy.get(SELECTORS.STEP).eq(0).should('contain.text', 'Message 1');
|
|
cy.get(SELECTORS.STEP).eq(1).should('contain.text', 'Message 2');
|
|
|
|
cy.step('Verify thread deletion');
|
|
|
|
cy.get(SELECTORS.THREAD_TEST1).find(SELECTORS.THREAD_OPTIONS).click();
|
|
cy.get(SELECTORS.DELETE_THREAD).should('be.visible').click();
|
|
cy.get(SELECTORS.CONFIRM_BUTTON).should('be.visible').click();
|
|
cy.get(SELECTORS.THREAD_TEST1).should('not.exist');
|
|
cy.get('body').type('{esc}');
|
|
};
|
|
|
|
const verifyThreadResume = () => {
|
|
cy.step('Verify thread resume');
|
|
|
|
cy.get('body').should('have.css', 'pointer-events', 'auto');
|
|
|
|
cy.get(SELECTORS.THREAD_TEST2).click();
|
|
cy.get(SELECTORS.LOADER).should('not.be.visible');
|
|
|
|
cy.get(SELECTORS.THREAD_HISTORY).contains('Hello').click();
|
|
cy.get(SELECTORS.LOADER).should('not.be.visible');
|
|
|
|
cy.get(SELECTORS.STEP).should('have.length', 10);
|
|
cy.get(SELECTORS.STEP).eq(0).should('contain.text', 'Hello');
|
|
cy.get(SELECTORS.STEP).eq(7).should('contain.text', 'Welcome back to Hello');
|
|
cy.get(SELECTORS.STEP).eq(8).should('contain.text', 'chat_profile');
|
|
};
|
|
|
|
const verifyContinueThread = () => {
|
|
cy.step('Verify thread continuation');
|
|
|
|
cy.get(SELECTORS.STEP).eq(7).should('contain.text', 'Welcome back to Hello');
|
|
submitMessage('Hello after restart');
|
|
|
|
cy.get(SELECTORS.STEP)
|
|
.eq(11)
|
|
.should('contain.text', 'Create step counter: 14');
|
|
cy.get(SELECTORS.STEP)
|
|
.eq(14)
|
|
.should('contain.text', 'Create step counter: 17');
|
|
};
|
|
|
|
const startNewThread = () => {
|
|
cy.step('Start new thread');
|
|
|
|
cy.get(SELECTORS.NEW_CHAT_BUTTON).click();
|
|
cy.get(SELECTORS.CONFIRM_NEW).click();
|
|
};
|
|
|
|
const cleanupThreadHistory = () => {
|
|
const pathItems = Cypress.spec.absolute.split(sep);
|
|
pathItems[pathItems.length - 1] = 'thread_history.pickle';
|
|
const threadHistoryFile = pathItems.join(sep);
|
|
|
|
// Clean up thread history file
|
|
const command =
|
|
platform() === 'win32'
|
|
? `del /f "${threadHistoryFile}"`
|
|
: `rm -f "${threadHistoryFile}"`;
|
|
cy.exec(command, { failOnNonZeroExit: false });
|
|
};
|
|
|
|
describe.skip('Data Layer', () => {
|
|
describe('Data Features with Persistence', () => {
|
|
before(cleanupThreadHistory);
|
|
afterEach(cleanupThreadHistory);
|
|
|
|
it('Verifies login, feedback, thread queue, thread list, and thread resume functionality', () => {
|
|
login();
|
|
startConversation();
|
|
|
|
verifyFeedback();
|
|
verifyThreadQueue();
|
|
|
|
verifyThreadList();
|
|
verifyThreadResume();
|
|
});
|
|
|
|
it('Verifies thread continuation after server restart and new thread creation', () => {
|
|
cy.task('restartChainlit', Cypress.spec).then(() => {
|
|
cy.section('Before server restart');
|
|
|
|
cy.visit('/');
|
|
|
|
login();
|
|
startConversation();
|
|
|
|
verifyFeedback();
|
|
verifyThreadQueue();
|
|
});
|
|
|
|
cy.task('restartChainlit', Cypress.spec).then(() => {
|
|
cy.section('After server restart');
|
|
|
|
verifyContinueThread();
|
|
|
|
startNewThread();
|
|
startConversation();
|
|
|
|
verifyFeedback();
|
|
verifyThreadQueue();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Access Control', () => {
|
|
before(cleanupThreadHistory);
|
|
afterEach(cleanupThreadHistory);
|
|
|
|
it("should not allow steal user's thread", () => {
|
|
login('user1', 'user1');
|
|
startConversation();
|
|
|
|
let stolenThreadId = '';
|
|
cy.location('pathname')
|
|
.should('match', /^\/thread\//)
|
|
.then((pathname) => {
|
|
const parts = pathname.split('/');
|
|
stolenThreadId = parts[2];
|
|
expect(stolenThreadId).to.match(/^[a-zA-Z0-9_-]+$/);
|
|
});
|
|
|
|
cy.clearCookies();
|
|
cy.clearLocalStorage();
|
|
|
|
login('user2', 'user2');
|
|
|
|
cy.intercept(
|
|
{
|
|
method: 'POST',
|
|
url: /\/ws\/socket\.io\/.*transport=polling/
|
|
},
|
|
(request) => {
|
|
if (
|
|
typeof request.body === 'string' &&
|
|
request.body.includes('"threadId"')
|
|
) {
|
|
request.body = request.body.replace(
|
|
/("threadId":\s*")[^"]*(")/,
|
|
`$1${stolenThreadId}$2`
|
|
);
|
|
expect(request.url).to.include('/ws/socket.io/');
|
|
expect(request.body).to.include(`"threadId":"${stolenThreadId}"`);
|
|
}
|
|
}
|
|
);
|
|
startNewThread();
|
|
|
|
cy.get(SELECTORS.STEP).should('have.length', 0);
|
|
});
|
|
|
|
it('should not allow request forgery', () => {
|
|
let elementId: string = null;
|
|
let sessionId: string | null = null;
|
|
|
|
setupWebSocketListener('element', (data) => {
|
|
elementId = data.id;
|
|
});
|
|
|
|
cy.intercept('POST', '/login').as('login');
|
|
|
|
cy.intercept('POST', '/set-session-cookie').as('setSession');
|
|
|
|
login('user1', 'user1');
|
|
|
|
startConversation();
|
|
|
|
let threadId: string = null;
|
|
|
|
cy.location('pathname')
|
|
.should('match', /^\/thread\//)
|
|
.then((pathname) => {
|
|
const parts = pathname.split('/');
|
|
threadId = parts[2];
|
|
expect(threadId).to.match(/^[a-zA-Z0-9_-]+$/);
|
|
});
|
|
|
|
// Wait for session ID capture
|
|
cy.wait('@setSession').then((interception) => {
|
|
sessionId = interception.request.body.session_id;
|
|
});
|
|
|
|
cy.wrap(null).should(() => {
|
|
expect(sessionId).to.not.be.null;
|
|
});
|
|
|
|
cy.then(() => {
|
|
cy.request({
|
|
method: 'PUT',
|
|
url: '/project/element',
|
|
body: {
|
|
element: {
|
|
type: 'custom',
|
|
id: 'test',
|
|
name: 'test',
|
|
display: 'inline',
|
|
url: 'http://example.org/test.txt'
|
|
},
|
|
sessionId: sessionId
|
|
}
|
|
});
|
|
});
|
|
|
|
cy.wrap(null).should(() => {
|
|
expect(elementId).to.exist;
|
|
});
|
|
|
|
cy.then(() => {
|
|
cy.request(`/project/thread/${threadId}/element/${elementId}`).then(
|
|
(response) => {
|
|
expect(response.body.url).to.not.equal('http://example.com/test.txt');
|
|
}
|
|
);
|
|
});
|
|
});
|
|
});
|