hoppscotch-backend-git/tests/auth.graphql.test.ts
tlemesle 2974765435 Add updateRequest for team requests
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 17:39:55 +02:00

681 lines
25 KiB
TypeScript

import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import axios from 'axios';
import { createApp } from '../src/app.ts';
import { loadConfig } from '../src/config.ts';
import { userFromOsIdentity } from '../src/os-user.ts';
const jwtSecret = 'test-jwt-secret-16chars';
const testUser = await userFromOsIdentity({
recordName: 'testuser',
email: 'testuser@example.com',
displayName: 'Test User',
});
describe('Auth + GraphQL', () => {
let gitRepoPath = '';
let metaPath = '';
let baseURL = '';
let server: ReturnType<typeof Bun.serve> | null = null;
beforeAll(async () => {
gitRepoPath = await mkdtemp(join(tmpdir(), 'hbg-gql-git-'));
metaPath = join(await mkdtemp(join(tmpdir(), 'hbg-gql-meta-')), 'meta.json');
const config = await loadConfig({
PORT: '0',
GIT_REPO_PATH: gitRepoPath,
META_PATH: metaPath,
JWT_SECRET: jwtSecret,
WEBAPP_FRONTEND_PATH: join(import.meta.dir, '../fixtures/webapp-frontend'),
WEBAPP_SERVER_SIGNING_SECRET: 'test-webapp-signing-secret',
HOPP_STATE_DIR: await mkdtemp(join(tmpdir(), 'hbg-gql-state-')),
});
const app = await createApp(config, { localUser: testUser });
server = Bun.serve({
port: 0,
fetch: app.fetch,
});
baseURL = `http://127.0.0.1:${server.port}`;
});
afterAll(async () => {
server?.stop(true);
await rm(gitRepoPath, { recursive: true, force: true });
await rm(join(metaPath, '..'), { recursive: true, force: true });
});
test('GraphQL me uses local OS-derived user without credentials', async () => {
const providers = await axios.get(`${baseURL}/v1/auth/providers`);
expect(providers.data.providers).toEqual([]);
const me = await axios.post(
`${baseURL}/graphql`,
{ query: '{ me { uid email displayName isAdmin } }' },
);
expect(me.data.errors).toBeUndefined();
expect(me.data.data.me.uid).toBe(testUser.uid);
expect(me.data.data.me.email).toBe(testUser.email);
expect(me.data.data.me.displayName).toBe(testUser.displayName);
});
test('createRESTRootUserCollection persists to git', async () => {
const create = await axios.post(
`${baseURL}/graphql`,
{
query: `mutation($title: String!) {
createRESTRootUserCollection(title: $title) {
id
title
type
}
}`,
variables: { title: 'From GraphQL' },
},
);
expect(create.data.errors).toBeUndefined();
const id = create.data.data.createRESTRootUserCollection.id as string;
expect(create.data.data.createRESTRootUserCollection.title).toBe('From GraphQL');
const file = Bun.file(join(gitRepoPath, 'collections', `${id}.json`));
expect(await file.exists()).toBe(true);
const saved = JSON.parse(await file.text());
expect(saved.name).toBe('From GraphQL');
const list = await axios.post(
`${baseURL}/graphql`,
{ query: '{ rootRESTUserCollections { id title } }' },
);
expect(list.data.data.rootRESTUserCollections.some(
(c: { id: string }) => c.id === id,
)).toBe(true);
const env = await axios.post(
`${baseURL}/graphql`,
{
query: `mutation {
createUserEnvironment(
name: "Dev"
variables: "[{\\"key\\":\\"k\\",\\"secret\\":false,\\"initialValue\\":\\"v\\",\\"currentValue\\":\\"v\\"}]"
) { id name isGlobal }
}`,
},
);
expect(env.data.errors).toBeUndefined();
expect(env.data.data.createUserEnvironment.name).toBe('Dev');
expect(await Bun.file(
join(gitRepoPath, 'environments', `${env.data.data.createUserEnvironment.id}.json`),
).exists()).toBe(true);
});
test('PAT create works without login', async () => {
const patRes = await axios.post(
`${baseURL}/v1/access-tokens/create`,
{ label: 'from-open', expiryInDays: 30 },
);
expect(patRes.data.token).toMatch(/^pat-/);
});
test('myTeams returns empty list before any team is created', async () => {
const res = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'GetMyTeams',
query: `query GetMyTeams($cursor: ID) {
myTeams(cursor: $cursor) {
id
name
myRole
ownersCount
teamMembers {
membershipID
user { photoURL displayName email uid }
role
}
}
}`,
variables: {},
},
);
expect(res.data.errors).toBeUndefined();
expect(res.data.data.myTeams).toEqual([]);
});
test('createTeam creates a team owned by the current user', async () => {
const createRes = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'CreateTeam',
query: `mutation CreateTeam($name: String!) {
createTeam(name: $name) {
id
name
members {
membershipID
role
user {
uid
displayName
email
photoURL
}
}
myRole
ownersCount
editorsCount
viewersCount
}
}`,
variables: { name: 'test workspace' },
},
);
expect(createRes.data.errors).toBeUndefined();
const team = createRes.data.data.createTeam;
expect(team.name).toBe('test workspace');
expect(team.myRole).toBe('OWNER');
expect(team.ownersCount).toBe(1);
expect(team.editorsCount).toBe(0);
expect(team.viewersCount).toBe(0);
expect(team.members).toHaveLength(1);
expect(team.members[0].role).toBe('OWNER');
expect(team.members[0].user.uid).toBe(testUser.uid);
expect(team.members[0].user.email).toBe(testUser.email);
const listRes = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'GetMyTeams',
query: `query GetMyTeams($cursor: ID) {
myTeams(cursor: $cursor) {
id
name
myRole
ownersCount
}
}`,
variables: {},
},
);
expect(listRes.data.errors).toBeUndefined();
expect(listRes.data.data.myTeams).toEqual([
{
id: team.id,
name: 'test workspace',
myRole: 'OWNER',
ownersCount: 1,
},
]);
});
test('createRootCollection persists under teams/{teamID}/collections', async () => {
const teamRes = await axios.post(
`${baseURL}/graphql`,
{
query: `mutation($name: String!) {
createTeam(name: $name) { id }
}`,
variables: { name: 'collections team' },
},
);
expect(teamRes.data.errors).toBeUndefined();
const teamID = teamRes.data.data.createTeam.id as string;
const createRes = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'CreateNewRootCollection',
query: `mutation CreateNewRootCollection($title: String!, $teamID: ID!) {
createRootCollection(title: $title, teamID: $teamID) {
id
}
}`,
variables: {
teamID,
title: 'test collection in test workspace',
},
},
);
expect(createRes.data.errors).toBeUndefined();
const collectionID = createRes.data.data.createRootCollection.id as string;
const file = Bun.file(join(
gitRepoPath,
'teams',
teamID,
'collections',
`${collectionID}.json`,
));
expect(await file.exists()).toBe(true);
const saved = JSON.parse(await file.text());
expect(saved.title).toBe('test collection in test workspace');
expect(saved.teamID).toBe(teamID);
expect(saved.parentID).toBeNull();
const listRes = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'RootCollectionsOfTeam',
query: `query RootCollectionsOfTeam($teamID: ID!, $cursor: ID) {
rootCollectionsOfTeam(teamID: $teamID, cursor: $cursor) {
id
title
data
}
}`,
variables: { teamID },
},
);
expect(listRes.data.errors).toBeUndefined();
expect(listRes.data.data.rootCollectionsOfTeam).toEqual([
{
id: collectionID,
title: 'test collection in test workspace',
data: null,
},
]);
});
test('collection children lists nested team collections', async () => {
const teamRes = await axios.post(
`${baseURL}/graphql`,
{
query: `mutation($name: String!) {
createTeam(name: $name) { id }
}`,
variables: { name: 'children team' },
},
);
expect(teamRes.data.errors).toBeUndefined();
const teamID = teamRes.data.data.createTeam.id as string;
const rootRes = await axios.post(
`${baseURL}/graphql`,
{
query: `mutation($title: String!, $teamID: ID!) {
createRootCollection(title: $title, teamID: $teamID) { id }
}`,
variables: { teamID, title: 'parent collection' },
},
);
expect(rootRes.data.errors).toBeUndefined();
const collectionID = rootRes.data.data.createRootCollection.id as string;
const empty = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'GetCollectionChildren',
query: `query GetCollectionChildren($collectionID: ID!, $cursor: ID) {
collection(collectionID: $collectionID) {
children(cursor: $cursor) {
id
title
data
}
}
}`,
variables: { collectionID },
},
);
expect(empty.data.errors).toBeUndefined();
expect(empty.data.data.collection.children).toEqual([]);
const childRes = await axios.post(
`${baseURL}/graphql`,
{
query: `mutation($childTitle: String!, $collectionID: ID!) {
createChildCollection(childTitle: $childTitle, collectionID: $collectionID) {
id
title
}
}`,
variables: {
collectionID,
childTitle: 'nested folder',
},
},
);
expect(childRes.data.errors).toBeUndefined();
const childID = childRes.data.data.createChildCollection.id as string;
expect(childRes.data.data.createChildCollection.title).toBe('nested folder');
const listed = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'GetCollectionChildren',
query: `query GetCollectionChildren($collectionID: ID!, $cursor: ID) {
collection(collectionID: $collectionID) {
children(cursor: $cursor) {
id
title
data
}
}
}`,
variables: { collectionID },
},
);
expect(listed.data.errors).toBeUndefined();
expect(listed.data.data.collection.children).toEqual([
{ id: childID, title: 'nested folder', data: null },
]);
});
test('createRequestInCollection persists under teams/{teamID}/requests', async () => {
const teamRes = await axios.post(
`${baseURL}/graphql`,
{
query: `mutation($name: String!) {
createTeam(name: $name) { id name }
}`,
variables: { name: 'requests team' },
},
);
expect(teamRes.data.errors).toBeUndefined();
const teamID = teamRes.data.data.createTeam.id as string;
const teamName = teamRes.data.data.createTeam.name as string;
const collRes = await axios.post(
`${baseURL}/graphql`,
{
query: `mutation($title: String!, $teamID: ID!) {
createRootCollection(title: $title, teamID: $teamID) { id }
}`,
variables: { teamID, title: 'request collection' },
},
);
expect(collRes.data.errors).toBeUndefined();
const collectionID = collRes.data.data.createRootCollection.id as string;
const requestJson = JSON.stringify({
v: '17',
endpoint: 'https://echo.hoppscotch.io',
name: 'test request',
method: 'GET',
params: [],
headers: [],
auth: { authType: 'inherit', authActive: true },
preRequestScript: '',
testScript: '',
body: { contentType: null, body: null },
requestVariables: [],
responses: {},
});
const createRes = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'CreateRequestInCollection',
query: `mutation CreateRequestInCollection($data: CreateTeamRequestInput!, $collectionID: ID!) {
createRequestInCollection(data: $data, collectionID: $collectionID) {
id
collection {
id
team {
id
name
}
}
}
}`,
variables: {
collectionID,
data: {
request: requestJson,
teamID,
title: 'test request in test collection',
},
},
},
);
expect(createRes.data.errors).toBeUndefined();
const created = createRes.data.data.createRequestInCollection;
expect(created.collection.id).toBe(collectionID);
expect(created.collection.team).toEqual({ id: teamID, name: teamName });
const file = Bun.file(join(
gitRepoPath,
'teams',
teamID,
'requests',
`${created.id}.json`,
));
expect(await file.exists()).toBe(true);
const saved = JSON.parse(await file.text());
expect(saved.title).toBe('test request in test collection');
expect(saved.collectionID).toBe(collectionID);
expect(saved.request.endpoint).toBe('https://echo.hoppscotch.io');
const listRes = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'GetCollectionRequests',
query: `query GetCollectionRequests($collectionID: ID!, $cursor: ID) {
requestsInCollection(collectionID: $collectionID, cursor: $cursor) {
id
title
request
}
}`,
variables: { collectionID },
},
);
expect(listRes.data.errors).toBeUndefined();
expect(listRes.data.data.requestsInCollection).toHaveLength(1);
expect(listRes.data.data.requestsInCollection[0].id).toBe(created.id);
expect(listRes.data.data.requestsInCollection[0].title).toBe(
'test request in test collection',
);
expect(JSON.parse(listRes.data.data.requestsInCollection[0].request).method)
.toBe('GET');
const updatedRequest = {
...JSON.parse(requestJson),
name: 'renamed request',
method: 'POST',
};
const updateRes = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'UpdateRequest',
query: `mutation UpdateRequest($data: UpdateTeamRequestInput!, $requestID: ID!) {
updateRequest(data: $data, requestID: $requestID) {
id
title
}
}`,
variables: {
requestID: created.id,
data: {
title: 'renamed request',
request: JSON.stringify(updatedRequest),
},
},
},
);
expect(updateRes.data.errors).toBeUndefined();
expect(updateRes.data.data.updateRequest).toEqual({
id: created.id,
title: 'renamed request',
});
const updatedFile = JSON.parse(await Bun.file(join(
gitRepoPath,
'teams',
teamID,
'requests',
`${created.id}.json`,
)).text());
expect(updatedFile.title).toBe('renamed request');
expect(updatedFile.request.method).toBe('POST');
});
test('team environments, SMTP, published docs, and mock servers queries', async () => {
const teamRes = await axios.post(
`${baseURL}/graphql`,
{
query: `mutation($name: String!) {
createTeam(name: $name) { id }
}`,
variables: { name: 'env team' },
},
);
expect(teamRes.data.errors).toBeUndefined();
const teamID = teamRes.data.data.createTeam.id as string;
const smtp = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'GetSMTPStatus',
query: 'query GetSMTPStatus { isSMTPEnabled }',
},
);
expect(smtp.data.errors).toBeUndefined();
expect(smtp.data.data.isSMTPEnabled).toBe(false);
const emptyEnvs = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'GetTeamEnvironments',
query: `query GetTeamEnvironments($teamID: ID!) {
team(teamID: $teamID) {
teamEnvironments {
id
name
variables
teamID
}
}
}`,
variables: { teamID },
},
);
expect(emptyEnvs.data.errors).toBeUndefined();
expect(emptyEnvs.data.data.team.teamEnvironments).toEqual([]);
const createEnv = await axios.post(
`${baseURL}/graphql`,
{
query: `mutation($variables: String!, $teamID: ID!, $name: String!) {
createTeamEnvironment(variables: $variables, teamID: $teamID, name: $name) {
id
name
variables
teamID
}
}`,
variables: {
teamID,
name: 'Staging',
variables: JSON.stringify([
{ key: 'baseUrl', secret: false, initialValue: 'https://example.com', currentValue: 'https://example.com' },
]),
},
},
);
expect(createEnv.data.errors).toBeUndefined();
const env = createEnv.data.data.createTeamEnvironment;
expect(env.name).toBe('Staging');
expect(env.teamID).toBe(teamID);
expect(await Bun.file(join(
gitRepoPath,
'teams',
teamID,
'environments',
`${env.id}.json`,
)).exists()).toBe(true);
const listed = await axios.post(
`${baseURL}/graphql`,
{
query: `query($teamID: ID!) {
team(teamID: $teamID) {
teamEnvironments { id name teamID }
}
}`,
variables: { teamID },
},
);
expect(listed.data.errors).toBeUndefined();
expect(listed.data.data.team.teamEnvironments).toEqual([
{ id: env.id, name: 'Staging', teamID },
]);
const docs = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'TeamPublishedDocsList',
query: `query TeamPublishedDocsList($teamID: ID!, $collectionID: ID, $skip: Int!, $take: Int!) {
teamPublishedDocsList(teamID: $teamID, collectionID: $collectionID, skip: $skip, take: $take) {
id
title
version
autoSync
url
documentTree
environmentName
collection { id }
createdOn
updatedOn
}
}`,
variables: { teamID, skip: 0, take: 100 },
},
);
expect(docs.data.errors).toBeUndefined();
expect(docs.data.data.teamPublishedDocsList).toEqual([]);
const mocks = await axios.post(
`${baseURL}/graphql`,
{
operationName: 'GetTeamMockServers',
query: `query GetTeamMockServers($teamID: ID!, $skip: Int, $take: Int) {
teamMockServers(teamID: $teamID, skip: $skip, take: $take) {
id
name
subdomain
serverUrlPathBased
serverUrlDomainBased
workspaceType
workspaceID
delayInMs
isPublic
isActive
createdOn
updatedOn
creator { uid }
collection { id title }
}
}`,
variables: { teamID },
},
);
expect(mocks.data.errors).toBeUndefined();
expect(mocks.data.data.teamMockServers).toEqual([]);
});
test('device-login and desktop-confirm pages show plain-text local user details', async () => {
for(const path of [
'/device-login',
'/desktop-confirm',
'/backend/desktop-confirm',
]) {
const page = await axios.get(
`${baseURL}${path}?redirect_uri=${encodeURIComponent('http://localhost:15000/device-token')}`,
);
expect(page.status).toBe(200);
expect(String(page.headers['content-type'])).toContain('text/html');
expect(page.data).toContain('Confirm Desktop Login');
expect(page.data).toContain(testUser.email);
expect(page.data).toContain(testUser.displayName);
expect(page.data).toContain(testUser.uid);
expect(page.data).toContain('<dl class="fields">');
expect(page.data).not.toContain('<input');
expect(page.data).not.toContain('type="password"');
expect(page.data).not.toContain('id="app"');
}
});
});