Skip to content

Commit eb67dd4

Browse files
committed
added naming of jest tests
1 parent 6566004 commit eb67dd4

File tree

8 files changed

+109
-31
lines changed

8 files changed

+109
-31
lines changed

src/commands/export.js

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ const path = require('path');
33
const {
44
EXPORTED_TESTS_DIR,
55
EXPORTED_TESTS_DATA_DIR,
6+
PYTHAGORA_METADATA_DIR,
67
METADATA_FILENAME,
8+
EXPORT_METADATA_FILENAME,
79
SRC_TO_ROOT,
810
MIN_TOKENS_FOR_GPT_RESPONSE,
911
MAX_GPT_MODEL_TOKENS
1012
} = require('../const/common');
1113
const {getAllGeneratedTests, updateMetadata} = require("../utils/common");
1214
const {convertOldTestForGPT} = require("../utils/legacy");
13-
const {getJestTestFromPythagoraData, getJestAuthFunction, getTokensInMessages, getPromptFromFile} = require("../helpers/openai");
15+
const {getJestTestFromPythagoraData, getJestTestName, getJestAuthFunction, getTokensInMessages, getPromptFromFile} = require("../helpers/openai");
1416
const {setUpPythagoraDirs} = require("../helpers/starting");
1517
const {testExported, pleaseCaptureLoginTestLog, enterLoginRouteLog, testEligibleForExportLog} = require("../utils/cmdPrint");
1618
const _ = require('lodash');
1719
const args = require('../utils/getArgs.js');
20+
const {logAndExit} = require("@pythagora.io/pythagora-dev/src/utils/cmdPrint");
1821

1922
async function createDefaultFiles(generatedTests) {
2023
if (!fs.existsSync('jest.config.js')) {
@@ -73,33 +76,81 @@ function cleanupGPTResponse(gptResponse) {
7376
return gptResponse;
7477
}
7578

76-
async function exportTest(originalTest) {
79+
function cleanupDataFolder() {
80+
const pythagoraTestsFolderPath = `./${EXPORTED_TESTS_DIR}`;
81+
const dataFolderPath = `./${EXPORTED_TESTS_DATA_DIR}`;
82+
83+
try {
84+
// Read the files in the ./pythagora_tests/data folder
85+
const files = fs.readdirSync(dataFolderPath);
86+
87+
files.forEach((file) => {
88+
const filePathInPythagoraTests = path.join(pythagoraTestsFolderPath, file.replace('.json', '.test.js'));
89+
90+
// Check if the file exists in the pythagora_tests folder
91+
if (!fs.existsSync(filePathInPythagoraTests)) {
92+
// File doesn't exist in the pythagora_tests folder, so delete it from the data folder
93+
const filePathInData = path.join(dataFolderPath, file);
94+
fs.unlinkSync(filePathInData);
95+
}
96+
});
97+
} catch (err) {
98+
// console.error('Error deleting files from the data folder:', err);
99+
}
100+
}
101+
102+
async function exportTest(originalTest, usedNames) {
77103
// TODO remove in the future
78104
let test = convertOldTestForGPT(originalTest);
79-
fs.writeFileSync(`./${EXPORTED_TESTS_DATA_DIR}/${test.testId}.json`, JSON.stringify(test.mongoQueries, null, 2));
80105

81106
let gptResponse = await getJestTestFromPythagoraData(test);
82107
let jestTest = cleanupGPTResponse(gptResponse);
83108

84-
fs.writeFileSync(`./${EXPORTED_TESTS_DIR}/${test.testId}.test.js`, jestTest);
109+
let testName = await getJestTestName(jestTest, usedNames);
110+
fs.writeFileSync(`./${EXPORTED_TESTS_DATA_DIR}/${testName.replace('.test.js', '.json')}`, JSON.stringify(test.mongoQueries, null, 2));
111+
fs.writeFileSync(`./${EXPORTED_TESTS_DIR}/${testName}`, jestTest.replace(test.testId, testName));
112+
85113
testExported(test.testId);
114+
return testName;
115+
}
116+
117+
function testExists(exportsMetadata, testId) {
118+
return !!exportsMetadata[testId] && exportsMetadata[testId].testName && fs.existsSync(`./${EXPORTED_TESTS_DIR}/${exportsMetadata[testId].testName}`)
119+
}
120+
121+
function saveExportJson(exportsMetadata, test, testName) {
122+
exportsMetadata[test.id] = {
123+
endpoint: test.endpoint,
124+
testName
125+
};
126+
fs.writeFileSync(`./${PYTHAGORA_METADATA_DIR}/${EXPORT_METADATA_FILENAME}`, JSON.stringify(exportsMetadata));
86127
}
87128

88129
(async () => {
89130
setUpPythagoraDirs();
131+
cleanupDataFolder();
132+
let exportsMetadata = JSON.parse(fs.readFileSync(`./${PYTHAGORA_METADATA_DIR}/${EXPORT_METADATA_FILENAME}`));
90133
let generatedTests = getAllGeneratedTests();
91134
await createDefaultFiles(generatedTests);
92135

93136
let testId = args.test_id;
94137
if (testId) {
138+
if (testExists(exportsMetadata, testId)) logAndExit(`Test with id ${testId} already generated, you can find it here: ${`./${EXPORTED_TESTS_DIR}/${exportsMetadata[testId].testName}`}. If you want to generate it again delete old one first.`);
139+
95140
let test = generatedTests.find(t => t.id === testId);
96141
if (!test) throw new Error(`Test with id ${testId} not found`);
97-
await exportTest(test)
142+
143+
let testName = await exportTest(test, Object.values(exportsMetadata).map(obj => obj.testName));
144+
saveExportJson(exportsMetadata, test, testName);
98145
}
99146
else {
100147
for (let test of generatedTests) {
101148
if (test.method === 'OPTIONS') continue;
102-
if (fs.existsSync(`./${EXPORTED_TESTS_DIR}/${test.id}.test.js`)) continue;
149+
if (testExists(exportsMetadata, test.id)) {
150+
console.log(`Test with id ${testId} already generated, you can find it here: ${`./${EXPORTED_TESTS_DIR}/${exportsMetadata[testId].testName}`}.`);
151+
continue;
152+
}
153+
103154
let testData = convertOldTestForGPT(test);
104155
let tokens = getTokensInMessages([
105156
{"role": "system", "content": "You are a QA engineer and your main goal is to find ways to break the application you're testing. You are proficient in writing automated integration tests for Node.js API servers.\n" +
@@ -112,9 +163,12 @@ async function exportTest(originalTest) {
112163

113164
let isEligibleForExport = (tokens + MIN_TOKENS_FOR_GPT_RESPONSE < MAX_GPT_MODEL_TOKENS);
114165

115-
if (isEligibleForExport) await exportTest(test)
116-
else testEligibleForExportLog(test.endpoint, test.id, isEligibleForExport);
166+
if (isEligibleForExport) {
167+
let testName = await exportTest(test, Object.values(exportsMetadata).map(obj => obj.testName));
168+
saveExportJson(exportsMetadata, test, testName);
169+
} else testEligibleForExportLog(test.endpoint, test.id, isEligibleForExport);
117170
}
118171
}
172+
119173
process.exit(0);
120174
})()

src/const/common.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module.exports = {
33
PYTHAGORA_METADATA_DIR: '.pythagora',
44
METADATA_FILENAME: 'metadata.json',
55
REVIEW_DATA_FILENAME: 'review.json',
6+
EXPORT_METADATA_FILENAME: 'export.json',
67
PYTHAGORA_ASYNC_STORE: 42069420,
78
PYTHAGORA_DELIMITER: '-_-',
89
EXPORTED_TESTS_DIR: 'pythagora_tests/exported_tests',

src/helpers/jestMethods.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ async function prepareDB(mongoQueries) {
2020
}
2121
}
2222

23-
async function setUpDb(testId) {
23+
async function setUpDb(testName) {
2424
await cleanupDb(global.Pythagora);
2525
// TODO organize better by test groups
26-
let data = require(`../${SRC_TO_ROOT}${EXPORTED_TESTS_DATA_DIR}/${testId}.json`);
26+
let data = require(`../${SRC_TO_ROOT}${EXPORTED_TESTS_DATA_DIR}/${testName.replace('.test.js', '.json')}`);
2727
await prepareDB(data);
28-
console.log(`MongoDB prepared for test ${testId}`);
28+
console.log(`MongoDB prepared for test ${testName}`);
2929
let preparedData = _.groupBy(data, 'collection');
3030
preparedData = _.mapValues(preparedData, docs => {
3131
return docs.map(doc => doc.preQueryDocs).flat();

src/helpers/openai.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ function getTokensInMessages(messages) {
2929
return encodedPrompt.text.length;
3030
}
3131

32-
async function createGPTChatCompletion(messages) {
32+
async function createGPTChatCompletion(messages, minTokens = MIN_TOKENS_FOR_GPT_RESPONSE, noStream = args.no_stream) {
3333
let tokensInMessages = getTokensInMessages(messages);
34-
if (tokensInMessages + MIN_TOKENS_FOR_GPT_RESPONSE > MAX_GPT_MODEL_TOKENS) {
34+
if (tokensInMessages + minTokens > MAX_GPT_MODEL_TOKENS) {
3535
console.error(`Too many tokens in messages: ${tokensInMessages}. Please try a different test.`);
3636
process.exit(1);
3737
}
@@ -44,7 +44,7 @@ async function createGPTChatCompletion(messages) {
4444
};
4545

4646
try {
47-
if (args.no_stream) {
47+
if (noStream) {
4848
let result = await openai.createChatCompletion(gptData);
4949
return result.data.choices[0].message.content;
5050
} else {
@@ -69,6 +69,18 @@ async function getJestTestFromPythagoraData(reqData) {
6969
]);
7070
}
7171

72+
async function getJestTestName(test, usedNames) {
73+
await getOpenAIClient();
74+
return await createGPTChatCompletion([
75+
{"role": "system", "content": "You are a QA engineer and your main goal is to think of good, human readable jest tests file names. You are proficient in writing automated integration tests for Node.js API servers.\n" +
76+
"When you respond, you don't say anything except the filename - no formatting, no explanation, no code - only filename.\n" },
77+
{
78+
"role": "user",
79+
"content": getPromptFromFile('generateJestTestName.txt', { test, usedNames }),
80+
},
81+
], 200, true);
82+
}
83+
7284
async function getJestAuthFunction(loginMongoQueriesArray, loginRequestBody, loginEndpointPath) {
7385
jestAuthFileGenerationLog();
7486
await getOpenAIClient();
@@ -152,6 +164,7 @@ function extractGPTMessageFromStreamData(input) {
152164

153165
module.exports = {
154166
getJestTestFromPythagoraData,
167+
getJestTestName,
155168
getJestAuthFunction,
156169
getTokensInMessages,
157170
getPromptFromFile

src/helpers/starting.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ const {
66
EXPORTED_TESTS_DIR,
77
EXPORTED_TESTS_DATA_DIR,
88
PYTHAGORA_METADATA_DIR,
9-
METADATA_FILENAME
9+
METADATA_FILENAME,
10+
EXPORT_METADATA_FILENAME
1011
} = require("../const/common");
1112

1213

@@ -92,6 +93,7 @@ function setUpPythagoraDirs() {
9293
if (!fs.existsSync(`./${EXPORTED_TESTS_DATA_DIR}`)) fs.mkdirSync(`./${EXPORTED_TESTS_DATA_DIR}`);
9394
if (!fs.existsSync(`./${PYTHAGORA_METADATA_DIR}/`)) fs.mkdirSync(`./${PYTHAGORA_METADATA_DIR}/`);
9495
if (!fs.existsSync(`./${PYTHAGORA_METADATA_DIR}/${METADATA_FILENAME}`)) fs.writeFileSync(`./${PYTHAGORA_METADATA_DIR}/${METADATA_FILENAME}`, '{}');
96+
if (!fs.existsSync(`./${PYTHAGORA_METADATA_DIR}/${EXPORT_METADATA_FILENAME}`)) fs.writeFileSync(`./${PYTHAGORA_METADATA_DIR}/${EXPORT_METADATA_FILENAME}`, '{}');
9597
}
9698

9799
module.exports = {

src/prompts/generateJestAuth.txt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,41 +55,41 @@ async function getAuthToken(appUrl, userDoc) {
5555
let response;
5656
try {
5757
const User = getMongoCollection(<USER_COLLECTION>);
58-
<TAKE_DEFAULT_USER_OBJ_IF_USERDOC_UNDEFINED>
58+
<SET_DEFAULT_USER>
59+
<REPLACE_USERDOC_PASSWORD_WITH_DEFAULTUSEROBJ_PASSWORD>
60+
if (!userDoc) userDoc = defaultUserObj;
5961

6062
const existingUser = await User.findOne({ <FIELD_TO_FIND_USER> });
6163
if (!existingUser) {
62-
await User.insertOne(userObj);
64+
await User.insertOne(userDoc);
65+
} else {
66+
await User.updateOne({email: userDoc.email}, {$set: {<SET_PASSWORD_TO_USERDOC_PASSWORD>}});
6367
}
6468

65-
<REPLACE_PASSWORD_WITH_HARDCODED_ONE>
66-
6769
response = await axios.post(appUrl + <LOGIN_ENDPOINT_PATH>, <LOGIN_BODY>)
6870
.catch(error => {
6971
return {Authorization: 'Bearer invalid-token'};
7072
});
7173

7274
const token = response.data.token || response.headers['authorization'];
7375

74-
return token ? { 'Authorization': 'Bearer ' + token, 'x-auth-token': token } : { 'Cookie': 'token=' + cookie.parse(response.headers['set-cookie'][0]).token }
76+
return token ? { 'Authorization': 'Bearer ' + token.replace('Bearer ', ''), 'x-auth-token': token } : { 'Cookie': 'token=' + cookie.parse(response.headers['set-cookie'][0]).token }
7577
} catch (err) {
7678
return {};
7779
}
7880
}
7981
```
82+
In section <REPLACE_USERDOC_PASSWORD_WITH_DEFAULTUSEROBJ_PASSWORD> you should replace password from userDoc with defaultUser password so that login can always pass. Here is example, that should work in most cases, how to do that. Modify this example only if password is in other object key eg. userDoc.pass
83+
```javascript
84+
if (userDoc) userDoc.password = defaultUserObj.password;
85+
```
86+
87+
<LOGIN_BODY> should always have same password as one I provided you as data that's sent in the request body during the login.
8088

8189
Input variables are:
8290
1. appUrl - the URL of the app
8391
2. userDoc - mongo document of the user that needs to be logged in for a specific test. Each test can require a different user to log in for the test to be successful. getAuthToken function needs to take that document and, before it inserts the document into the database, replace the password field with the password value captured in the data above so that we're always able to log the user in with the same password. If `userDoc` is undefined or invalid, you should use default userDoc which was provided above as `Mongo queries made during the execution of the login endpoint` you just have to find which is user document.
8492

85-
Don't modify userObj before this:
86-
```javascript
87-
if (!existingUser) {
88-
await User.insertOne(userObj);
89-
}
90-
```
91-
Document `userObj` should always be inserted to database with original values, especially any type of password or hash. After you checked for `existingUser` and inserted it to database, then you can modify it if needed.
92-
9393
When replacing <LOGIN_BODY> do not hardcode anything but use `userDoc`.
9494

9595
Don't require `global` because that is the native NodeJS global variable.

src/prompts/generateJestTest.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Here are the details about the parameters of objects inside `mongoQueries` array
2424

2525
-----------------------------------------------------------------------------------------------------------------------
2626

27-
`preQueryDocs` and `postQueryDocs` are important so that you can test if an update in the database happened correctly so make sure to include in the test a check to see if the database was correctly updated. Also, since Mongo's `ObjectId` is not a valid JSON value, you might see a string like this - "ObjectId(\"642548c3dabd5517e39b5827\")". Keep in mind that this should be converted to `new ObjectId("642548c3dabd5517e39b5827")` and if you need to initiate ObjectId, you can require it from "mongodb" - like this:
27+
`preQueryDocs` and `postQueryDocs` are important so that you can test if an update in the database happened correctly so make sure to include in the test a check to see if the database was correctly updated. Also, since Mongo's `ObjectId` is not a valid JSON value, sometimes you might see a string like this - "ObjectId(\"642548c3dabd5517e39b5827\")". Keep in mind that this should be converted to `new ObjectId("642548c3dabd5517e39b5827")` and if you need to initiate ObjectId, you can require it from "mongodb" - like this:
2828
```javascript
2929
const { ObjectId } = require("mongodb");
3030
let someObjectId = new ObjectId(id);
@@ -36,7 +36,7 @@ Don't require `global` because that is the native NodeJS global variable.
3636

3737
If the database was updated, check if the database was updated correctly. When you need to make a query to a Mongo collection, use `global.getMongoCollection(collection)` function which will return the MongoDB collection which you can query how you want. For example, if you want to do a `find` query on a collection "users", you can do `global.getMongoCollection('users').find(query)`.
3838

39-
If you need to use the Mongo database for anything, add `let mongoDocuments = await global.setUpDb(testId);` to the beginning of the `beforeAll` function. You don't need to insert any documents in the database because all needed documents will be inserted in the `setUpDb` function. You can find the `testId` in the JSON request data that I will give you.
39+
If you need to use the Mongo database for anything, add `let mongoDocuments = await global.setUpDb(testId);` to the beginning of the `beforeAll` function. You don't need to insert any documents in the database because all needed documents will be inserted in the `setUpDb` function. You can find the `testId` in the JSON request data that I will give you. Response from `await global.setUpDb(testId)` will be mongo document so if you use it you don't have to convert ObjectId as mentioned above.
4040
The returned `mongoDocuments` variable is a JSON object that has a collection name as a key and an array of all documents in that collection that are present in the database as a value. The format of `mongoDocuments` object is this:
4141
```json
4242
{
@@ -120,7 +120,7 @@ beforeAll(async () => {
120120
```
121121
If you need to set some headers for test to be successful, set only necessary headers.
122122

123-
Response from `getAuthToken()` has to be stored in `authToken` variable and shouldn't be modified. This is the only correct way of adding token or cookie to header:
123+
If authentication is needed, response from `getAuthToken()` has to be stored in `authToken` variable and shouldn't be modified. In that case, you shouldn't do anything else with `authToken` and you have to add it to headers only like this:
124124
```javascript
125125
const response = await axios.get(<URL>, {headers:Object.assign(headers, authToken)})
126126
```

src/prompts/generateJestTestName.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
I will give you whole file that contains one or more Jest tests. I want you to think of good, meaningfull and human readable filename to store that test.
2+
3+
Filename you create should always end with `.test.js` as extension since this is jest test.
4+
5+
Here is the file:
6+
```json
7+
{{test}}
8+
```

0 commit comments

Comments
 (0)