test(apps): test domain linking

test(apps): test linking domain
This commit is contained in:
Nicolas Meienberger 2022-09-05 21:38:52 +02:00
parent 95c9196e37
commit 60ef5816a7
8 changed files with 278 additions and 13 deletions

View file

@ -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

View file

@ -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}`] = '';

View file

@ -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();
});
});

View file

@ -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', () => {

View file

@ -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}`);
}
};

View file

@ -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) {

View file

@ -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();
});
});

View file

@ -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 };
};