EFCore – testy na bazie w pamięci

Gdy korzystamy z EFCore i chcemy uruchamiać testy na bazie w pamięci, możemy do naszych klas wstrzykiwać DbContext in memory:

    private DarwinDbContext GetDbContext()
    {
        var builder = new DbContextOptionsBuilder<MyDbContext>();
        builder.UseInMemoryDatabase("SthName");
        var options = builder.Options;
        return new MyDbContext(options);
    }

Teraz możemy wpaść w pułapkę, że zapisy wykonane w jednym teście, będą widoczne w innym. Można temu zaradzić na dwa sposoby.

1. Na koniec każdego testu wykonać:

dbContext.Database.EnsureDeleted();

2. Dla każdego testu zwracać odseparowaną bazę:

builder.UseInMemoryDatabase(Guid.NewGuid().ToString());

Do tematu testowania i baz danych jeszcze wrócę 😉

ASP.NET – role użytkowników

Wykonanie akcji w kontrolerze przez nie zalogowanego użytkownika można zabezpieczyć dodając atrybut: [Authorize]. A jak pozwolić wykonać akcję tylko użytkownikowi ze specjalnymi uprawnieniami?

Można zadeklarować własny atrybut, który dziedziczy po AuthorizeAttribute i w nim dodać logikę sprawdzenia, czy dany użytkownik może daną akcję wykonać.

Zacznijmy od tego, że użytkownik należy do jakiejś roli. Można tu wykorzystać role wbudowane lub dodać własny mechanizm ról. Ten drugi przypadek jest kuszący, jeśli przewidujemy, że wbudowane role mogą nam w przyszłości nie wystarczyć. Np., gdy będziemy chcieli przypisywać role w ramach czasowych.

Zdefiniowałem enum.:

public enum Role
{
    adminOrganization = 1,
    accountant = 2
}

Kod mojego atrybutu:

    public class AuthorizeCustomAttribute : AuthorizeAttribute
    {
        private readonly List<Role> _allowedroles;
        public AuthorizeCustomAttribute(params Role[] roles)
        {
            _allowedroles = roles.ToList();
        }

        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
            if (httpContext.User == null)
                return false;

            return UserOrganizationRolesHelper.IsUserRole(httpContext.User.Identity.Name, _allowedroles);
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            filterContext.Result = new RedirectToRouteResult(
               new RouteValueDictionary
               {
                    { "controller", "Home" },
                    { "action", "UnAuthorized" }
               });
        }
    }

Z atrybutu korzystamy:

[AuthorizeCustom(Role.adminOrganization, Role.accountant)]
public class PlacesController : Controller

Logika sprawdzenia, czy użytkownik należy do roli jest związanaz moim rozwiązaniem trzymania przypisanych do użytkownika ról:

        public static bool IsUserRole(string userName, IList<Role> roles)
        {
            var userOrg = GetUserOrganization(userName);
            List<UserRole> userRoles = JsonConvert.DeserializeObject<List<UserRole>>(userOrg.Roles);
            return userRoles.Exists(x => roles.Contains(x.Role) && x.Active);
        }

Camp Manager – nie każdy może założyć konto?

We wczesnych wersjach aplikacji, każdy, kto znał adres strony, mógł założyć sobie konto i pooglądać dane. Ba, mógł je poedytować. Czyli mamy dwa problemy do rozwiązania:

  1. nie każdy powinien móc założyć konto
  2. uprawnienia dla użytkowników!

Tutaj zajmę się tym pierwszym.

Założenia:

  • pierwsze i najważniejsze, nie narobić się 🙂. Czyli nie wyrzucam ani nie wywracam do góry nogami istniejącego mechanizmu zakładania użytkownika. Dlatego:
  • dodaję stronę, gdzie użytkownik może wpisać maile, na które można zarejestrować nowego użytkownika
  • wpiąć się w istniejący mechanizm zakładania nowego użytkownika i nie pozwolić mu założyć konta, jeśli jego email nie został wcześniej zarejestrowany. Jest to standardowy mechanizm wygenerowany z szablonem projektu ASP.NET

W pierwszy kroku dodaję nowy model, który mógłby mieć tylko id i email. U mnie ma ciut więcej, a to już z myślą o uprawnieniach:

    public class UserEmailAllow
    {
        public int Id { get; set; }
        public string Email { get; set; }
        public string StartRoles { get; set; }
        public DateTime? DateExpire { get; set; }

        [Required]
        public Organization Organization { get; set; }
    }

I taką klasę dodałem do mojego DbContext.

Kontrolera i widoku nie pokazuję, to zwykły CRUD, czyli skorzystałem z generatora. Jedynie dodałem sprawdzenie przed zapisem:

            if (_db.Users.FirstOrDefault(x => x.Email == userEmailAllowViewModel.Email) != null)
            {
                ModelState.AddModelError("", "Email istnieje w zarejestrowanych użytkownikach");
                return View(userEmailAllowViewModel);
            }

            if (_db.UserEmailAllow.FirstOrDefault(x => x.Email == userEmailAllowViewModel.Email) != null)
            {
                ModelState.AddModelError("", "Email istnieje w oczekujących na rejestrację");
                return View(userEmailAllowViewModel);
            }

Pozostaje nam wpięcie w zakładanie nowego użytkownika. W kontrolerze: AccountController, jest metoda Register. Wystarczy dodać w niej poniższy kawałek kodu, tuż po sprawdzeniu czy model jest poprawny:

            ApplicationDbContext db = new ApplicationDbContext();
            var userAllow = db.UserEmailAllow.FirstOrDefault(x => x.Email == model.Email);
            if (userAllow == null)
            {
                AddErrors(new IdentityResult("Email nie zarejestrowany. Zgłoś się do administratora"));
                return View(model);
            }

Konferencja DNA Conf 2020

Jest taki koleś(*) Maciej Aniserowicz, który zamieszał w tamtym roku w świecie kursów online dla IT.  Kiedyś programista, teraz slowbusinessman(**).  Wydał książkę, potem zajął się głównie produkcją kursów online. Najpierw wyprodukował kurs sam (treść, marketing, sprzedaż), potem zajął się tylko marketingiem, sprzedażą, a treści tworzą inni specjaliści. W tamtym roku wypuścił bardzo zaawansowany, bardzo fajny kurs online razem z 3 wymiataczami (Łukasz Szydło, Jakub Kubryński i Jakub Pilimon). Sprzedał go za ponad 6 mln zł.

Pod koniec roku Maciek wymyślił, a trzej pozostali podchwycili i razem zorganizowali konferencję DNA Conf. W świecie IT konferencji jest na pęczki, ale ta była wyjątkowa. Zamiast sprzedać bilety, postanowili, że wesprą cel charytatywny. Trzeba było wpłacić minimum 500 zł na WOŚP, przesłać potwierdzenie przelewu i można było pojawić się 2020-02-28 w Warszawie. Konferencja była na 300 osób, więc chłopaki zebrali ponad 150 tys. na orkiestrę. Koszty konferencji (obiło mi się o uszy ok. 60 tys. zł) pokryli z własnej kieszeni, czterej wymienieni wyżej organizatorzy. Maćka zespół zaangażował się w organizację, trzej wymiatacze wystąpili z prezentacjami.

Konferencja była super, ale feedback to zostawię w ankiecie. Właściwie to chciałem tylko napisać: chapeau bas! Za zorganizowanie konferencji, za wsparcie WOŚP, za brak kija w d…, za to że Wam się chce.

(*) znamy się z Maćkiem parę(naście) lat, wiem, że za koleś się nie obrazi

(**) slowbusinessman bo woli wolno nie szybko: https://slowbiz.pl

Ror cz. 6 – flash[:notice]

Często w kodzie kontrolera widać użycie flash, np.:

flash[:notice] = "Author saved"

Powoduje to wyświetlenie komunikatu dla użytkownika przy najbliższym wyświetleniu jakiejkolwiek strony. I tylko przy najbliższym, czyli komunikat pokaże się raz.

Użycie flash można dodać jako parametr do redirect_to. Czyli zamiast linii:

flash[:notice] = "Author saved"
redirect_to authors_path

Można napisać w jednej lini:

redirect_to authors_path, notice: "Author saved"

Wyświetlenie komunikatu na stronie, musi być uwzględnione w kodzie widoku. Najlepiej jak mamy to zapisane w: /views/layouts/application.html.erb. Np. tak:

<% if flash[:notice].present? %>
  <div class="alert alert-success">
    <%= flash[:notice] %>
  </div>
<% end %>

Nie jest to najlepszy kawałek do obsługi komunikatów, bo zamiast notice, możemy użyć też alert, czy nawet abc. I tu uwaga, możemy napisać:

flash[:abc] = "message"

Ale w redirect_to, możemy użyć tylko notice i alert. Żeby móc używać nasze abc w redirect_to, trzeba w /controllers/application_controller.rb dodać linię:

add_flash_types(:abc)

MacOs i .NET Core

Chciałem dzisiaj sprawdzić kilka linijek w C#, a pod ręką miałem tylko MacBook Air. Stwierdziłem, że sprawdzę, jak szybko uruchomię kawałek kodu w C#. Ściągnąłem ze stron MS .NET Core 3.1 SDK dla MacOs i zainstalowałem. W terminalu uruchomiłem:

dotnet --version

I dostałem:

3.1.100

No, jest nieźle. Wykonałem:

dotnet new console -o FirstAppConsole
cd FirstAppConsole
dotnet run

I zobaczyłem nieśmiertelny: „Hello world!”. Nieźle, nieźle, bo zajęło mi to ok. 15 min.

A wracając trochę do początku, to chciałem sprawdzić, jak za pomocą Regex zweryfikować, czy istnieje w ciągu conajmniej jeden z szukanych wyrazów. Poniżej rozwiązanie:

            string regexPattern = @"(\bMój\b|\btest\b)";
            string myString = "Mój testowy ciąg znaków";

            bool isOk = Regex.IsMatch(myString, regexPattern, RegexOptions.IgnoreCase);
            Console.WriteLine(isOk);

Update. Pierwsza linia w przykładzie, może wyglądać tak:

            string regexPattern = @"\b(Mój|test)\b";

Ror cz. 5 – usuwanie

W kontrolerze interesuje nas metoda:

  # DELETE /authors/1
  # DELETE /authors/1.json
  def destroy
    @author.destroy
    respond_to do |format|
      format.html { redirect_to authors_url, notice: 'Author was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

I to właściwie wszystko, bo dla usuwania obiektu, domyślnie nie generuje się nowy widok. Wywołanie usuwania jest pod linkiem, który znajduje się na widoku listy autorów (Ror cz. 3 – kontrolery i widoki)

Najprostszy model mamy za sobą, czas przejść do modeli powiązanych relacją, ale to w kolejnej części.

RoR cz. 4 – dodawanie i edycja

Kontroler

W kontrolerze interesują nas 2 metody dotyczące dodawania nowego autora. Obie wygenerowane automatycznie:

  # GET /authors/new
  def new
    @author = Author.new
  end

Wykonywana przed wyświetleniem formularza.

I druga, wykonywana przy Post:

  # POST /authors
  # POST /authors.json
  def create
    @author = Author.new(author_params)

    respond_to do |format|
      if @author.save
        format.html { redirect_to @author, notice: 'Author was successfully created.' }
        format.json { render :show, status: :created, location: @author }
      else
        format.html { render :new }
        format.json { render json: @author.errors, status: :unprocessable_entity }
      end
    end
  end

Zobaczmy jeszcze metodę prywatną:

  def author_params
    params.require(:author).permit(:name, :surname)
  end

Wskazuje nam ona, które właściwości klasy, są skopiowane z widoku do nowego obiektu. Zapobiega ona „złośliwemu” ustawieniu właściwości ukrytych dla formularza.

Analogicznie, są dwie metody dotyczące edycji:

  # GET /authors/1/edit
  def edit
  end

I druga dla POST:

  # PATCH/PUT /authors/1
  # PATCH/PUT /authors/1.json
  def update
    respond_to do |format|
      if @author.update(author_params)
        format.html { redirect_to @author, notice: 'Author was successfully updated.' }
        format.json { render :show, status: :ok, location: @author }
      else
        format.html { render :edit }
        format.json { render json: @author.errors, status: :unprocessable_entity }
      end
    end
  end

Metoda edit jest pusta, bo mamy w klasie zdefiniowany filtr:

before_action :set_author, only: [:show, :edit, :update, :destroy]

I metodę prywatną:

    # Use callbacks to share common setup or constraints between actions.
    def set_author
      @author = Author.find(params[:id])
    end

Metoda ta jet wywoływana przed każdą metodą wymienioną w filtrze, czyli też przed edit.

Widoki

Automatycznie wygenerowały się widoki i teraz interesują mnie trzy z nich new.html.erb:

<h1>New Author</h1>

<%= render 'form', author: @author %>

<%= link_to 'Back', authors_path %>

Edit.html.erb:

<h1>Editing Author</h1>

<%= render 'form', author: @author %>

<%= link_to 'Show', @author %> |
<%= link_to 'Back', authors_path %>

Bardziej rozbudowany i ciekawy jest _form/html/erb:

<%= form_with(model: author, local: true) do |form| %>
  <% if author.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(author.errors.count, "error") %> prohibited this author from being saved:</h2>

      <ul>
      <% author.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name, id: :author_name %>
  </div>

  <div class="field">
    <%= form.label :surname %>
    <%= form.text_field :surname, id: :author_surname %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

Jest to widok częściowy, umieszczony w osobnym pliku, bo używamy go nie tylko przy dodawaniu nowego obiektu, ale też przy edycji.

Widok częściowy może otrzymać parametry. Np. widok new.html.erb przekazuje zmienną: button_name (napis na przycisku submit):

<h3>New author</h3>
<%= render partial: "form", locals: { button_name: "Save new" } %>

a edit.html.erb na:

<h3>Edit author</h3>
<%= render partial: "form", locals: { button_name: "Save changes" } %>

A widok częściowy wygląda jak poniżej. W przedostatniej linijce widzimy wykorzystanie button_name:

<% if @author.errors.present? %>
    <div class="alert alert-danger">
        <p>Errors:</p>
        <ol>
            <% @author.errors.full_messages.each do |message| %>
            <!--<li><%= message.titleize %></li> -->
            <li><%= message %></li>
            <% end %>
        </ol>
    </div>
<% end %>
<%= form_for @author do |f| %>
<div> 
    <%= f.label :name, "Name" %>
    <%= f.text_field :name , class: "form-control "%>
</div>
<div> 
    <%= f.label :name, "Surname" %>
    <%= f.text_field :surname , class: "form-control "%>
</div>
<%= f.submit button_name, class: "btn btn-primary" %>
<% end %>

Widoki korzystają też teraz z bootstrapa.

Ror cz. 3 – kontrolery i widoki

Potrzebujemy kontroler i widoki, żeby obsłużyć nowy model. Wygenerujmy kontroler:

rails g controller Authors

Ale to jest pusty kontroler bez żadnej akcji.

Możemy wygenerować:

rails g controller Authors index new create

Teraz mamy kontroler ze szkieletem 3 akcji i 3 puste widoki odpowiadające tym akcjom.

Możemy wygenerować cały CRUD:

rails g controller Authors index new create edit update show destroy

Ale możemy wygenerować cały CRUD razem z widokami za jednym zamachem:

rails g scaffold Author name: string surname:string

Teraz mamy ciała metod w kontrolerze, gotowe widoki i dodany wpis do pliku: routes.rb

resources :authors

Co załatwia nam routing adresów. Do routingu wrócę w jednym z kolejnych wpisów.

Index

W kontrolerze, metoda index wygląda tak:

def index
   @authors = Author.all
end

I nie potrzebuję jej zmieniać.

Widok views/authors/index.html.erb po wygenerowaniu:

<p id="notice"><%= notice %></p>

<h1>Authors</h1>

<table>
<thead>
<tr>
<th>Name</th>
<th>Surname</th>
<th colspan="3"></th>
</tr>
</thead>

<tbody>
<% @authors.each do |author| %>
<tr>
<td><%= author.name %></td>
<td><%= author.surname %></td>
<td><%= link_to 'Show', author %></td>
<td><%= link_to 'Edit', edit_author_path(author) %></td>
<td><%= link_to 'Destroy', author, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>

<br>

<%= link_to 'New Author', new_author_path %>

A zmieniam go na:

<h3>Authors</h3>

<p><%= link_to "New author", new_author_path, class: "btn btn-success" %> </p>

<table class="table table-stripped">
<thead>
<tr>
<th>Name</th>
<th>Surname</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% @authors.each do |author| %>
<tr>
<td><%= author.name %></td>
<td><%= author.surname %></td>
<td>
<%= link_to "Show", author_path(author), class: "btn btn-default btn-xs" %>
<%= link_to "Edit", edit_author_path(author), class: "btn btn-primary btn-xs" %>
<%= link_to "Delete", author_path(author), method: :delete, class: "btn btn-danger btn-xs", data: { confirm: "Are you sure?" } %>
</td>
</tr>
<% end %>
</tbody>
</table>

Czyli korzystam z bootstrapa, a link tworzenia nowego autora przeniosłem na górę. Usunąłem też ’notice”, ale do tego wrócę w kolejnej części, kiedy zajmiemy się dodawaniem/edycją autora

Ruby on Rails krok 2 – pierwszy model

Railsy są oparte o wzorzec MVC. Zaczynamy od M (model)Model w railsach to wzorzec ActiveRecord. Nie jestem fanem tego rozwiązania, ale na razie chcę poznać Railsy z całym dobrodziejstwem inwentarza.
Chcę stworzyć pierwszy model: autor z polami: name i surname.Z poziomu terminala wykonuję komendę:

rails generate model Author name:string surname:string

Zamiast:

rails generate ...

Można używać:

rails g ...

Mamy teraz klasę:

app/models/author.rb

Klasa jest pusta (tylko dziedziczy po ApplicationRecord). Właściwości są generowane na podstawie struktury tabeli.

Gdybyśmy chcieli usunąć model:

rails destroy model author

Migracja bazy danych

Drugi wygenerowany plik, to migracja bazy danych. Zapis zmian w strukturze bazy danych. W naszym przypadku jest to utworzenie nowej tabeli:

authors.db/migrate/data_czas_create_authors.rb

Żeby zmigrować schemat bazy danych, na której pracujemy, trzeba wykonać instrukcję:

rake db:migrate

Migracje można cofać. Ostatnią migrację cofamy za pomocą:

rake db:rollback 

Można cofnąć kilka ostatnich migracji, np: 3

rake db:rollback STEP=3 

Możemy sprawdzić, które migracje zostały wykonane na naszej bazie, a które nie:

 rake db:migrate:status 

Do migracji wrócę jeszcze w jednej z kolejnych części.

Zapis / edycja modelu w bazie z poziomu konsoli

Uruchamiamy konsolę:

rails console 

Wpisujemy:

Author.all
a = Author.new
a.name = “Jacek”
a.surname = “Dukaj”
a.save
Author.all

Zapisaliśmy pierwszego autora do bazy. Teraz czas na wygenerowanie widoków i kontrolerów, ale to już w następnej części.