Files
av-parser/tests/AvParser.Infrastructure.Tests/Media/ShowcaseNameTests.cs
T
Leonid PershinandClaude Opus 5 181f974a37 Add the media store: content-addressed blobs, SQLite index, showcase
First half of replacing the stub text domain with a media collector. Nothing
references this yet - the store is standalone and fully tested before anything
depends on it.

Blobs are addressed by SHA-256 and sharded two levels deep, so the same picture
re-uploaded at a dozen addresses costs one file. Downloads stage in a sibling
temp directory on the same volume and are promoted by rename, which is what
keeps blobs/ free of truncated files: a crash leaves a stray .part that the next
startup sweeps, never a half-image indistinguishable from a real one.

The SQLite index holds provenance separately from content, so purging one source
leaves blobs another source still references - that is what ref_count buys, and
it is recomputed rather than incremented because the item upsert can replace a
row pointing at a different blob. The seen_url journal deliberately outlives a
purge: without that, the next run downloads again exactly what the user just
deleted. Terminal outcomes are split from retryable ones so a flaky network does
not permanently lose content.

The showcase gives every item a dated, named path via hard links - a second name
for one file, not a second file. Hard links are a filesystem privilege rather
than a guarantee, so it degrades to copying and records which it achieved; the
UI has to be able to admit that. Names suggested by the origin are treated as
hostile: only the last path segment survives, Windows device names are pushed
aside, and the extension comes from the sniffed kind, never from the remote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 20:46:21 +03:00

77 lines
2.4 KiB
C#

using AvParser.Infrastructure.Media;
namespace AvParser.Infrastructure.Tests.Media;
public class ShowcaseNameTests
{
private const string Hash = "a3f19b7c5d2e8f0142536475869708192a3b4c5d6e7f8091a2b3c4d5e6f70819";
[Fact]
public void A_plain_name_is_kept()
{
ShowcaseLinker.SanitiseName("kitten", Hash).ShouldBe("kitten");
}
[Fact]
public void An_absent_name_falls_back_to_the_hash()
{
ShowcaseLinker.SanitiseName(null, Hash).ShouldBe(Hash[..12]);
ShowcaseLinker.SanitiseName(" ", Hash).ShouldBe(Hash[..12]);
}
[Theory]
[InlineData("../../etc/passwd")]
[InlineData("..\\..\\windows\\system32\\config")]
[InlineData("/absolute/path/name")]
public void A_name_cannot_walk_out_of_its_directory(string suggested)
{
// The suggestion comes from a remote server, so it is treated as hostile rather than
// merely untidy: only the last segment survives, and separators never do.
var name = ShowcaseLinker.SanitiseName(suggested, Hash);
name.ShouldNotContain("/");
name.ShouldNotContain("\\");
name.ShouldNotContain("..");
}
[Fact]
public void The_extension_the_origin_suggested_is_dropped()
{
// The extension is derived from the sniffed kind, never from what the origin claimed.
ShowcaseLinker.SanitiseName("photo.exe", Hash).ShouldBe("photo");
}
[Theory]
[InlineData("CON")]
[InlineData("nul")]
[InlineData("COM1")]
[InlineData("lpt9")]
public void Windows_device_names_are_pushed_out_of_the_way(string reserved)
{
ShowcaseLinker.SanitiseName(reserved, Hash).ShouldBe($"_{reserved}");
}
[Fact]
public void An_over_long_name_is_trimmed()
{
var name = ShowcaseLinker.SanitiseName(new string('x', 400), Hash);
name.Length.ShouldBeLessThanOrEqualTo(80);
}
[Fact]
public void A_name_of_nothing_but_junk_falls_back_to_the_hash()
{
// Dots rather than, say, '?': the set of invalid filename characters differs by platform,
// but a name that trims away to nothing does so everywhere.
ShowcaseLinker.SanitiseName("...", Hash).ShouldBe(Hash[..12]);
ShowcaseLinker.SanitiseName("\t\t", Hash).ShouldBe(Hash[..12]);
}
[Fact]
public void Control_characters_are_replaced_but_spaces_survive()
{
ShowcaseLinker.SanitiseName("a b\tc", Hash).ShouldBe("a b_c");
}
}