and other relatively accurate stories
gizra
// @amitaibu
The Gizra stack
Drupal
Elm
Yesod (Haskell)
Clients pay for the features
not the bugs
Articulate the problem
It's not about fighting bugs. It's about setting our priorities
Correctness
and my expectations from computers
Elm
Generate JavaScript with great performance and no runtime exceptions
module Counter exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
-- MODEL
type alias Model =
Int
emptyModel : Model
emptyModel =
0
-- UPDATE
type Msg
= Decrement
| Increment
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Decrement ->
( model - 1
, Cmd.none
)
Increment ->
( model + 1
, Cmd.none
)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (toString model) ]
, button [ onClick Increment ] [ text "+" ]
]
--
type alias User =
{ avatarUrl : String
, name : String
}
viewAvatar : String -> Html a
viewAvatar url =
img [ src url ] []
--
--
type alias User =
{ avatarUrl : String
, name : String
}
viewAvatar : String -> Html a
viewAvatar url =
img [ src url ] []
main = viewAvatar "But this isn't a URL!"
Compile Error Vs Runtime Mistakes
Types 101
type Bool = False | True
type UserType = Anonymous | Authenticated String | Premium String Date
--
type alias User =
{ avatarUrl : String
, name : String
}
viewAvatar : String -> Html a
viewAvatar url =
img [ src url ] []
--
type Url = Url String
type alias User =
{ avatarUrl : Url
, name : String
}
viewAvatar : Url -> Html a
viewAvatar url =
let
(Url val) = url
in
img [ src val ] []
Maybe values
!empty(), you will not be missed
type alias User =
{ avatarUrl : Url
, name : String
}
emptyUser =
{ avatarUrl = Url ""
, name = ""
}
type alias Model =
{ user : User
}
emptyModel =
{ user = emptyUser
}
type alias Model =
{ user : Maybe User
}
emptyModel =
{ user = Nothing
}
type alias Model =
{ user : Maybe User
}
emptyModel =
{ user = Just <| User "https://example.com/avatar" "amitaibu"
}
{-|
* `NotAsked` - We haven't asked for the data yet.
* `Loading` - We've asked, but haven't got an answer yet.
* `Failure` - We asked, but something went wrong. Here's the error.
* `Success` - Everything worked, and here's the data.
-}
type RemoteData e a
= NotAsked
| Loading
| Failure e
| Success a
type alias WebData a =
RemoteData Http.Error a
type alias Model =
{ user : WebData User
}
emptyModel =
{ user = NotAsked
}
debugUser =
{ user = Success <| User "https://example.com/avatar" "amitaibu"
}
type Language
= English
| Spanish
type TranslationId
= Login
| WelcomeBack { name : String }
type alias TranslationSet =
{ english : String
, spanish : String
}
translations : TranslationId -> TranslationSet
translations id =
case id of
Login ->
{ english = "Please login"
, spanish = "Por favor haga login"
}
WelcomeBack val ->
{ english = "Welcome back " ++ val.name
, spanish = "Bienvenido " ++ val.name
}
translate : Language -> TranslationId -> String
translate lang id =
case lang of
English ->
.english <| translations id
Spanish ->
.spanish <| translations id
What can possibly go wrong?
drupal_add_css('static/css/bootstrap.css');
addStylesheet $ StaticR css_bootstrap_css
Finally! Drupal (8)!
OG8
Safe Guards
throw new \Exception();
Be your own devil's advocate
Functional testing
Kernel testing
Unit testing
/**
* Tests the formatter changes by user and membership.
*/
public function testGroupTab() {
$this->drupalLogin($this->user1);
$this->drupalGet('group/node/' . $this->group->id() . '/admin');
$this->assertResponse(200);
$this->drupalGet('group/node/' . $this->nonGroup->id() . '/admin');
$this->assertResponse(403);
}
/**
* Tests access to the create entity form through the user interface.
*
* @group og
* @coversDefaultClass \Drupal\og\Form\GroupSubscribeForm
*/
class GroupSubscribeFormTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'system',
'user',
'field',
'node',
'og',
];
/**
* A user object.
*
* @var \Drupal\user\Entity\User
*/
protected $user1;
/**
* A group entity.
*
* @var \Drupal\node\Entity\Node
*/
protected $group1;
/**
* A group entity.
*
* @var \Drupal\node\Entity\Node
*/
protected $group2;
/**
* A group entity.
*
* @var \Drupal\node\Entity\Node
*/
protected $group3;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(['og']);
$this->installEntitySchema('og_membership');
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installSchema('system', 'sequences');
// Create bundles.
$groupBundle1 = Unicode::strtolower($this->randomMachineName());
$groupBundle2 = Unicode::strtolower($this->randomMachineName());
$groupBundle3 = Unicode::strtolower($this->randomMachineName());
// Define the entities as groups.
Og::groupTypeManager()->addGroup('node', $groupBundle1);
Og::groupTypeManager()->addGroup('node', $groupBundle2);
Og::groupTypeManager()->addGroup('node', $groupBundle3);
// Create node author user.
$user = User::create(['name' => $this->randomString()]);
$user->save();
// Create groups.
$this->group1 = Node::create([
'type' => $groupBundle1,
'title' => $this->randomString(),
'uid' => $user->id(),
]);
$this->group1->save();
$this->group2 = Node::create([
'type' => $groupBundle2,
'title' => $this->randomString(),
'uid' => $user->id(),
]);
$this->group2->save();
// Create an unpublished node, so users won't have access to it.
$this->group3 = Node::create([
'type' => $groupBundle3,
'title' => $this->randomString(),
'uid' => $user->id(),
'status' => NODE_NOT_PUBLISHED,
]);
$this->group3->save();
// Change the permissions of group to "subscribe".
/** @var OgRole $role */
$role = OgRole::getRole('node', $groupBundle1, OgRoleInterface::ANONYMOUS);
$role
->grantPermission('subscribe')
->save();
// Change the permissions of group to allow "subscribe without approval".
$role = OgRole::getRole('node', $groupBundle2, OgRoleInterface::ANONYMOUS);
$role
->grantPermission('subscribe without approval')
->save();
// Change the permissions of group to allow "subscribe without approval" on
// the unpublished node.
$role = OgRole::getRole('node', $groupBundle3, OgRoleInterface::ANONYMOUS);
$role
->grantPermission('subscribe without approval')
->save();
}
/**
* Tests subscribe confirmation related text.
*
* @covers ::isStateActive
*/
public function testIsStateActive() {
$user = $this->createUser(['access content']);
/** @var GroupSubscribeForm $form */
$form = \Drupal::entityManager()->getFormObject('og_membership', 'subscribe');
// Pending membership.
$membership_pending = OgMembership::create();
$membership_pending
->setGroup($this->group1)
->setUser($user);
$form->setEntity($membership_pending);
$this->assertFalse($form->isStateActive());
}
/**
* Tests the group check access.
*
* @group og
* @coversDefaultClass \Drupal\og\Access\GroupCheck
*/
class GroupCheckTest extends UnitTestCase {
/**
* The entity type manager prophecy used in the test.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $entityTypeManager;
/**
* The entity type prophecy used in the test.
*
* @var \Drupal\Core\Entity\EntityTypeInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $entityType;
/**
* The entity storage prophecy used in the test.
*
* @var \Drupal\Core\Entity\EntityStorageInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $entityStorage;
/**
* The OG access service prophecy used in the test.
*
* @var \Drupal\og\OgAccess|\Prophecy\Prophecy\ObjectProphecy
*/
protected $ogAccess;
/**
* The route service prophecy used in the test.
*
* @var \Symfony\Component\Routing\Route|\Prophecy\Prophecy\ObjectProphecy
*/
protected $route;
/**
* A user used in the test.
*
* @var \Drupal\user\UserInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $user;
/**
* The entity type ID of the test group.
*
* @var string
*/
protected $entityTypeId;
/**
* The bundle ID of the test group.
*
* @var string
*/
protected $bundle;
/**
* The test group entity used in the test..
*
* @var \Drupal\Core\Entity\EntityInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $group;
/**
* A random entity ID.
*
* @var int
*/
protected $entityId;
/**
* The group manager used in the test.
*
* @var \Drupal\og\GroupTypeManager|\Prophecy\Prophecy\ObjectProphecy
*/
protected $groupTypeManager;
/**
* The access result used in the test.
*
* @var \Drupal\Core\Access\AccessResultInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $accessResult;
/**
* The route match service used in the test.
*
* @var \\Drupal\Core\Routing\RouteMatchInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $routeMatch;
/**
* {@inheritdoc}
*/
public function setUp() {
$this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
$this->entityType = $this->prophesize(EntityTypeInterface::class);
$this->entityStorage = $this->prophesize(EntityStorageInterface::class);
$this->ogAccess = $this->prophesize(OgAccessInterface::class);
$this->route = $this->prophesize(Route::class);
$this->routeMatch = $this->prophesize(RouteMatchInterface::class);
$this->entityTypeId = $this->randomMachineName();
$this->bundle = $this->randomMachineName();
$this->entityId = rand(10, 50);
$this->groupTypeManager = $this->prophesize(GroupTypeManager::class);
$this->user = $this->prophesize(AccountInterface::class);
$this->group = $this->prophesize(EntityInterface::class);
$this->accessResult = $this->prophesize(AccessResultInterface::class);
$container = new ContainerBuilder();
$container->set('og.group_type_manager', $this->groupTypeManager->reveal());
\Drupal::setContainer($container);
}
/**
* Tests a non-existing group.
*
* @covers ::access
*/
public function testNoGroup() {
$this
->entityTypeManager
->getDefinition($this->entityTypeId, FALSE)
->willReturn($this->entityType);
$this
->entityTypeManager
->getStorage($this->entityTypeId)
->willReturn($this->entityStorage);
$this->entityStorage
->load($this->entityId)
->willReturn(NULL);
$group_check = new GroupCheck($this->entityTypeManager->reveal(), $this->ogAccess->reveal());
$result = $group_check->access($this->user->reveal(), $this->route->reveal(), $this->routeMatch->reveal(), $this->entityTypeId, $this->entityId);
$this->assertTrue($result->isForbidden());
}
}