bus/static/tests/helpers/mock_python_environment.js

343 lines
12 KiB
JavaScript
Raw Normal View History

2024-05-03 15:03:07 +03:00
/** @odoo-module **/
import { TEST_USER_IDS } from "@bus/../tests/helpers/test_constants";
import { registry } from "@web/core/registry";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { makeMockServer } from "@web/../tests/helpers/mock_server";
import { serializeDateTime, serializeDate } from "@web/core/l10n/dates";
const { DateTime } = luxon;
const modelDefinitionsPromise = new Promise((resolve) => {
QUnit.begin(() => resolve(getModelDefinitions()));
});
/**
* Fetch model definitions from the server then insert fields present in the
* `bus.model.definitions` registry. Use `addModelNamesToFetch`/`insertModelFields`
* helpers in order to add models to be fetched, default values to the fields,
* fields to a model definition.
*
* @return {Map<string, Object>} A map from model names to model fields definitions.
* @see model_definitions_setup.js
*/
async function getModelDefinitions() {
const modelDefinitionsRegistry = registry.category("bus.model.definitions");
const modelNamesToFetch = modelDefinitionsRegistry.get("modelNamesToFetch");
const fieldsToInsertRegistry = modelDefinitionsRegistry.category("fieldsToInsert");
// fetch the model definitions.
const formData = new FormData();
formData.append("csrf_token", odoo.csrf_token);
formData.append("model_names_to_fetch", JSON.stringify(modelNamesToFetch));
const response = await window.fetch("/bus/get_model_definitions", {
body: formData,
method: "POST",
});
if (response.status !== 200) {
throw new Error("Error while fetching required models");
}
const modelDefinitions = new Map(Object.entries(await response.json()));
for (const [modelName, fields] of modelDefinitions) {
// insert fields present in the fieldsToInsert registry : if the field
// exists, update its default value according to the one in the
// registry; If it does not exist, add it to the model definition.
const fieldNamesToFieldToInsert = fieldsToInsertRegistry.category(modelName).getEntries();
for (const [fname, fieldToInsert] of fieldNamesToFieldToInsert) {
if (fname in fields) {
fields[fname].default = fieldToInsert.default;
} else {
fields[fname] = fieldToInsert;
}
}
// apply default values for date like fields if none was passed.
for (const fname in fields) {
const field = fields[fname];
if (["date", "datetime"].includes(field.type) && !field.default) {
const defaultFieldValue =
field.type === "date"
? () => serializeDate(DateTime.utc())
: () => serializeDateTime(DateTime.utc());
field.default = defaultFieldValue;
} else if (fname === "active" && !("default" in field)) {
// records are active by default.
field.default = true;
}
}
}
// add models present in the fake models registry to the model definitions.
const fakeModels = modelDefinitionsRegistry.category("fakeModels").getEntries();
for (const [modelName, fields] of fakeModels) {
modelDefinitions.set(modelName, fields);
}
return modelDefinitions;
}
let _cookie = {};
export const pyEnvTarget = {
cookie: {
get(key) {
return _cookie[key];
},
set(key, value) {
_cookie[key] = value;
},
delete(key) {
delete _cookie[key];
},
},
_authenticate(user) {
if (!user) {
throw new Error("Unauthorized");
}
this.cookie.set("sid", user.id);
},
/**
* Authenticate a user on the mock server given its login
* and password.
*
* @param {string} login
* @param {string|} password
*/
authenticate(login, password) {
const user = this.mockServer.getRecords(
"res.users",
[
["login", "=", login],
["password", "=", password],
],
{ active_test: false }
)[0];
this._authenticate(user);
this.cookie.set("authenticated_user_sid", this.cookie.get("sid"));
},
/**
* Logout the current user.
*/
logout() {
if (this.cookie.get("authenticated_user_sid") === this.cookie.get("sid")) {
this.cookie.delete("authenticated_user_sid");
}
this.cookie.delete("sid");
const [publicUser] = this.mockServer.getRecords(
"res.users",
[["id", "=", this.publicUserId]],
{ active_test: false }
);
this.authenticate(publicUser.login, publicUser.password);
},
/**
* Execute the provided function with the given user
* authenticated then restore the original user.
*
* @param {number} userId
* @param {Function} fn
*/
async withUser(userId, fn) {
const user = this.currentUser;
const targetUser = this.mockServer.getRecords("res.users", [["id", "=", userId]], {
active_test: false,
})[0];
this._authenticate(targetUser);
let result;
try {
result = await fn();
} finally {
if (user) {
this._authenticate(user);
} else {
this.logout();
}
}
return result;
},
/**
* The current user, either the one authenticated or the one
* impersonated by `withUser`.
*/
get currentUser() {
let user;
const currentUserId = this.cookie.get("sid");
if ("res.users" in this.mockServer.models && currentUserId) {
user = this.mockServer.getRecords("res.users", [["id", "=", currentUserId]], {
active_test: false,
})[0];
user = user ? { ...user, _is_public: () => user.id === this.publicUserId } : undefined;
}
return user;
},
/**
* The current partner, either the one of the current user or
* the one of the user impersonated by `withUser`.
*/
get currentPartner() {
if ("res.partner" in this.mockServer.models && this.currentUser?.partner_id) {
return this.mockServer.getRecords(
"res.partner",
[["id", "=", this.currentUser?.partner_id]],
{ active_test: false }
)[0];
}
return undefined;
},
get currentUserId() {
return this.currentUser?.id;
},
get currentPartnerId() {
return this.currentPartner?.id;
},
getData() {
return this.mockServer.models;
},
getViews() {
return this.mockServer.archs;
},
simulateConnectionLost(closeCode) {
this.mockServer._simulateConnectionLost(closeCode);
},
...TEST_USER_IDS,
};
let pyEnv;
/**
* Creates an environment that can be used to setup test data as well as
* creating data after test start.
*
* @param {Object} serverData serverData to pass to the mockServer.
* @param {Object} [serverData.action] actions to be passed to the mock
* server.
* @param {Object} [serverData.views] views to be passed to the mock
* server.
* @returns {Object} An environment that can be used to interact with
* the mock server (creation, deletion, update of records...)
*/
export async function startServer({ actions, views = {} } = {}) {
const models = {};
const modelDefinitions = await modelDefinitionsPromise;
const recordsToInsertRegistry = registry
.category("bus.model.definitions")
.category("recordsToInsert");
for (const [modelName, fields] of modelDefinitions) {
const records = [];
if (recordsToInsertRegistry.contains(modelName)) {
// prevent tests from mutating the records.
records.push(...JSON.parse(JSON.stringify(recordsToInsertRegistry.get(modelName))));
}
models[modelName] = { fields: { ...fields }, records };
// generate default views for this model if none were passed.
const viewArchsSubRegistries = registry.category("bus.view.archs").subRegistries;
for (const [viewType, archsRegistry] of Object.entries(viewArchsSubRegistries)) {
views[`${modelName},false,${viewType}`] =
views[`${modelName},false,${viewType}`] ||
archsRegistry.get(modelName, archsRegistry.get("default"));
}
}
pyEnv = new Proxy(pyEnvTarget, {
get(target, name) {
if (name in target) {
return target[name];
}
const modelAPI = {
/**
* Simulate a 'create' operation on a model.
*
* @param {Object[]|Object} values records to be created.
* @returns {integer[]|integer} array of ids if more than one value was passed,
* id of created record otherwise.
*/
create(values) {
if (!values) {
return;
}
if (!Array.isArray(values)) {
values = [values];
}
const recordIds = values.map((value) =>
target.mockServer.mockCreate(name, value)
);
return recordIds.length === 1 ? recordIds[0] : recordIds;
},
/**
* Simulate a 'search' operation on a model.
*
* @param {Array} domain
* @param {Object} context
* @returns {integer[]} array of ids corresponding to the given domain.
*/
search(domain, context = {}) {
return target.mockServer.mockSearch(name, [domain], context);
},
/**
* Simulate a `search_count` operation on a model.
*
* @param {Array} domain
* @return {number} count of records matching the given domain.
*/
searchCount(domain) {
return this.search(domain).length;
},
/**
* Simulate a 'search_read' operation on a model.
*
* @param {Array} domain
* @param {Object} kwargs
* @returns {Object[]} array of records corresponding to the given domain.
*/
searchRead(domain, kwargs = {}) {
return target.mockServer.mockSearchRead(name, [domain], kwargs);
},
/**
* Simulate an 'unlink' operation on a model.
*
* @param {integer[]} ids
* @returns {boolean} mockServer 'unlink' method always returns true.
*/
unlink(ids) {
return target.mockServer.mockUnlink(name, [ids]);
},
/**
* Simulate a 'write' operation on a model.
*
* @param {integer[]} ids ids of records to write on.
* @param {Object} values values to write on the records matching given ids.
* @returns {boolean} mockServer 'write' method always returns true.
*/
write(ids, values) {
return target.mockServer.mockWrite(name, [ids, values]);
},
};
if (name === "bus.bus") {
modelAPI["_sendone"] = target.mockServer._mockBusBus__sendone.bind(
target.mockServer
);
modelAPI["_sendmany"] = target.mockServer._mockBusBus__sendmany.bind(
target.mockServer
);
}
return modelAPI;
},
});
pyEnv["mockServer"] = await makeMockServer({ actions, models, views });
pyEnv["mockServer"].pyEnv = pyEnv;
registerCleanup(() => {
pyEnv = undefined;
_cookie = {};
});
if ("res.users" in pyEnv.mockServer.models) {
const adminUser = pyEnv["res.users"].searchRead([["id", "=", pyEnv.adminUserId]])[0];
pyEnv.authenticate(adminUser.login, adminUser.password);
}
return pyEnv;
}
/**
*
* @returns {Object} An environment that can be used to interact with the mock
* server (creation, deletion, update of records...)
*/
export function getPyEnv() {
return pyEnv || startServer();
}