auth-branding-logo.test.ts99 lines · main
1import { describe, expect, it } from 'bun:test';
2
3import { ValidationError } from '@briven/shared';
4
5import {
6 ALLOWED_LOGO_TYPES,
7 LOGO_MAX_BYTES,
8 brandingLogoPublicUrl,
9 sniffLogoContentType,
10 validateLogoUpload,
11} from './auth-branding-logo.js';
12
13describe('validateLogoUpload', () => {
14 it('accepts every allowed content-type at a sane size', () => {
15 for (const ct of ALLOWED_LOGO_TYPES) {
16 expect(() => validateLogoUpload({ contentType: ct, size: 1024 })).not.toThrow();
17 }
18 });
19
20 it('accepts svg with a charset suffix', () => {
21 expect(() =>
22 validateLogoUpload({ contentType: 'image/svg+xml; charset=utf-8', size: 512 }),
23 ).not.toThrow();
24 });
25
26 it('is case-insensitive on the media type', () => {
27 expect(() => validateLogoUpload({ contentType: 'IMAGE/PNG', size: 512 })).not.toThrow();
28 });
29
30 it('accepts exactly the cap', () => {
31 expect(() =>
32 validateLogoUpload({ contentType: 'image/png', size: LOGO_MAX_BYTES }),
33 ).not.toThrow();
34 });
35
36 it('rejects a disallowed content-type', () => {
37 expect(() => validateLogoUpload({ contentType: 'image/gif', size: 512 })).toThrow(
38 ValidationError,
39 );
40 expect(() => validateLogoUpload({ contentType: 'application/pdf', size: 512 })).toThrow(
41 ValidationError,
42 );
43 expect(() => validateLogoUpload({ contentType: 'text/html', size: 512 })).toThrow(
44 ValidationError,
45 );
46 });
47
48 it('rejects an empty content-type', () => {
49 expect(() => validateLogoUpload({ contentType: '', size: 512 })).toThrow(ValidationError);
50 });
51
52 it('rejects an over-cap file', () => {
53 expect(() =>
54 validateLogoUpload({ contentType: 'image/png', size: LOGO_MAX_BYTES + 1 }),
55 ).toThrow(ValidationError);
56 });
57
58 it('rejects an empty / non-positive file', () => {
59 expect(() => validateLogoUpload({ contentType: 'image/png', size: 0 })).toThrow(
60 ValidationError,
61 );
62 expect(() => validateLogoUpload({ contentType: 'image/png', size: -5 })).toThrow(
63 ValidationError,
64 );
65 expect(() => validateLogoUpload({ contentType: 'image/png', size: Number.NaN })).toThrow(
66 ValidationError,
67 );
68 });
69});
70
71describe('brandingLogoPublicUrl', () => {
72 it('points at the public serve route and is cache-busted', () => {
73 const url = brandingLogoPublicUrl('p_abc123');
74 expect(url).toContain('/v1/projects/p_abc123/auth/branding/logo');
75 expect(url).toMatch(/\?v=\d+$/);
76 });
77});
78
79describe('sniffLogoContentType', () => {
80 it('keeps a trusted header type', () => {
81 expect(sniffLogoContentType(new Uint8Array([1, 2, 3]), 'image/png')).toBe(
82 'image/png',
83 );
84 });
85
86 it('sniffs png magic when header is missing', () => {
87 const png = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
88 expect(sniffLogoContentType(png, 'application/octet-stream')).toBe(
89 'image/png',
90 );
91 });
92
93 it('sniffs svg text when header is wrong', () => {
94 const svg = new TextEncoder().encode(
95 '<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg"></svg>',
96 );
97 expect(sniffLogoContentType(svg, null)).toBe('image/svg+xml');
98 });
99});