681 lines
25 KiB
TypeScript
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"');
|
|
}
|
|
});
|
|
});
|