using System.Linq; using AwesomeAssertions; using NUnit.Framework; using Soneta.Business; using Soneta.Core; using Soneta.HR; using Soneta.HR2; using Soneta.Kadry; using Soneta.Types; using Prac = Soneta.Kadry.Pracownik; namespace Soneta.Skills.Test.KadryPlace.Pracownik; /// /// Rozdział K (część druga) — RODO/GIODO, struktura organizacyjna, oceny, rekrutacja (receptury K6–K9). /// /// Testy to wykonywalna dokumentacja publicznego kontraktu platformy Soneta dla zaawansowanych /// obszarów kadrowych. Wszystkie te obszary łączy jedna cecha: rekordy operacyjne wymagają /// referencji do definicji konfiguracyjnych (słowników GIODO, struktury organizacyjnej, ocen, /// stanowisk/etapów rekrutacji), które w bazie Demo (GoldStandard) mogą nie istnieć. Strategia /// jest jednolita: definicję pobieramy dynamicznie (pierwszy rekord z tabeli konfiguracyjnej); gdy /// jej brak — test jest oznaczany Assert.Ignore z powodem. Tam, gdzie da się przetestować /// realnie (odczyt kolekcji, dodanie wpisu przy dostępnej definicji), robimy to na żywych danych. /// /// /// K6 — RODO/GIODO: new GIODOOświadczenie(pracownik, def), new GIODOUprawnienie(pracownik, def); /// kolekcje GIODOOświadczenia/GIODOUprawnienia/GIODOUdostępnienia; /// GIODOWymianaDanych bez publicznego ctora → tylko odczyt + [Ignore]; zapis teczki do pliku → [Ignore]. /// K7 — struktura organizacyjna: new PowiązanieStrukturyOrganizacyjnej(element, pracownik), /// Etat.Wydzial (dane historyczne), manager StrukturaOraganizacyjna (odczyt). /// K8 — oceny: new OcenaPracownika(pracownik) + new ElementOcenyPracownika(ocena), /// new CelOkresowyPracownika(pracownik). /// K9 — rekrutacja: new RekrutacjaAplikacja(pracownik, wydziałDefStanowiska), /// new Rekrutacja(pracownik), new EtapRekrutacji(rekrutacja). /// /// /// Operujemy wyłącznie na publicznym kontrakcie — jak dodatek programisty zewnętrznego bez /// dostępu do źródeł. Baza Demo z rollbackiem po teście (helper InTransaction z TestBase). /// /// [TestFixture] public class RozdzialK2_RodoZzlTest : PracownikTestBase { // Pracownik-host dla wpisów — dowolny etatowy z Demo (stabilny punkt wejścia). private Prac Host() => Pracownik(Pracownik_.Andrzejewski) ?? PierwszyPracownik(); // Pierwszy rekord z tabeli konfiguracyjnej (lub null) — bez twardej zależności od nazwy słownika. private static T Pierwsza(Table tabela) where T : Row => tabela.Cast().FirstOrDefault(); // ============================== K6 — RODO/GIODO ============================== [Test] [Description("K6: new GIODOOświadczenie(pracownik, definicja) — Host wynika z ctora (Pracownik implementuje " + "IGIODOOświadczenieHost), Definicja (GIODODefOswiadcz) jest WYMAGANA przez ctor; pole Data to Date; " + "Rodzaj/Okres są WYLICZANE (read-only) z definicji; wpis trafia do pracownik.GIODOOświadczenia. " + "Gdy w Demo brak definicji oświadczenia lub brak prawa zapisu do obszaru RODO → Ignore.")] public void K6_GIODOOswiadczenie_DodanieZDefinicja_TrafiaDoKolekcji() { // Tabela konfiguracyjna czytana wprost z sesji operacyjnej (jak słowniki w K1). var def = Pierwsza(Session.GetCore().GIODODefOswiadcz); if (def == null) Assert.Ignore("Brak definicji oświadczenia GIODO (CoreModule.GIODODefOswiadcz) w bazie Demo — wpisu nie można utworzyć (Definicja jest wymagana w ctorze)."); var pracownik = Host(); GIODOOświadczenie oswiadczenie = null; try { InTransaction(() => { // Definicja wynika z ctora; Rodzaj/Okres są wyliczane przez platformę — nie ustawiamy ich ręcznie. oswiadczenie = Session.AddRow(new GIODOOświadczenie(pracownik, def)); oswiadczenie.Data = Date.Today; oswiadczenie.SposobPozyskania = "Formularz papierowy"; }); } catch (AccessWriteDeniedException) { // Egzekucji praw nie testujemy (safe-code §7.2) — rola Demo blokuje zapis do obszaru RODO/GIODO. Assert.Ignore("Rola bazy Demo nie ma prawa zapisu do GIODOOświadczenie — egzekucji praw nie testujemy (safe-code §7.2)."); } oswiadczenie.Host.Should().Be(pracownik, "ctor (host, definicja) ustawia Host na pracownika"); oswiadczenie.Definicja.Should().Be(def, "Definicja przekazywana jest w ctorze"); oswiadczenie.Data.Should().Be(Date.Today); pracownik.GIODOOświadczenia.Cast() .Should().Contain(oswiadczenie, "wpis trafia do kolekcji SubTable pracownika"); } [Test] [Description("K6: new GIODOUprawnienie(pracownik, definicja) — Uprawniony z ctora (IGIODOUprawnienieHost), " + "Definicja (GIODODefUprawn) wymagana; pola Data/Przyznane/Odebrane to Date (Okres jest wyliczany, " + "read-only); wpis trafia do pracownik.GIODOUprawnienia. Gdy brak definicji w Demo → Ignore.")] public void K6_GIODOUprawnienie_DodanieZDefinicja_TrafiaDoKolekcji() { var def = Pierwsza(Session.GetCore().GIODODefUprawn); if (def == null) Assert.Ignore("Brak definicji uprawnienia GIODO (CoreModule.GIODODefUprawn) w bazie Demo — wpisu nie można utworzyć."); var pracownik = Host(); GIODOUprawnienie uprawnienie = null; try { InTransaction(() => { uprawnienie = Session.AddRow(new GIODOUprawnienie(pracownik, def)); uprawnienie.Data = Date.Today; uprawnienie.Przyznane = Date.Today; // Okres jest wyliczany — nie ustawiamy go bezpośrednio. }); } catch (AccessWriteDeniedException) { Assert.Ignore("Rola bazy Demo nie ma prawa zapisu do GIODOUprawnienie — egzekucji praw nie testujemy (safe-code §7.2)."); } uprawnienie.Uprawniony.Should().Be(pracownik, "ctor (uprawniony, definicja) ustawia Uprawniony"); uprawnienie.Definicja.Should().Be(def); uprawnienie.Przyznane.Should().Be(Date.Today); pracownik.GIODOUprawnienia.Cast().Should().Contain(uprawnienie); } [Test] [Description("K6: GIODOWymianaDanych (pozyskanie/udostępnienie/powierzenie) NIE ma publicznego ctora — " + "rekordy tworzą wyłącznie workery (DodajPozyskanieDanychWorker itd.). Kolekcja GIODOUdostępnienia " + "jest jednak dostępna do ODCZYTU jako część publicznego kontraktu.")] public void K6_GIODOUdostepnienia_KolekcjaDostepnaDoOdczytu() { var pracownik = Host(); // GIODOUdostępnienia to SubTable — odczyt jest częścią kontraktu, // nawet gdy w Demo nie ma żadnych zapisów wymiany danych. pracownik.GIODOUdostępnienia.Should().NotBeNull("kolekcja wymiany danych jest zawsze dostępna (odczyt)"); pracownik.GIODOUdostępnienia.Cast().Should().OnlyContain(w => w != null); } [Test] [Ignore("Dodanie GIODOWymianaDanych wymaga workera (DodajPozyskanieDanychWorker/DodajUdostępnienieDanychWorker/" + "DodajPowierzenieDanychWorker) oraz podmiotu (IKontrahent) i — w zależności od kierunku — definicji " + "dokumentu/zbioru danych z konfiguracji modułu RODO, których baza Demo może nie mieć. Brak publicznego " + "ctora uniemożliwia deterministyczny zapis bez tej konfiguracji.")] [Description("K6: dodanie zapisu wymiany danych GIODO przez DodajPozyskanieDanychWorker (CommitUI + Save).")] public void K6_GIODOWymianaDanych_DodaniePrzezWorker() { } [Test] [Ignore("Zapis teczki personalnej (Pracownik.ZapiszTeczkęDoPlikuWorker.ZapiszTeczkeDoPliku()) to operacja " + "plikowa — serializuje dokumentację pracownika do plików/katalogu na dysku. Poza zakresem testów " + "jednostkowych (zależność od systemu plików).")] [Description("K6: zapis teczki personalnej RODO do pliku (operacja plikowa).")] public void K6_ZapisTeczkiDoPliku() { } // ============================== K7 — Struktura organizacyjna ============================== [Test] [Description("K7: new PowiązanieStrukturyOrganizacyjnej(element, pracownik) — Zrodlo z ctora (Pracownik " + "implementuje IŹródłoPowiązaniaStrukturyOrganizacyjnej), Element to istniejący element struktury " + "(CoreModule.ElementyStrOrg — NIE definicja DefElStrukturOrg); Okres to FromTo; wpis trafia do " + "pracownik.PowiązaniaStrOrg. Gdy brak elementów struktury w Demo lub brak prawa zapisu → Ignore.")] public void K7_PowiazanieStruktury_DodanieZElementem_TrafiaDoKolekcji() { // Elementy struktury (instancje) są w ElementyStrOrg; DefElStrukturOrg trzyma DEFINICJE elementów. var element = Pierwsza(Session.GetCore().ElementyStrOrg); if (element == null) Assert.Ignore("Brak elementów struktury organizacyjnej (CoreModule.ElementyStrOrg) w bazie Demo — powiązania nie można utworzyć."); var pracownik = Host(); PowiązanieStrukturyOrganizacyjnej powiazanie = null; try { InTransaction(() => { powiazanie = Session.AddRow(new PowiązanieStrukturyOrganizacyjnej(element, pracownik)); powiazanie.Okres = new FromTo(Date.Today, Date.MaxValue); }); } catch (AccessWriteDeniedException) { Assert.Ignore("Rola bazy Demo nie ma prawa zapisu do PowiązanieStrukturyOrganizacyjnej — egzekucji praw nie testujemy (safe-code §7.2)."); } powiazanie.Zrodlo.Should().Be(pracownik, "ctor (element, zrodlo) ustawia Zrodlo na pracownika"); powiazanie.Element.Should().Be(element); pracownik.PowiązaniaStrOrg.Cast().Should().Contain(powiazanie); } [Test] [Description("K7: pracownik.StrukturaOraganizacyjna to manager (StrukturaOraganizacyjnaManager) — API tylko " + "do odczytu nawigacji przełożeni/podwładni. Jest zawsze dostępny, niezależnie od konfiguracji struktury.")] public void K7_StrukturaOrganizacyjna_ManagerOdczytuJestDostepny() { var pracownik = Host(); pracownik.StrukturaOraganizacyjna.Should().NotBeNull("manager struktury jest zawsze dostępny (odczyt)"); pracownik.StrukturaOraganizacyjna.Should().BeOfType(); // Przełożony „na dzień" może być null (brak skonfigurowanej struktury) — czytamy bez wyjątku. var _ = pracownik.StrukturaOraganizacyjna.GetDomyślnyPrzełożony(Date.Today); } [Test] [Description("K7: Etat.Wydzial to dane HISTORYCZNE (na PracHistoria.Etat) i jednostka organizacyjna pracownika. " + "Dla etatowego pracownika z Demo wydział na zapisie obowiązującym dziś jest ustawiony (wymagany dla etatu).")] public void K7_EtatWydzial_JestUstawionyDlaEtatowca() { var pracownik = Host(); var ph = pracownik[Date.Today]; // zapis historii obowiązujący na dzień (A15) ph.Should().NotBeNull("etatowy pracownik z Demo ma zapis historii obowiązujący dziś"); // Wydzial jest wymagany dla etatu — odczyt jako część kontraktu (referencja do Soneta.Kadry.Wydzial). ph.Etat.Should().NotBeNull(); ph.Etat.Wydzial.Should().NotBeNull("Etat.Wydzial (jednostka organizacyjna) jest wymagany dla etatu"); } // ============================== K8 — Oceny okresowe ============================== [Test] [Description("K8: new OcenaPracownika(pracownik) (arkusz, root w HR.OcenyPracownikow) + new ElementOcenyPracownika(ocena) " + "gdzie ocena jest IOcenaPracownika; ElementOcenyPracownika.Wartosc to decimal (Typ/Data są wyliczane, read-only). " + "Element wymaga Definicja (HR.DefElemOcenPrac) — gdy brak w Demo, sam arkusz i pusta kolekcja elementów wystarczają.")] public void K8_OcenaPracownika_ArkuszZElementem_TrafiaDoKolekcji() { var hr = Session.GetHR(); var pracownik = Host(); var defElementu = Pierwsza(hr.DefElemOcenPrac); OcenaPracownika ocena = null; ElementOcenyPracownika element = null; InTransaction(() => { ocena = Session.AddRow(new OcenaPracownika(pracownik)); ocena.Nazwa = "Ocena roczna 2026"; ocena.Data = Date.Today; // Element dodajemy tylko gdy istnieje definicja (Definicja jest wymagana do zapisu elementu). if (defElementu != null) { element = Session.AddRow(new ElementOcenyPracownika(ocena)); // ocena jako IOcenaPracownika element.Definicja = defElementu; element.Wartosc = 4m; // Wartosc to decimal (Typ/Data ustawia platforma na podstawie definicji) } }); ocena.Pracownik.Should().Be(pracownik, "ctor (Pracownik) ustawia ocenianego"); ocena.Nazwa.Should().Be("Ocena roczna 2026"); pracownik.Oceny.Cast().Should().Contain(ocena, "arkusz trafia do kolekcji pracownika"); if (defElementu != null) { element.Ocena.Should().Be(ocena, "ctor (IOcenaPracownika) wiąże element z arkuszem"); element.Wartosc.Should().Be(4m); ocena.ElementyOceny.Cast().Should().Contain(element); } else { Assert.Warn("Brak definicji elementu oceny (HR.DefElemOcenPrac) w Demo — przetestowano sam arkusz oceny bez pozycji."); } } [Test] [Description("K8: new CelOkresowyPracownika(pracownik) (root w HR2.CeleOkresowePrac); pola Nazwa/Data/Termin/Opis; " + "Definicja to Soneta.Oceny.DefinicjaElementuOceny (opcjonalna referencja konfiguracyjna); wpis trafia " + "do pracownik.CeleOkresowe.")] public void K8_CelOkresowy_Dodanie_TrafiaDoKolekcji() { var pracownik = Host(); CelOkresowyPracownika cel = null; InTransaction(() => { cel = Session.AddRow(new CelOkresowyPracownika(pracownik)); cel.Nazwa = "Wdrożenie nowego modułu"; cel.Data = Date.Today; cel.Termin = new Date(2026, 12, 31); cel.Opis = (MemoText)"Cel rozwojowy na bieżący okres oceny."; }); cel.Pracownik.Should().Be(pracownik, "ctor (Pracownik) ustawia pracownika celu"); cel.Nazwa.Should().Be("Wdrożenie nowego modułu"); cel.Termin.Should().Be(new Date(2026, 12, 31)); pracownik.CeleOkresowe.Cast().Should().Contain(cel); } // ============================== K9 — Rekrutacja ============================== [Test] [Description("K9: new RekrutacjaAplikacja(kandydat, wydziałDefStanowiska) — kandydat to Pracownik, ctor przyjmuje " + "WydziałDefinicjiStanowiska (powstaje z new WydziałDefinicjiStanowiska(DefinicjaStanowiska) — typ z Soneta.HR). " + "Stan to StanAplikacji; wpis trafia do kandydat.Aplikacje. Gdy brak definicji stanowiska (HR.DefStanowisk) → Ignore.")] public void K9_RekrutacjaAplikacja_DodanieZeStanowiskiem_TrafiaDoKolekcji() { var hr = Session.GetHR(); var defStanowiska = Pierwsza(hr.DefStanowisk); if (defStanowiska == null) Assert.Ignore("Brak definicji stanowiska (HR.DefStanowisk) w bazie Demo — aplikacji rekrutacyjnej nie można utworzyć (ctor wymaga WydziałDefinicjiStanowiska)."); var kandydat = Host(); RekrutacjaAplikacja aplikacja = null; InTransaction(() => { // WydziałDefinicjiStanowiska powstaje z DefinicjaStanowiska (ctor w Soneta.HR). var wydzialDef = new WydziałDefinicjiStanowiska(defStanowiska); aplikacja = Session.AddRow(new RekrutacjaAplikacja(kandydat, wydzialDef)); aplikacja.Data = Date.Today; aplikacja.Stan = StanAplikacji.Wprowadzona; }); aplikacja.Pracownik.Should().Be(kandydat, "ctor (Pracownik, …) ustawia kandydata"); aplikacja.Stanowisko.Should().Be(defStanowiska, "WydziałDefinicjiStanowiska niesie referencję do DefinicjaStanowiska"); aplikacja.Stan.Should().Be(StanAplikacji.Wprowadzona); kandydat.Aplikacje.Cast().Should().Contain(aplikacja); } [Test] [Description("K9: new Rekrutacja(kandydat) (root w HR.Rekrutacje; impl. IOcenaPracownika) ustawia pole Pracownik; " + "+ new EtapRekrutacji(rekrutacja) wiąże etap przez pole Rekrutacja; Etap.Definicja to HR.DefEtaRekrutacji " + "(wymagana do zapisu etapu), Etap.Lp/Data. Gdy brak definicji etapu w Demo, testujemy samą rekrutację (warn). " + "Gdy brak prawa zapisu → Ignore.")] public void K9_RekrutacjaIEtap_Dodanie_TrafiaDoKolekcji() { var hr = Session.GetHR(); var kandydat = Host(); var defEtapu = Pierwsza(hr.DefEtaRekrutacji); Rekrutacja rekrutacja = null; EtapRekrutacji etap = null; try { InTransaction(() => { rekrutacja = Session.AddRow(new Rekrutacja(kandydat)); if (defEtapu != null) { etap = Session.AddRow(new EtapRekrutacji(rekrutacja)); etap.Definicja = defEtapu; etap.Lp = 1; etap.Data = Date.Today; } }); } catch (AccessWriteDeniedException) { Assert.Ignore("Rola bazy Demo nie ma prawa zapisu do Rekrutacja/EtapRekrutacji — egzekucji praw nie testujemy (safe-code §7.2)."); } rekrutacja.Should().NotBeNull("ctor (Pracownik) tworzy rekrutację dla kandydata"); rekrutacja.Pracownik.Should().Be(kandydat, "ctor (Pracownik) ustawia kandydata rekrutacji"); // Rekrutacja jest rootem w HR.Rekrutacje (kolekcje na Pracowniku wiążą się przez relacje child). hr.Rekrutacje.Cast().Should().Contain(rekrutacja, "rekrutacja trafia do tabeli głównej HR.Rekrutacje"); if (defEtapu != null) { etap.Rekrutacja.Should().Be(rekrutacja, "ctor (Rekrutacja) wiąże etap z rekrutacją"); etap.Lp.Should().Be(1); hr.EtapyRekrutacji.Cast().Should().Contain(etap, "etap trafia do tabeli głównej HR.EtapyRekrutacji"); } else { Assert.Warn("Brak definicji etapu rekrutacji (HR.DefEtaRekrutacji) w Demo — przetestowano samą rekrutację bez etapów."); } } [Test] [Description("K9: kandydat.Aplikacje / Rekrutacje / EtapyRekrutacji / Kandydatury to kolekcje SubTable dostępne " + "do odczytu jako część publicznego kontraktu — niezależnie od stanu konfiguracji rekrutacji.")] public void K9_KolekcjeRekrutacji_DostepneDoOdczytu() { var kandydat = Host(); kandydat.Aplikacje.Should().NotBeNull(); kandydat.Rekrutacje.Should().NotBeNull(); kandydat.EtapyRekrutacji.Should().NotBeNull(); kandydat.Kandydatury.Should().NotBeNull(); } }