Compare commits
No commits in common. "main" and "v0.0.6" have entirely different histories.
7
.gitignore
vendored
7
.gitignore
vendored
@ -20,12 +20,5 @@ Thumbs.db
|
|||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
# Playwright
|
|
||||||
/test-results/
|
|
||||||
/playwright-report/
|
|
||||||
/blob-report/
|
|
||||||
/playwright/.cache/
|
|
||||||
/playwright/.auth
|
|
||||||
|
|
||||||
uploads
|
uploads
|
||||||
.direnv
|
.direnv
|
||||||
|
2164
package-lock.json
generated
2164
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -1,21 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "fotochallenge",
|
"name": "fotochallenge",
|
||||||
"version": "0.0.8",
|
"version": "0.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "npm run test:integration && npm run test:unit",
|
"test": "vitest",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"lint": "prettier --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write ."
|
||||||
"test:integration": "playwright test",
|
|
||||||
"test:unit": "vitest"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.46.1",
|
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
"@sveltejs/adapter-node": "^5.2.0",
|
"@sveltejs/adapter-node": "^5.2.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
@ -34,7 +31,7 @@
|
|||||||
"sass": "^1.77.5",
|
"sass": "^1.77.5",
|
||||||
"simple-svelte-autocomplete": "^2.5.2",
|
"simple-svelte-autocomplete": "^2.5.2",
|
||||||
"svelte": "^4.2.7",
|
"svelte": "^4.2.7",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^3.6.0",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.4.1",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"typescript-eslint": "^8.0.0-alpha.20",
|
"typescript-eslint": "^8.0.0-alpha.20",
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
|
||||||
|
|
||||||
const config: PlaywrightTestConfig = {
|
|
||||||
webServer: {
|
|
||||||
command: 'npm run build && npm run preview',
|
|
||||||
port: 4173,
|
|
||||||
},
|
|
||||||
testDir: 'tests',
|
|
||||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
@ -1,12 +1,4 @@
|
|||||||
{
|
{
|
||||||
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
|
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
|
||||||
extends: ['local>renovate-bot/renovate-config'],
|
extends: ['local>renovate-bot/renovate-config'],
|
||||||
packageRules: [
|
|
||||||
{
|
|
||||||
groupName: 'all non-major dependencies',
|
|
||||||
groupSlug: 'all-minor-patch',
|
|
||||||
matchPackageNames: ['*'],
|
|
||||||
matchUpdateTypes: ['minor', 'patch'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { log, timedExecution, requestIdHeader } from '$lib';
|
import { log, timedExecution } from '$lib';
|
||||||
import { validate, v7 as uuidv7 } from 'uuid';
|
import { validate, v7 as uuidv7 } from 'uuid';
|
||||||
import type { Handle } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
const requestIdHeader = 'x-request-id';
|
||||||
// use incoming requestId, if it is a valid uuid, else generate one
|
|
||||||
|
/** @type {import('@sveltejs/kit').Handle} */
|
||||||
|
export async function handle({ event, resolve }) {
|
||||||
|
// use incomming requestId, if it is a valid uuid, else generate one
|
||||||
const reqIdFromRequest = event.request.headers.get(requestIdHeader);
|
const reqIdFromRequest = event.request.headers.get(requestIdHeader);
|
||||||
const { requestId, fromRequest } =
|
const { requestId, fromRequest } =
|
||||||
reqIdFromRequest && validate(reqIdFromRequest)
|
reqIdFromRequest && validate(reqIdFromRequest)
|
||||||
@ -18,15 +20,15 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
clientIP: event.getClientAddress(),
|
clientIP: event.getClientAddress(),
|
||||||
};
|
};
|
||||||
if (fromRequest) {
|
if (fromRequest) {
|
||||||
log.trace(context, 'using incoming request-id');
|
log.trace(context, 'using incomming request-id');
|
||||||
}
|
}
|
||||||
|
|
||||||
// make requestId available to handlers
|
// make requestId available to handlers
|
||||||
event.locals.requestId = requestId;
|
event.locals.requestId = requestId;
|
||||||
|
|
||||||
const { executionTime, result: response } = await timedExecution(
|
const { executionTime, result: response } = await timedExecution(async () => {
|
||||||
async () => await resolve(event)
|
return await resolve(event);
|
||||||
);
|
});
|
||||||
response.headers.set(requestIdHeader, requestId);
|
response.headers.set(requestIdHeader, requestId);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
@ -40,4 +42,4 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import safePath, { timedExecution } from '$lib';
|
import safePath from '$lib';
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('safe path', () => {
|
describe('safe path', () => {
|
||||||
@ -30,21 +30,3 @@ describe('safe path', () => {
|
|||||||
expect(safePath('./uplodas', '..foobar..')).toBe(true);
|
expect(safePath('./uplodas', '..foobar..')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('timedExecution', () => {
|
|
||||||
const asyncIdentity = async <T>(v: T): Promise<T> => v;
|
|
||||||
const identity = <T>(v: T): T => v;
|
|
||||||
|
|
||||||
it('works with async', async () => {
|
|
||||||
const { executionTime, result } = await timedExecution(() => asyncIdentity(5));
|
|
||||||
expect(result).toBe(5);
|
|
||||||
// execution time is always positive
|
|
||||||
expect(executionTime).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
it('works with sync', async () => {
|
|
||||||
const { executionTime, result } = await timedExecution(() => identity(5));
|
|
||||||
expect(result).toBe(5);
|
|
||||||
// execution time is always positive
|
|
||||||
expect(executionTime).toBeGreaterThanOrEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,6 +1,7 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import bunyan from 'bunyan';
|
import bunyan from 'bunyan';
|
||||||
|
import type { MaybePromise } from '@sveltejs/kit';
|
||||||
|
|
||||||
export const log = bunyan.createLogger({
|
export const log = bunyan.createLogger({
|
||||||
name: 'fotochallenge',
|
name: 'fotochallenge',
|
||||||
@ -8,7 +9,7 @@ export const log = bunyan.createLogger({
|
|||||||
src: true,
|
src: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const safePath = (basePath: string, name: string): boolean => {
|
function safePath(basePath: string, name: string): boolean {
|
||||||
const fullPath = `${basePath}/${name}`;
|
const fullPath = `${basePath}/${name}`;
|
||||||
const relative = path.relative(basePath, fullPath);
|
const relative = path.relative(basePath, fullPath);
|
||||||
return (
|
return (
|
||||||
@ -20,9 +21,7 @@ const safePath = (basePath: string, name: string): boolean => {
|
|||||||
// result is not an absolute path
|
// result is not an absolute path
|
||||||
!path.isAbsolute(relative)
|
!path.isAbsolute(relative)
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default safePath;
|
|
||||||
|
|
||||||
const defaultPath: string = './uploads';
|
const defaultPath: string = './uploads';
|
||||||
if (!('STORAGE_PATH' in process.env)) {
|
if (!('STORAGE_PATH' in process.env)) {
|
||||||
@ -30,9 +29,7 @@ if (!('STORAGE_PATH' in process.env)) {
|
|||||||
}
|
}
|
||||||
export const storagePath: string = process.env.STORAGE_PATH ?? defaultPath;
|
export const storagePath: string = process.env.STORAGE_PATH ?? defaultPath;
|
||||||
|
|
||||||
export const requestIdHeader = 'x-request-id';
|
export default safePath;
|
||||||
|
|
||||||
export type MaybePromise<T> = T | Promise<T>;
|
|
||||||
|
|
||||||
export async function timedExecution<T>(
|
export async function timedExecution<T>(
|
||||||
fn: () => MaybePromise<T>
|
fn: () => MaybePromise<T>
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
import { storagePath } from '$lib';
|
|
||||||
import { json } from '@sveltejs/kit';
|
|
||||||
import { stat, access, constants } from 'fs/promises';
|
|
||||||
import type { RequestHandler } from './$types';
|
|
||||||
|
|
||||||
type Status = 'OK' | 'ERROR';
|
|
||||||
type Result = { status: Status; checks: Checks };
|
|
||||||
|
|
||||||
type Checks = { [key: string]: CheckResult };
|
|
||||||
type CheckResult = true | string;
|
|
||||||
|
|
||||||
const fileExists = async (path: string) => !!(await stat(path).catch(() => false));
|
|
||||||
const isDirectory = async (path: string) =>
|
|
||||||
!!(await stat(path)
|
|
||||||
.then((s) => s.isDirectory())
|
|
||||||
.catch(() => false));
|
|
||||||
const isWritable = async (path: string) =>
|
|
||||||
await access(path, constants.W_OK)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false);
|
|
||||||
|
|
||||||
const checkStoragePath = async (path: string): Promise<CheckResult> => {
|
|
||||||
if (!fileExists(path)) {
|
|
||||||
return '`STORAGE_PATH` does not exist';
|
|
||||||
} else if (!isDirectory(path)) {
|
|
||||||
return '`STORAGE_PATH` is not a directory';
|
|
||||||
} else if (!isWritable(path)) {
|
|
||||||
return '`STORAGE_PATH` is not writable';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GET: RequestHandler = async () => {
|
|
||||||
const storagePathResult = await checkStoragePath(storagePath);
|
|
||||||
|
|
||||||
const checks: Checks = {
|
|
||||||
storagePath: storagePathResult,
|
|
||||||
};
|
|
||||||
|
|
||||||
const healthy = Object.values(checks)
|
|
||||||
.map((r) => (r === true ? true : false))
|
|
||||||
.reduce((prev, next) => prev && next, true);
|
|
||||||
|
|
||||||
const status = healthy ? 'OK' : 'ERROR';
|
|
||||||
|
|
||||||
const result: Result = { status, checks };
|
|
||||||
const httpStatus = healthy ? 200 : 500;
|
|
||||||
|
|
||||||
return json(result, { status: httpStatus, headers: { healthy: healthy.toString() } });
|
|
||||||
};
|
|
@ -1,12 +1,12 @@
|
|||||||
import { storagePath } from '$lib';
|
import { storagePath } from '$lib';
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { readdirSync, statSync } from 'fs';
|
import { readdirSync, statSync } from 'fs';
|
||||||
import type { RequestHandler } from './$types';
|
|
||||||
|
|
||||||
export const GET: RequestHandler = () => {
|
export function GET() {
|
||||||
const names = readdirSync(storagePath)
|
const names = readdirSync(storagePath).filter((f) =>
|
||||||
.filter((f) => statSync(`${storagePath}/${f}`).isDirectory())
|
statSync(`${storagePath}/${f}`).isDirectory()
|
||||||
.toSorted();
|
);
|
||||||
|
names.sort();
|
||||||
|
|
||||||
return json(names);
|
return json(names);
|
||||||
};
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
test('healthy test', async ({ playwright }) => {
|
|
||||||
const context = await playwright.request.newContext();
|
|
||||||
const response = await context.get('health');
|
|
||||||
await expect(response.status()).toBe(200);
|
|
||||||
await expect(response.headers()).toHaveProperty('healthy');
|
|
||||||
await expect(response.headers()['healthy']).toBe('true');
|
|
||||||
|
|
||||||
const body = await response.json();
|
|
||||||
await expect(body).toHaveProperty('status');
|
|
||||||
await expect(body['status']).toBe('OK');
|
|
||||||
});
|
|
@ -1,10 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
test('contains header text', async ({ playwright }) => {
|
|
||||||
const context = await playwright.request.newContext();
|
|
||||||
const response = await context.get('');
|
|
||||||
await expect(response.status()).toBe(200);
|
|
||||||
|
|
||||||
const body = (await response.body()).toString();
|
|
||||||
await expect(body).toContain('Gabi und Hannes Fotochallenge');
|
|
||||||
});
|
|
@ -1,37 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
import { validate, NIL } from 'uuid';
|
|
||||||
import { requestIdHeader } from '$lib';
|
|
||||||
|
|
||||||
test('response contains request-id header', async ({ playwright }) => {
|
|
||||||
const context = await playwright.request.newContext();
|
|
||||||
const response = await context.get('health');
|
|
||||||
const headers = response.headers();
|
|
||||||
await expect(headers).toHaveProperty(requestIdHeader);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('request-id is valid uuid', async ({ playwright }) => {
|
|
||||||
const context = await playwright.request.newContext();
|
|
||||||
const response = await context.get('health');
|
|
||||||
const headers = response.headers();
|
|
||||||
const requestId = headers[requestIdHeader];
|
|
||||||
await expect(validate(requestId)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('reuse valid incoming uuid', async ({ playwright }) => {
|
|
||||||
const context = await playwright.request.newContext();
|
|
||||||
|
|
||||||
const response = await context.get('health', { headers: { [requestIdHeader]: NIL } });
|
|
||||||
const headers = response.headers();
|
|
||||||
const requestId = headers[requestIdHeader];
|
|
||||||
await expect(requestId).toBe(NIL);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ignore invalid incoming uuid', async ({ playwright }) => {
|
|
||||||
const invalid = '00000000-0000-0000-0000-00000000000z';
|
|
||||||
const context = await playwright.request.newContext();
|
|
||||||
|
|
||||||
const response = await context.get('health', { headers: { [requestIdHeader]: invalid } });
|
|
||||||
const headers = response.headers();
|
|
||||||
const requestId = headers[requestIdHeader];
|
|
||||||
await expect(requestId).not.toBe(invalid);
|
|
||||||
});
|
|
Reference in New Issue
Block a user