PD Guard: модуль безопасности персональных данных

This commit is contained in:
Onbehalfofme
2026-09-22 17:43:33 +03:00
commit 41052fac6a
80 changed files with 91230 additions and 0 deletions
@@ -0,0 +1,87 @@
package ru.pdguard.detect;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Денилист известных людей: встроенный список и дозагрузка сверху без пересборки. */
class NameDictionaryTest {
/** Возвращает состояние словаря к встроенному списку — не течёт в остальные тесты. */
@AfterEach
void resetToBundledList() {
NameDictionary.useExternalFile(Path.of("config/well-known.txt"));
}
@Test
void bundledListCoversClassicFigures() {
// Сравнение по началу слова покрывает обычную русскую флексию («Пушкин» →
// «Пушкина»), но не прилагательное склонение («Толстой» → «Толстого», где
// «-ой» меняется на «-ого» целиком, а не дописывается) — известное
// ограничение самого приёма, не завязанное на список имён.
assertTrue(NameDictionary.isWellKnown("Стихи Пушкина"));
assertTrue(NameDictionary.isWellKnown("Портрет Толстой"));
}
@Test
void bundledListCoversCurrentPublicFigures() {
assertTrue(NameDictionary.isWellKnown("Заявление Набиуллиной"));
assertTrue(NameDictionary.isWellKnown("Интервью Путина"));
}
@Test
void unknownSurnameIsNotWellKnown() {
assertFalse(NameDictionary.isWellKnown("Заявление Смирнова"));
}
/**
* Фамилии, в честь которых чаще всего называют улицы в России (Росреестр).
* Само по себе «улица Ленина» никогда не попало бы под ФИО — для этого
* правила нужны два слова, — но денилист должен покрывать и составные
* названия («Феликса Дзержинского»), и вариации написания («Будённый»/
* «Буденный»).
*/
@Test
void bundledListCoversCommemorativeStreetNames() {
assertTrue(NameDictionary.isWellKnown("улица Кирова"));
// «-ский» склоняется целиком («Дзержинский» → «Дзержинского»), для таких
// основа обрезается сразу до «ск» — см. Declension.
assertTrue(NameDictionary.isWellKnown("проспект Дзержинского"));
assertTrue(NameDictionary.isWellKnown("улица Донского"));
// «Будённый» — чистое прилагательное без «-ский» (как «Толстой»): та же
// известная граница приёма, родительный падеж («Будённого») им не ловится.
assertTrue(NameDictionary.isWellKnown("улица Будённый"));
assertTrue(NameDictionary.isWellKnown("улица Буденный"), "написание без «ё» тоже должно ловиться");
assertTrue(NameDictionary.isWellKnown("улица Жукова"));
assertTrue(NameDictionary.isWellKnown("улица Островского"));
}
@Test
void externalFileAddsNamesWithoutRebuild(@TempDir Path dir) throws IOException {
Path file = dir.resolve("well-known.txt");
Files.writeString(file, "Кастомов\n", StandardCharsets.UTF_8);
NameDictionary.useExternalFile(file);
assertTrue(NameDictionary.isWellKnown("Интервью Кастомова"),
"дописанное сверху имя должно распознаваться наравне со встроенными");
assertTrue(NameDictionary.isWellKnown("Стихи Пушкина"),
"встроенный список не должен теряться при дозагрузке");
}
@Test
void missingExternalFileFallsBackToBundledListOnly(@TempDir Path dir) {
NameDictionary.useExternalFile(dir.resolve("нет-такого-файла.txt"));
assertTrue(NameDictionary.isWellKnown("Стихи Пушкина"));
assertFalse(NameDictionary.isWellKnown("Заявление Смирнова"));
}
}
@@ -0,0 +1,62 @@
package ru.pdguard.detect;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Словарь населённых пунктов — не только города, но и сёла, посёлки, деревни, хутора. */
class ToponymDictionaryTest {
@Test
void recognisesNominativeCase() {
assertTrue(ToponymDictionary.isKnownSettlement("Москва"));
assertTrue(ToponymDictionary.isKnownSettlement("Казань"));
assertTrue(ToponymDictionary.isKnownSettlement("Санкт-Петербург"));
}
@Test
void recognisesInflectedForms() {
assertTrue(ToponymDictionary.isKnownSettlement("Москве"), "дательный падеж города на гласную");
assertTrue(ToponymDictionary.isKnownSettlement("Тамбове"), "предложный падеж города на согласную");
assertTrue(ToponymDictionary.isKnownSettlement("Казани"), "родительный падеж");
}
@Test
void recognisesCisCapitals() {
assertTrue(ToponymDictionary.isKnownSettlement("Минск"));
assertTrue(ToponymDictionary.isKnownSettlement("Алматы"));
}
@Test
void rejectsMadeUpWord() {
assertFalse(ToponymDictionary.isKnownSettlement("Ерунда"));
assertFalse(ToponymDictionary.isKnownSettlement("Бла-бла"));
}
@Test
void isCaseInsensitive() {
assertTrue(ToponymDictionary.isKnownSettlement("МОСКВА"));
assertTrue(ToponymDictionary.isKnownSettlement("москва"));
}
/**
* Из переписи 2020–2021, не из ручного списка городов: сёла, посёлки,
* деревни, хутора, станицы, аулы, аалы — ровно то, чего не было, пока
* словарь ограничивался официальными городами. Примеры из ТЗ («рп.
* Ильинское», «с. Кукуево») и по одному реальному названию на тип
* населённого пункта.
*/
@Test
void recognisesSettlementsFromCensusNotJustOfficialCities() {
assertTrue(ToponymDictionary.isKnownSettlement("Ильинское"), "рп. Ильинское — пример из ТЗ");
assertTrue(ToponymDictionary.isKnownSettlement("Кукуево"), "с. Кукуево — пример из ТЗ");
assertTrue(ToponymDictionary.isKnownSettlement("Прогресс"), "хутор");
assertTrue(ToponymDictionary.isKnownSettlement("Гиагинская"), "станица");
assertTrue(ToponymDictionary.isKnownSettlement("Блечепсин"), "аул");
assertTrue(ToponymDictionary.isKnownSettlement("Энем"), "посёлок городского типа");
assertTrue(ToponymDictionary.isKnownSettlement("Аксеновка"), "деревня");
assertTrue(ToponymDictionary.isKnownSettlement("Екатериновка"), "слобода");
assertTrue(ToponymDictionary.isKnownSettlement("Сартыков"), "аал (Хакасия)");
}
}
@@ -0,0 +1,69 @@
package ru.pdguard.detect;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Контрольные суммы ОГРН/ОГРНИП: первые 12/14 цифр по модулю 11/13, младший разряд остатка. */
class ValidatorsTest {
@Test
void validOgrnPassesChecksum() {
assertTrue(Validators.ogrn("1027700123450"));
assertTrue(Validators.ogrn("1025000678900"));
assertTrue(Validators.ogrn("1045002233440"));
}
@Test
void invalidOgrnChecksumFails() {
assertFalse(Validators.ogrn("1027700123451"), "последняя цифра изменена — сумма не сходится");
}
@Test
void ogrnWrongLengthFails() {
assertFalse(Validators.ogrn("102770012345"), "12 цифр — не хватает контрольной");
assertFalse(Validators.ogrn("10277001234500"), "14 цифр — лишняя");
}
/** Остаток от деления на 11 может быть 10 — тогда контрольная цифра 0, не 10. */
@Test
void ogrnRemainderTenMapsToZero() {
assertTrue(Validators.ogrn("1000000000000"));
}
@Test
void validOgrnipPassesChecksum() {
assertTrue(Validators.ogrnip("304500116000157"));
assertTrue(Validators.ogrnip("312500000000013"));
assertTrue(Validators.ogrnip("305500112233041"));
}
@Test
void invalidOgrnipChecksumFails() {
assertFalse(Validators.ogrnip("304500116000158"), "последняя цифра изменена — сумма не сходится");
}
@Test
void ogrnipWrongLengthFails() {
assertFalse(Validators.ogrnip("30450011600015"), "14 цифр — не хватает контрольной");
assertFalse(Validators.ogrnip("30450011600015700"), "18 цифр — лишние");
}
/** Остаток от деления на 13 может быть 10 — тогда контрольная цифра 0, не 10. */
@Test
void ogrnipRemainderTenMapsToZero() {
assertTrue(Validators.ogrnip("100000000000000"));
}
@Test
void nonDigitCharactersAreIgnored() {
assertTrue(Validators.ogrn("10-27700-123450"), "разделители в номере не мешают счёту цифр");
}
@Test
void emptyValueIsNotValid() {
assertFalse(Validators.ogrn(""));
assertFalse(Validators.ogrnip(""));
}
}