test(apps): test domain linking
test(apps): test linking domain
This commit is contained in:
parent
95c9196e37
commit
60ef5816a7
8 changed files with 278 additions and 13 deletions
|
@ -99,7 +99,7 @@ If you want to link a domain to your dashboard, you can do so by providing the `
|
|||
sudo ./scripts/start.sh --domain mydomain.com
|
||||
```
|
||||
|
||||
A Let's Encrypt certificate will be generated and installed automatically. Make sure to have port 443 open on your firewall and that your domain has an **A** record pointing to your server IP.
|
||||
A Let's Encrypt certificate will be generated and installed automatically. Make sure to have ports 80 and 443 open on your firewall and that your domain has an **A** record pointing to your server IP.
|
||||
|
||||
## ❤️ Contributing
|
||||
|
||||
|
|
|
@ -8,10 +8,13 @@ interface IProps {
|
|||
status?: AppStatusEnum;
|
||||
requiredPort?: number;
|
||||
randomField?: boolean;
|
||||
exposed?: boolean;
|
||||
domain?: string;
|
||||
exposable?: boolean;
|
||||
}
|
||||
|
||||
const createApp = async (props: IProps) => {
|
||||
const { installed = false, status = AppStatusEnum.RUNNING, requiredPort, randomField = false } = props;
|
||||
const { installed = false, status = AppStatusEnum.RUNNING, requiredPort, randomField = false, exposed = false, domain = '', exposable = false } = props;
|
||||
|
||||
const categories = Object.values(AppCategoriesEnum);
|
||||
|
||||
|
@ -34,6 +37,7 @@ const createApp = async (props: IProps) => {
|
|||
author: faker.name.firstName(),
|
||||
source: faker.internet.url(),
|
||||
categories: [categories[faker.datatype.number({ min: 0, max: categories.length - 1 })]],
|
||||
exposable,
|
||||
};
|
||||
|
||||
if (randomField) {
|
||||
|
@ -63,6 +67,8 @@ const createApp = async (props: IProps) => {
|
|||
id: appInfo.id,
|
||||
config: { TEST_FIELD: 'test' },
|
||||
status,
|
||||
exposed,
|
||||
domain,
|
||||
}).save();
|
||||
|
||||
MockFiles[`${config.ROOT_FOLDER}/app-data/${appInfo.id}`] = '';
|
||||
|
|
|
@ -195,6 +195,46 @@ describe('generateEnvFile', () => {
|
|||
expect(e.message).toBe('App not-existing-app not found');
|
||||
}
|
||||
});
|
||||
|
||||
it('Should add APP_EXPOSED to env file', async () => {
|
||||
const domain = faker.internet.domainName();
|
||||
const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, exposed: true, domain });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
generateEnvFile(appEntity);
|
||||
|
||||
const envmap = await getEnvMap(appInfo.id);
|
||||
|
||||
expect(envmap.get('APP_EXPOSED')).toBe('true');
|
||||
expect(envmap.get('APP_DOMAIN')).toBe(domain);
|
||||
});
|
||||
|
||||
it('Should not add APP_EXPOSED if domain is not provided', async () => {
|
||||
const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, exposed: true });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
generateEnvFile(appEntity);
|
||||
|
||||
const envmap = await getEnvMap(appInfo.id);
|
||||
|
||||
expect(envmap.get('APP_EXPOSED')).toBeUndefined();
|
||||
expect(envmap.get('APP_DOMAIN')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Should not add APP_EXPOSED if app is not exposed', async () => {
|
||||
const { appEntity, appInfo, MockFiles } = await createApp({ installed: true, domain: faker.internet.domainName() });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
generateEnvFile(appEntity);
|
||||
|
||||
const envmap = await getEnvMap(appInfo.id);
|
||||
|
||||
expect(envmap.get('APP_EXPOSED')).toBeUndefined();
|
||||
expect(envmap.get('APP_DOMAIN')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAvailableApps', () => {
|
||||
|
@ -239,14 +279,10 @@ describe('getAppInfo', () => {
|
|||
expect(app?.id).toEqual(appInfo.id);
|
||||
});
|
||||
|
||||
it('Should throw an error if app does not exist', async () => {
|
||||
try {
|
||||
await getAppInfo('not-existing-app');
|
||||
expect(true).toBe(false);
|
||||
} catch (e: any) {
|
||||
expect(e).toBeDefined();
|
||||
expect(e.message).toBe('Error loading app not-existing-app');
|
||||
}
|
||||
it('Should return null if app does not exist', async () => {
|
||||
const app = await getAppInfo(faker.random.word());
|
||||
|
||||
expect(app).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -135,6 +135,22 @@ describe('Install app', () => {
|
|||
expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/test.yml`)).toBe(false);
|
||||
expect(fs.existsSync(`${config.ROOT_FOLDER}/apps/${app1.id}/docker-compose.yml`)).toBe(true);
|
||||
});
|
||||
|
||||
it('Should throw if app is exposed and domain is not provided', async () => {
|
||||
await expect(AppsService.installApp(app1.id, { TEST_FIELD: 'test' }, true)).rejects.toThrowError('Domain is required if app is exposed');
|
||||
});
|
||||
|
||||
it('Should throw if app is exposed and config does not allow it', async () => {
|
||||
await expect(AppsService.installApp(app1.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`App ${app1.id} is not exposable`);
|
||||
});
|
||||
|
||||
it('Should throw if app is exposed and domain is not valid', async () => {
|
||||
const { MockFiles, appInfo } = await createApp({ exposable: true });
|
||||
// @ts-ignore
|
||||
fs.__createMockFiles(MockFiles);
|
||||
|
||||
await expect(AppsService.installApp(appInfo.id, { TEST_FIELD: 'test' }, true, 'test')).rejects.toThrowError('Domain test is not valid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Uninstall app', () => {
|
||||
|
@ -334,6 +350,18 @@ describe('Update app config', () => {
|
|||
|
||||
expect(envMap.get('RANDOM_FIELD')).toBe('test');
|
||||
});
|
||||
|
||||
it('Should throw if app is exposed and domain is not provided', () => {
|
||||
return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true)).rejects.toThrowError('Domain is required');
|
||||
});
|
||||
|
||||
it('Should throw if app is exposed and domain is not valid', () => {
|
||||
return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test')).rejects.toThrowError('Domain test is not valid');
|
||||
});
|
||||
|
||||
it('Should throw if app is exposed and config does not allow it', () => {
|
||||
return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`App ${app1.id} is not exposable`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get app config', () => {
|
||||
|
|
|
@ -139,7 +139,7 @@ export const getAppInfo = (id: string): AppInfo | null => {
|
|||
const configFile: AppInfo = readJsonFile(`/apps/${id}/config.json`);
|
||||
configFile.description = readFile(`/apps/${id}/metadata/description.md`).toString();
|
||||
return configFile;
|
||||
} else if (fileExists(`/repos/${repoId}`)) {
|
||||
} else if (fileExists(`/repos/${repoId}/apps/${id}/config.json`)) {
|
||||
const configFile: AppInfo = readJsonFile(`/repos/${repoId}/apps/${id}/config.json`);
|
||||
configFile.description = readFile(`/repos/${repoId}/apps/${id}/metadata/description.md`);
|
||||
|
||||
|
@ -150,6 +150,7 @@ export const getAppInfo = (id: string): AppInfo | null => {
|
|||
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw new Error(`Error loading app ${id}`);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import validator from 'validator';
|
||||
import { createFolder, ensureAppFolder, readFile, readJsonFile } from '../fs/fs.helpers';
|
||||
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, runAppScript } from './apps.helpers';
|
||||
import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
|
||||
|
@ -69,6 +70,10 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
|
|||
throw new Error('Domain is required if app is exposed');
|
||||
}
|
||||
|
||||
if (domain && !validator.isFQDN(domain)) {
|
||||
throw new Error(`Domain ${domain} is not valid`);
|
||||
}
|
||||
|
||||
ensureAppFolder(id, true);
|
||||
const appIsValid = await checkAppRequirements(id);
|
||||
|
||||
|
@ -130,6 +135,16 @@ const updateAppConfig = async (id: string, form: Record<string, string>, exposed
|
|||
throw new Error('Domain is required if app is exposed');
|
||||
}
|
||||
|
||||
if (domain && !validator.isFQDN(domain)) {
|
||||
throw new Error(`Domain ${domain} is not valid`);
|
||||
}
|
||||
|
||||
const appInfo: AppInfo | null = await readJsonFile(`/apps/${id}/config.json`);
|
||||
|
||||
if (!appInfo?.exposable && exposed) {
|
||||
throw new Error(`App ${id} is not exposable`);
|
||||
}
|
||||
|
||||
let app = await App.findOne({ where: { id } });
|
||||
|
||||
if (!app) {
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||
import { gcall } from '../../../test/gcall';
|
||||
import { loginMutation, registerMutation } from '../../../test/mutations';
|
||||
import { isConfiguredQuery, MeQuery } from '../../../test/queries';
|
||||
import User from '../../auth/user.entity';
|
||||
import { UserResponse } from '../auth.types';
|
||||
import { createUser } from './user.factory';
|
||||
|
||||
let db: DataSource | null = null;
|
||||
const TEST_SUITE = 'authresolver';
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await setupConnection(TEST_SUITE);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await db?.destroy();
|
||||
await teardownConnection(TEST_SUITE);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetModules();
|
||||
jest.resetAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
await User.clear();
|
||||
});
|
||||
|
||||
describe('Test: me', () => {
|
||||
const email = faker.internet.email();
|
||||
let user1: User;
|
||||
|
||||
beforeEach(async () => {
|
||||
user1 = await createUser(email);
|
||||
});
|
||||
|
||||
it('should return null if no user is logged in', async () => {
|
||||
const { data } = await gcall<{ me: User }>({
|
||||
source: MeQuery,
|
||||
});
|
||||
|
||||
expect(data?.me).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the user if a user is logged in', async () => {
|
||||
const { data } = await gcall<{ me: User | null }>({
|
||||
source: MeQuery,
|
||||
userId: user1.id,
|
||||
});
|
||||
|
||||
expect(data?.me?.username).toEqual(user1.username);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: register', () => {
|
||||
const email = faker.internet.email();
|
||||
const password = faker.internet.password();
|
||||
|
||||
it('should register a user', async () => {
|
||||
const { data } = await gcall<{ register: UserResponse }>({
|
||||
source: registerMutation,
|
||||
variableValues: {
|
||||
input: { username: email, password },
|
||||
},
|
||||
});
|
||||
|
||||
expect(data?.register.user?.username).toEqual(email.toLowerCase());
|
||||
});
|
||||
|
||||
it('should not register a user with an existing username', async () => {
|
||||
await createUser(email);
|
||||
|
||||
const { errors } = await gcall<{ register: UserResponse }>({
|
||||
source: registerMutation,
|
||||
variableValues: {
|
||||
input: { username: email, password },
|
||||
},
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toEqual('User already exists');
|
||||
});
|
||||
|
||||
it('should not register a user with a malformed email', async () => {
|
||||
const { errors } = await gcall<{ register: UserResponse }>({
|
||||
source: registerMutation,
|
||||
variableValues: {
|
||||
input: { username: 'not an email', password },
|
||||
},
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toEqual('Invalid username');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: login', () => {
|
||||
const email = faker.internet.email();
|
||||
|
||||
beforeEach(async () => {
|
||||
await createUser(email);
|
||||
});
|
||||
|
||||
it('should login a user', async () => {
|
||||
const { data } = await gcall<{ login: UserResponse }>({
|
||||
source: loginMutation,
|
||||
variableValues: {
|
||||
input: { username: email, password: 'password' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(data?.login.user?.username).toEqual(email.toLowerCase());
|
||||
});
|
||||
|
||||
it('should not login a user with an incorrect password', async () => {
|
||||
const { errors } = await gcall<{ login: UserResponse }>({
|
||||
source: loginMutation,
|
||||
variableValues: {
|
||||
input: { username: email, password: 'wrong password' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toEqual('Wrong password');
|
||||
});
|
||||
|
||||
it('should not login a user with a malformed email', async () => {
|
||||
const { errors } = await gcall<{ login: UserResponse }>({
|
||||
source: loginMutation,
|
||||
variableValues: {
|
||||
input: { username: 'not an email', password: 'password' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(errors?.[0].message).toEqual('User not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: logout', () => {
|
||||
const email = faker.internet.email();
|
||||
let user1: User;
|
||||
|
||||
beforeEach(async () => {
|
||||
user1 = await createUser(email);
|
||||
});
|
||||
|
||||
it('should logout a user', async () => {
|
||||
const { data } = await gcall<{ logout: boolean }>({
|
||||
source: 'mutation { logout }',
|
||||
userId: user1.id,
|
||||
});
|
||||
|
||||
expect(data?.logout).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: isConfigured', () => {
|
||||
it('should return false if no users exist', async () => {
|
||||
const { data } = await gcall<{ isConfigured: boolean }>({
|
||||
source: isConfiguredQuery,
|
||||
});
|
||||
|
||||
expect(data?.isConfigured).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true if a user exists', async () => {
|
||||
await createUser(faker.internet.email());
|
||||
|
||||
const { data } = await gcall<{ isConfigured: boolean }>({
|
||||
source: isConfiguredQuery,
|
||||
});
|
||||
|
||||
expect(data?.isConfigured).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import * as argon2 from 'argon2';
|
||||
import validator from 'validator';
|
||||
import { UsernamePasswordInput, UserResponse } from './auth.types';
|
||||
import User from './user.entity';
|
||||
|
||||
|
@ -22,19 +23,24 @@ const login = async (input: UsernamePasswordInput): Promise<UserResponse> => {
|
|||
|
||||
const register = async (input: UsernamePasswordInput): Promise<UserResponse> => {
|
||||
const { password, username } = input;
|
||||
const email = username.trim().toLowerCase();
|
||||
|
||||
if (!username || !password) {
|
||||
throw new Error('Missing email or password');
|
||||
}
|
||||
|
||||
const user = await User.findOne({ where: { username: username.trim().toLowerCase() } });
|
||||
if (username.length < 3 || !validator.isEmail(email)) {
|
||||
throw new Error('Invalid username');
|
||||
}
|
||||
|
||||
const user = await User.findOne({ where: { username: email } });
|
||||
|
||||
if (user) {
|
||||
throw new Error('User already exists');
|
||||
}
|
||||
|
||||
const hash = await argon2.hash(password);
|
||||
const newUser = await User.create({ username: username.trim().toLowerCase(), password: hash }).save();
|
||||
const newUser = await User.create({ username: email, password: hash }).save();
|
||||
|
||||
return { user: newUser };
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue