Files
splatoon3.ink/app/sync/R2Syncer.test.mjs
Matt Isenhower 70e81bbc38
Some checks failed
Deploy / deploy-frontend (push) Has been cancelled
Deploy / deploy-backend (push) Has been cancelled
Generate canonical asset URLs upstream
2026-08-30 10:20:12 -07:00

198 lines
6.5 KiB
JavaScript

import path from 'node:path';
import os from 'node:os';
import fs from 'node:fs/promises';
import { ListObjectsV2Command, PutObjectCommand } from '@aws-sdk/client-s3';
import { S3SyncClient } from 's3-sync-client';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import R2Syncer from './R2Syncer.mjs';
class FakeSyncClient
{
calls = [];
async sync(source, target, options) {
this.calls.push({ options, source, target });
return { created: [], deleted: [], updated: [] };
}
}
class FakeS3Client
{
uploads = [];
async send(command) {
if (command instanceof ListObjectsV2Command) {
return { Contents: [], IsTruncated: false };
}
if (command instanceof PutObjectCommand) {
this.uploads.push({
...command.input,
Body: await streamText(command.input.Body),
});
return {};
}
throw new Error(`Unexpected command: ${command.constructor.name}`);
}
}
function isIncluded(filters, key) {
let excluded = false;
for (let filter of filters) {
if (!excluded && filter.exclude) {
excluded = filter.exclude(key);
}
if (excluded && filter.include) {
excluded = !filter.include(key);
}
}
return !excluded;
}
async function streamText(stream) {
if (Buffer.isBuffer(stream)) {
return stream.toString();
}
let chunks = [];
for await (let chunk of stream) {
chunks.push(Buffer.from(chunk));
}
return Buffer.concat(chunks).toString();
}
describe('R2Syncer', () => {
let temporaryDirectories = [];
let config;
beforeEach(() => {
temporaryDirectories = [];
config = {
bucket: 'splatoon3-ink-assets',
};
});
afterEach(() => {
return Promise.all(temporaryDirectories.map(directory => fs.rm(directory, {
force: true,
recursive: true,
})));
});
it('uploads only public data and generated images with canonical R2 keys', async () => {
let syncClient = new FakeSyncClient;
let syncer = new R2Syncer({ config, syncClient });
await syncer.upload();
expect(syncClient.calls).toHaveLength(1);
let call = syncClient.calls[0];
expect(call.source).toBe(path.resolve('dist'));
expect(call.target).toBe('s3://splatoon3-ink-assets');
expect(call.options).not.toHaveProperty('partSize');
expect(isIncluded(call.options.filters, 'data/schedules.json')).toBe(true);
expect(isIncluded(call.options.filters, 'data/festivals.US.ics')).toBe(true);
expect(isIncluded(call.options.filters, 'data/archive/old.json')).toBe(false);
expect(isIncluded(call.options.filters, 'data/index.html')).toBe(false);
expect(isIncluded(call.options.filters, 'assets/splatnet/v3/stage.png')).toBe(true);
expect(isIncluded(call.options.filters, 'assets/splatnet/.DS_Store')).toBe(false);
expect(isIncluded(call.options.filters, 'assets/main.js')).toBe(false);
expect(isIncluded(call.options.filters, 'status-screenshots/schedules.png')).toBe(true);
expect(isIncluded(call.options.filters, 'status-screenshots/index.html')).toBe(false);
expect(call.options.relocations[0]('assets/splatnet/v3/stage.png'))
.toBe('splatnet/v3/stage.png');
expect(call.options.relocations[0]('data/schedules.json'))
.toBe('data/schedules.json');
});
it('does not transform public data upload bodies', async () => {
let localPath = await fs.mkdtemp(path.join(os.tmpdir(), 'r2-syncer-'));
temporaryDirectories.push(localPath);
let jsonPath = path.join(localPath, 'schedules.json');
await fs.writeFile(
jsonPath,
'{"image":"https://assets.splatoon3.ink/splatnet/one.png"}',
);
let syncClient = new FakeSyncClient;
let syncer = new R2Syncer({ config, syncClient });
await syncer.upload();
let commandInput = syncClient.calls[0].options.commandInput;
let input = {
Body: { path: jsonPath },
ContentLength: 61,
Key: 'data/schedules.json',
};
let result = commandInput(input);
expect(result.Body).toBeUndefined();
expect(result.ContentLength).toBeUndefined();
expect(result).toMatchObject({
CacheControl: 'no-cache, stale-while-revalidate=5, stale-if-error=86400',
ContentType: 'application/json',
});
expect(result).not.toHaveProperty('ACL');
});
it('does not rewrite image upload bodies', async () => {
let syncClient = new FakeSyncClient;
let syncer = new R2Syncer({ config, syncClient });
await syncer.upload();
let commandInput = syncClient.calls[0].options.commandInput;
let body = { path: '/unused/image.png' };
let result = commandInput({
Body: body,
ContentLength: 5,
Key: 'splatnet/stage.png',
});
expect(result.Body).toBeUndefined();
expect(result.ContentType).toBe('image/png');
});
it('uploads canonical data and relocated images through the real sync client', async () => {
let localPath = await fs.mkdtemp(path.join(os.tmpdir(), 'r2-syncer-'));
temporaryDirectories.push(localPath);
await fs.mkdir(path.join(localPath, 'data'), { recursive: true });
await fs.mkdir(path.join(localPath, 'assets/splatnet'), { recursive: true });
await fs.writeFile(
path.join(localPath, 'data/schedules.json'),
'{"image":"https://assets.splatoon3.ink/splatnet/stage.png"}',
);
await fs.writeFile(
path.join(localPath, 'data/festivals.US.ics'),
'URL:https://splatoon3.ink\r\n'
+ 'ATTACH:https://assets.splatoon3.ink/splatnet/fest.png\r\n',
);
await fs.writeFile(path.join(localPath, 'assets/splatnet/stage.png'), 'image');
await fs.writeFile(path.join(localPath, 'assets/main.js'), 'static site');
let s3Client = new FakeS3Client;
let syncClient = new S3SyncClient({ client: s3Client });
let syncer = new R2Syncer({ config, localPath, s3Client, syncClient });
await syncer.upload();
expect(s3Client.uploads.map(upload => upload.Key).sort()).toEqual([
'data/festivals.US.ics',
'data/schedules.json',
'splatnet/stage.png',
]);
let json = s3Client.uploads.find(upload => upload.Key === 'data/schedules.json');
expect(json.Body).toBe(
'{"image":"https://assets.splatoon3.ink/splatnet/stage.png"}',
);
expect(json.ContentLength).toBe(Buffer.byteLength(json.Body));
let calendar = s3Client.uploads.find(upload => upload.Key === 'data/festivals.US.ics');
expect(calendar.Body).toBe(
'URL:https://splatoon3.ink\r\n'
+ 'ATTACH:https://assets.splatoon3.ink/splatnet/fest.png\r\n',
);
expect(calendar.ContentLength).toBe(Buffer.byteLength(calendar.Body));
expect(calendar.ContentType).toBe('text/calendar');
});
});