wrolpi/app/src/test-fixtures.js
lrnselfreliance 93d878952c
Add a Repos module that mirrors git repositories (#783)
Repos keeps local mirrors of git repositories, cloned over https and kept up to date on a schedule.

- A Repo is a Collection (kind='repo') plus a `repository` row.  The new `git` downloader clones it, then updates it
  (weekly by default, or Never: the clone is kept as it is and only updated on request).
- Repos are saved in `repos/<tag>/<name>` (the Repos Directory, ignored by default) or a directory the user chooses.
  An existing clone can be imported where it is.  repos.yaml is the source of truth.
- A GitHub-like page shows each repo's README, files, and history, with a ZIP download.  READMEs are searchable
  under the global search's Other tab.
- git is hardened against hostile repos and configs: https only, pinned addresses with no private addresses (for the
  repo and its submodules), sanitized configs, no hooks or links, and only clones WROLPi made or imported are
  updated.
- Tagging a Repo moves its clone as one directory.  Repo changes wait for repos.yaml to be imported at startup.
- The file mover groups unindexed files with no primary file as one FileGroup, and never replaces a file at the
  destination.  A failed tag move restores the Collection's tag.  A Collection's directory cannot leave the media
  directory.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:16:58 -06:00

250 lines
8.9 KiB
JavaScript

/**
* Shared test fixtures: the data and context values the app expects, in one place.
*
* Two problems this exists to solve.
*
* The first is duplication. A settings object was written out by hand in every spec that
* needed one, twenty-odd keys at a time, so a spec asserting one field still had to know and
* restate the other nineteen. Nothing kept those copies in step with each other or with the
* API, and adding a required field meant hunting them down.
*
* The second is drift, which is the worse of the two. A fixture that no longer matches the
* shape it stands in for makes a test *confidently* wrong: it passes while the component
* would break in the browser. The theme context fixture carried `i`, `s` and `t` with
* hardcoded greys for months after ThemeProvider stopped supplying them, and one
* component was still reading a prop that no longer existed. test-fixtures.test.js compares
* these against the real contexts so that class of drift fails a test rather than surviving.
*
* Everything here is a function taking overrides, never an exported constant: a shared mutable
* object lets one test change what the next one sees, and that failure is miserable to find.
*
* Importable from jest tests, Cypress component specs, and e2e `cy.intercept` bodies alike --
* the same fixture in all three, so they cannot disagree about what the API returns.
*/
import {defaultTheme} from './themes/names';
/* --------------------------------------------------------------------------- */
/* API shapes */
/* --------------------------------------------------------------------------- */
/*
* GET /api/settings and GET /api/status.
*
* Every field each endpoint returns, and nothing it does not. Held to the API's own OpenAPI
* spec by wrolpi/test/test_api_fixtures.py, which is where drift now surfaces: seven settings
* fields were missing from this file, and the status fixture had two -- `cpu_percent` and
* `memory_percent` -- that no endpoint has ever returned and no component has ever read. They
* came from a hand-written cypress body and had been copied onwards since.
*
* A missing field is invisible in the frontend suite: the component reads `undefined` from a
* body the real API would have filled in, and every assertion about everything else still
* passes.
*/
/** GET /api/settings */
export const settingsFixture = (overrides = {}) => ({
archive_destination: 'archive/%(domain_tag)s/%(domain)s',
check_for_upgrades: true,
download_daily_limit_global: null,
download_daily_limit_per_domain: null,
download_manager_disabled: false,
download_manager_stopped: false,
download_on_startup: true,
download_timeout: 0,
download_wait: 20,
download_window_start: null,
download_window_end: null,
hotspot_device: 'wlan0',
hotspot_on_startup: true,
hotspot_password: 'wrolpi hotspot',
hotspot_ssid: 'WROLPi',
ignore_outdated_zims: false,
// Relative to the media directory, as the API returns them.
ignored_directories: [],
// Python logging integers: 40 error, 30 warning, 20 info, 10 debug, 5 trace.
log_level: 20,
map_default_location: null,
map_destination: 'map',
// `/media/wrolpi` on a Pi, but the API is the authority on this and a test that
// hardcodes it elsewhere is asserting its own guess.
media_directory: '/media/wrolpi',
nav_color: 'violet',
playlists_destination: 'playlists',
repos_destination: 'repos/%(repo_tag)s/%(repo_name)s',
require_cookies_unlocked: false,
require_media_mounted: false,
save_ffprobe_json: true,
tags_directory: true,
throttle_on_startup: false,
timezone: null,
version: '1.0.0',
videos_destination: 'videos/%(channel_tag)s/%(channel_name)s',
wrol_mode: false,
zims_destination: 'zims',
...overrides,
});
/**
* GET /api/status
*
* The stats are empty and the counters idle: a healthy machine with nothing happening, which is
* the state most specs want as a starting point. A spec testing a warning threshold or an
* available upgrade overrides the one field it is about.
*/
export const statusFixture = (overrides = {}) => ({
cpu_stats: {
cores: 4,
cur_frequency: 1500,
max_frequency: 1800,
min_frequency: 600,
percent: 10,
temperature: 45,
high_temperature: 75,
critical_temperature: 85,
},
dockerized: false,
downloads: {
pending: 0,
recurring: 0,
disabled: false,
stopped: false,
outside_download_window: false,
daily_limit_reached: false,
},
drives_stats: [],
flags: {},
is_rpi: false,
is_rpi4: false,
is_rpi5: false,
load_stats: {minute_1: 0.1, minute_5: 0.1, minute_15: 0.1},
local_time: '2026-07-30T12:00:00-06:00',
memory_stats: {total: 4000000000, used: 1000000000, free: 3000000000, cached: 500000000},
processes_stats: [],
sanic_workers: {},
version: '1.0.0',
wrol_mode: false,
// Upgrade info, seeded by the API and refreshed by its upgrade check.
commits_behind: 0,
current_commit: null,
git_branch: null,
latest_commit: null,
update_available: false,
...overrides,
});
/** A Domain Collection, as the domains endpoints return it. */
export const domainFixture = (overrides = {}) => ({
id: 1,
domain: 'example.com',
archive_count: 42,
size: 1024000,
tag_name: null,
directory: '',
can_be_tagged: false,
description: '',
...overrides,
});
export const domainsFixture = (count = 3) => Array.from({length: count}, (_, i) => domainFixture({
id: i + 1,
domain: `example${i + 1}.com`,
archive_count: (i + 1) * 10,
size: (i + 1) * 1000000,
}));
export const tagFixture = (overrides = {}) => ({
id: 1,
name: 'Favorite',
color: '#gray',
file_group_count: 0,
zim_entry_count: 0,
channel_count: 0,
domain_count: 0,
...overrides,
});
/* --------------------------------------------------------------------------- */
/* Context values */
/* --------------------------------------------------------------------------- */
/*
* These are the values a provider carries, not mocks of a provider. Passing one to the real
* `Context.Provider` runs the real consumers against real data, which is why none of the
* components using them need `jest.mock` -- see test-utils' render().
*/
export const themeContextFixture = (overrides = {}) => ({
theme: defaultTheme,
savedTheme: null,
isDark: false,
// Media filtering is off unless a test asks for it; the themes that offer a filter
// decide their own default, and a fixture should not pre-empt that.
mediaFilter: undefined,
mediaFilterEnabled: false,
setMediaFilterEnabled: () => {},
setTheme: () => {},
setDarkTheme: () => {},
setLightTheme: () => {},
cycleSavedTheme: () => {},
...overrides,
});
// `loaded` says the first poll has landed; the fixtures carry real values, so they are loaded.
export const statusContextFixture = (overrides = {}) => ({
status: statusFixture(),
fetchStatus: () => Promise.resolve(),
loaded: true,
...overrides,
});
export const settingsContextFixture = (overrides = {}) => ({
settings: settingsFixture(),
fetchSettings: () => Promise.resolve(),
saveSettings: () => Promise.resolve(),
pending: false,
loaded: true,
failed: false,
...overrides,
});
export const queryContextFixture = (overrides = {}) => ({
searchParams: new URLSearchParams(),
setSearchParams: () => {},
updateQuery: () => {},
getLocationStr: () => '',
...overrides,
});
/**
* The value `useTags` returns.
*
* The render helpers supply the real `TagsProvider` when a test does not pass tags, so this
* is for the cases that need particular tags without an API round trip. The component
* members are rendered as plain text: a test asserting on tag chips wants to find the name,
* not to re-test how Tags.js draws one.
*/
export const tagsContextFixture = (overrides = {}) => {
const tags = overrides.tags ?? [];
const findTagByName = (name) => tags.find(tag => tag.name === name);
return {
tags,
tagNames: tags.map(tag => tag.name),
NameToTagLabel: ({name}) => name ?? null,
TagsGroup: ({tagNames: names}) => (names || []).join(', '),
TagsLinkGroup: ({tagNames: names}) => (names || []).join(', '),
fetchTags: () => Promise.resolve(),
findTagByName,
SingleTag: ({name}) => name ?? null,
fuzzyMatchTagsByName: (name) => tags.filter(tag => tag.name.includes(name)),
...overrides,
};
};
/** The value FileWorkerStatusContext carries; no file worker is running by default. */
export const fileWorkerStatusFixture = (overrides = {}) => ({
status: null,
error: null,
refresh: () => Promise.resolve(),
setFastPolling: () => {},
...overrides,
});