Files
coder/site/src/api/api.test.ts
T

466 lines
13 KiB
TypeScript

import {
MockStoppedWorkspace,
MockTemplate,
MockTemplateVersion2,
MockTemplateVersionParameter1,
MockTemplateVersionParameter2,
MockWorkspace,
MockWorkspaceBuild,
MockWorkspaceBuildParameter1,
} from "#/testHelpers/entities";
import { API, getURLWithSearchParams, MissingBuildParameters } from "./api";
import type * as TypesGen from "./typesGenerated";
const axiosInstance = API.getAxiosInstance();
describe("api.ts", () => {
describe("login", () => {
it("should return LoginResponse", async () => {
// given
const loginResponse: TypesGen.LoginWithPasswordResponse = {
session_token: "abc_123_test",
};
vi.spyOn(axiosInstance, "post").mockResolvedValueOnce({
data: loginResponse,
});
// when
const result = await API.login("test", "123");
// then
expect(axiosInstance.post).toHaveBeenCalled();
expect(result).toStrictEqual(loginResponse);
});
it("should throw an error on 401", async () => {
// given
// ..ensure that we await our expect assertion in async/await test
expect.assertions(1);
const expectedError = {
message: "Validation failed",
errors: [{ field: "email", code: "email" }],
};
const axiosMockPost = vi.fn().mockImplementationOnce(() => {
return Promise.reject(expectedError);
});
axiosInstance.post = axiosMockPost;
try {
await API.login("test", "123");
} catch (error) {
expect(error).toStrictEqual(expectedError);
}
});
});
describe("logout", () => {
it("should return without erroring", async () => {
// given
const axiosMockPost = vi.fn().mockImplementationOnce(() => {
return Promise.resolve();
});
axiosInstance.post = axiosMockPost;
// when
await API.logout();
// then
expect(axiosMockPost).toHaveBeenCalled();
});
it("should throw an error on 500", async () => {
// given
// ..ensure that we await our expect assertion in async/await test
expect.assertions(1);
const expectedError = {
message: "Failed to logout.",
};
const axiosMockPost = vi.fn().mockImplementationOnce(() => {
return Promise.reject(expectedError);
});
axiosInstance.post = axiosMockPost;
try {
await API.logout();
} catch (error) {
expect(error).toStrictEqual(expectedError);
}
});
});
describe("getApiKey", () => {
it("should return APIKeyResponse", async () => {
// given
const apiKeyResponse: TypesGen.GenerateAPIKeyResponse = {
key: "abc_123_test",
};
const axiosMockPost = vi.fn().mockImplementationOnce(() => {
return Promise.resolve({ data: apiKeyResponse });
});
axiosInstance.post = axiosMockPost;
// when
const result = await API.getApiKey();
// then
expect(axiosMockPost).toHaveBeenCalled();
expect(result).toStrictEqual(apiKeyResponse);
});
it("should throw an error on 401", async () => {
// given
// ..ensure that we await our expect assertion in async/await test
expect.assertions(1);
const expectedError = {
message: "No Cookie!",
};
const axiosMockPost = vi.fn().mockImplementationOnce(() => {
return Promise.reject(expectedError);
});
axiosInstance.post = axiosMockPost;
try {
await API.getApiKey();
} catch (error) {
expect(error).toStrictEqual(expectedError);
}
});
});
describe("getURLWithSearchParams - workspaces", () => {
it.each<[string, TypesGen.WorkspaceFilter | undefined, string]>([
["/api/v2/workspaces", undefined, "/api/v2/workspaces"],
["/api/v2/workspaces", { q: "" }, "/api/v2/workspaces"],
[
"/api/v2/workspaces",
{ q: "owner:1" },
"/api/v2/workspaces?q=owner%3A1",
],
[
"/api/v2/workspaces",
{ q: "owner:me" },
"/api/v2/workspaces?q=owner%3Ame",
],
])("Workspaces - getURLWithSearchParams(%p, %p) returns %p", (basePath, filter, expected) => {
expect(getURLWithSearchParams(basePath, filter)).toBe(expected);
});
});
describe("getURLWithSearchParams - users", () => {
it.each<[string, TypesGen.UsersRequest | undefined, string]>([
["/api/v2/users", undefined, "/api/v2/users"],
[
"/api/v2/users",
{ q: "status:active" },
"/api/v2/users?q=status%3Aactive",
],
["/api/v2/users", { q: "" }, "/api/v2/users"],
])("Users - getURLWithSearchParams(%p, %p) returns %p", (basePath, filter, expected) => {
expect(getURLWithSearchParams(basePath, filter)).toBe(expected);
});
});
describe("update", () => {
describe("given a running workspace", () => {
it("stops with current version before starting with the latest version", async () => {
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
transition: "stop",
});
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
template_version_id: MockTemplateVersion2.id,
transition: "start",
});
vi.spyOn(API, "getTemplate").mockResolvedValueOnce({
...MockTemplate,
active_version_id: MockTemplateVersion2.id,
});
await API.updateWorkspace(MockWorkspace);
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
transition: "stop",
log_level: undefined,
});
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
transition: "start",
template_version_id: MockTemplateVersion2.id,
rich_parameter_values: [],
});
});
it("fails when having missing parameters", async () => {
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValue(
MockWorkspaceBuild,
);
vi.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]);
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValue([
MockTemplateVersionParameter1,
{ ...MockTemplateVersionParameter2, mutable: false },
]);
let error = new Error();
try {
await API.updateWorkspace(MockWorkspace);
} catch (e) {
error = e as Error;
}
expect(error).toBeInstanceOf(MissingBuildParameters);
// Verify if the correct missing parameters are being passed
expect((error as MissingBuildParameters).parameters).toEqual([
MockTemplateVersionParameter1,
{ ...MockTemplateVersionParameter2, mutable: false },
]);
});
it("creates a build with no parameters if it is already filled", async () => {
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
transition: "stop",
});
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
template_version_id: MockTemplateVersion2.id,
transition: "start",
});
vi.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([
MockWorkspaceBuildParameter1,
]);
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValue([
{
...MockTemplateVersionParameter1,
required: true,
mutable: false,
},
]);
await API.updateWorkspace(MockWorkspace);
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
transition: "stop",
log_level: undefined,
});
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
transition: "start",
template_version_id: MockTemplate.active_version_id,
rich_parameter_values: [],
});
});
});
describe("given a stopped workspace", () => {
it("creates a build with start and the latest template", async () => {
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce(
MockWorkspaceBuild,
);
vi.spyOn(API, "getTemplate").mockResolvedValueOnce({
...MockTemplate,
active_version_id: MockTemplateVersion2.id,
});
await API.updateWorkspace(MockStoppedWorkspace);
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(
MockStoppedWorkspace.id,
{
transition: "start",
template_version_id: MockTemplateVersion2.id,
rich_parameter_values: [],
},
);
});
});
});
describe("chat configuration endpoints", () => {
it.each<[string, () => Promise<unknown>, unknown]>([
[
"/api/experimental/chats/models",
() => API.experimental.getChatModels(),
{
providers: [],
},
],
[
"/api/experimental/chats/providers",
() => API.experimental.getChatProviderConfigs(),
[],
],
[
"/api/experimental/chats/model-configs",
() => API.experimental.getChatModelConfigs(),
[],
],
])("returns response data for %s", async (path, request, responseData) => {
vi.spyOn(axiosInstance, "get").mockResolvedValueOnce({
data: responseData,
});
const result = await request();
expect(axiosInstance.get).toHaveBeenCalledWith(path);
expect(result).toStrictEqual(responseData);
});
it.each<[string, () => Promise<unknown>]>([
[
"/api/experimental/chats/models",
() => API.experimental.getChatModels(),
],
[
"/api/experimental/chats/providers",
() => API.experimental.getChatProviderConfigs(),
],
[
"/api/experimental/chats/model-configs",
() => API.experimental.getChatModelConfigs(),
],
])("rethrows axios errors for %s", async (path, request) => {
const expectedError = new Error("request failed");
vi.spyOn(axiosInstance, "get").mockRejectedValueOnce(expectedError);
await expect(request()).rejects.toBe(expectedError);
expect(axiosInstance.get).toHaveBeenCalledWith(path);
});
});
describe("user secrets endpoints", () => {
const userId = "me";
const secretName = "EXAMPLE_TOKEN";
const secretNameWithPathChars = "foo%2Fbar value";
const userSecret: TypesGen.UserSecret = {
id: "00000000-0000-0000-0000-000000000001",
name: secretName,
description: "Example token for tests",
env_name: secretName,
file_path: "",
created_at: "2026-05-04T00:00:00Z",
updated_at: "2026-05-04T00:00:00Z",
};
it("lists user secrets with the correct method and URL", async () => {
const axiosMockGet = vi.fn().mockResolvedValueOnce({
data: [userSecret],
});
axiosInstance.get = axiosMockGet;
const result = await API.getUserSecrets(userId);
expect(axiosMockGet).toHaveBeenCalledWith("/api/v2/users/me/secrets");
expect(result).toStrictEqual([userSecret]);
});
it("gets a user secret with the correct method and URL", async () => {
const axiosMockGet = vi.fn().mockResolvedValueOnce({
data: userSecret,
});
axiosInstance.get = axiosMockGet;
const result = await API.getUserSecret(userId, secretNameWithPathChars);
expect(axiosMockGet).toHaveBeenCalledWith(
"/api/v2/users/me/secrets/foo%252Fbar%20value",
);
expect(result).toStrictEqual(userSecret);
});
it("creates a user secret with the correct method and URL", async () => {
const request: TypesGen.CreateUserSecretRequest = {
name: secretName,
value: "",
description: "Example token for tests",
env_name: secretName,
};
const axiosMockPost = vi.fn().mockResolvedValueOnce({
data: userSecret,
});
axiosInstance.post = axiosMockPost;
const result = await API.createUserSecret(userId, request);
expect(axiosMockPost).toHaveBeenCalledWith(
"/api/v2/users/me/secrets",
request,
);
expect(result).toStrictEqual(userSecret);
});
it("updates a user secret with the correct method and URL", async () => {
const request: TypesGen.UpdateUserSecretRequest = {
description: "Updated example token for tests",
};
const updatedSecret: TypesGen.UserSecret = {
...userSecret,
description: "Updated example token for tests",
updated_at: "2026-05-04T00:01:00Z",
};
const axiosMockPatch = vi.fn().mockResolvedValueOnce({
data: updatedSecret,
});
axiosInstance.patch = axiosMockPatch;
const result = await API.updateUserSecret(
userId,
secretNameWithPathChars,
request,
);
expect(axiosMockPatch).toHaveBeenCalledWith(
"/api/v2/users/me/secrets/foo%252Fbar%20value",
request,
);
expect(result).toStrictEqual(updatedSecret);
});
it("deletes a user secret with the correct method and URL", async () => {
const axiosMockDelete = vi.fn().mockResolvedValueOnce(undefined);
axiosInstance.delete = axiosMockDelete;
await API.deleteUserSecret(userId, secretNameWithPathChars);
expect(axiosMockDelete).toHaveBeenCalledWith(
"/api/v2/users/me/secrets/foo%252Fbar%20value",
);
});
});
describe("chat ACL endpoints", () => {
const chatId = "chat-1";
const chatACL: TypesGen.ChatACL = {
users: [],
groups: [],
};
it("gets a chat ACL", async () => {
vi.spyOn(axiosInstance, "get").mockResolvedValueOnce({
data: chatACL,
});
const result = await API.experimental.getChatACL(chatId);
expect(axiosInstance.get).toHaveBeenCalledWith(
`/api/experimental/chats/${chatId}/acl`,
);
expect(result).toStrictEqual(chatACL);
});
it("updates a chat ACL", async () => {
const request: TypesGen.UpdateChatACL = {
user_roles: { "user-1": "read" },
};
vi.spyOn(axiosInstance, "patch").mockResolvedValueOnce({});
await API.experimental.updateChatACL(chatId, request);
expect(axiosInstance.patch).toHaveBeenCalledWith(
`/api/experimental/chats/${chatId}/acl`,
request,
);
});
});
});