Currently I'm trying to test a controller which uses the authentication component to retrieve the user id. Since I'm fairly new to unit/integration testing, I have no idea how to make this working. Moreover, all the content that I could find for this particular problem is written for Cakephp version 2.
Controller function:
public function favorite(){
// Particular line where the problem is occurring:
$userId = $this->Auth->User()['id'];
// Get all favorite pictures of the user
$query = $this->UsersPictures->getFavoritePictures($userId);
$results = $query->all();
// Replace the link in the result set by a presigned url of Amazon
foreach($results as $result){
$result->picture->link = $this->Aws->getTokenizedItem($result->picture->link);
}
$this->set([
'success' => true,
'data' => [
'pictures' => $results
],
'_serialize' => ['success', 'data']
]);
}
Integration test:
public function testFavoriteShouldPass(){
$this->configRequest([
'headers' => [
'Accept' => 'application/json'
]
]);
$this->get('api/pictures/favorite/1.json');
$expected = [
'success' => true,
'data' => [
[
'user_id' => 1,
'picture_id' => 1,
'created' => '2016-04-03T20:35:40+0000',
'modified' => '2016-04-03T20:35:40+0000',
'picture' => [
'id' => 1,
'album_id' => 1,
'description' => 'Lorem ipsum dolor sit amet',
'link' => 'test',
'favorite' => true,
'created' => null,
'modified' => null,
'cover_photo' => true
]
]
]
];
$this->assertEquals($expected, $response);
}
My question is how can I insert a user with a default id of 1 for $this->Auth->User()['id']. I saw in other questions that I need to use something that looks like this:
$this->_controller->Auth
->staticExpects($this->any())
->method('user')
->will($this->returnValue([
'id' => 1,
'username' => 'admin',
'created' => '2013-05-08 00:00:00',
'modified' => '2013-05-08 00:00:00',
'email' => 'me#me.com',
]));
However, I read that staticExpects is deprecated from phpunit version 3.8 (I'm using 5.2). How should I mock this?
You can set the session data in your integration tests using $this->session(), for example (taken from cakephp book):
// Set session data
$this->session([
'Auth' => [
'User' => [
'id' => 1,
'username' => 'testing',
// other keys.
]
]
]);
You can actually find it in their docs: http://book.cakephp.org/3.0/en/development/testing.html#controller-integration-testing
You can find it in the section
Testing Actions That Require Authentication
If you want to use the same session data on each test you can use the setUp method of your integration test class, like so:
public function setUp()
{
parent::setUp();
// Set session data
$this->session([
'Auth' => [
'User' => [
'id' => 1,
'username' => 'testing',
// other keys.
]
]
]);
}
With thanks to azahar :), this worked for me:
public function controllerSpy($event)
{
parent::controllerSpy($event);
if (isset($this->_controller)) {
$this->_controller->Auth->setUser([
'id' => 1,
'username' => 'testtesttesttest',
'email' => 'john#doe.com',
'first_name' => 'John',
'last_name' => 'Doe',
'uuid' => 'wepoewoweo-ew-ewewpoeopw',
'sign_in_count' => 1,
'current_sign_in_ip' => '127.0.0.1',
'active' => true
]);
}
}
Related
Using the POST method $data = $this->request->getData(); , I get the archive:
[
'category_id' => '62',
'title' => 'Name-1',
'body' => '<p>Text</p>
',
'price' => '30',
'is_new' => '1',
'img' => [
'tmp_name' => 'D:\Web\OpenServer\userdata\temp\php70D9.tmp',
'error' => (int) 0,
'name' => 'IronMan.jpg',
'type' => 'image/jpeg',
'size' => (int) 131830
]
]
By preparing these data for the record in the database:
$product = $this->Products->patchEntity($product, $data);
But the patchEntity() method cuts out all the information about the image.
I get:
object(App\Model\Entity\Product) {
'category_id' => (int) 62,
'title' => 'Name-1',
'body' => '<p>Text</p>
',
'price' => (float) 30,
'is_new' => (int) 1,
'img' => '', // <--- empty :(
'[new]' => true,
'[accessible]' => [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'img' => true,
'is_new' => true,
'created' => true,
'modified' => true,
'category' => true
],
'[dirty]' => [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'is_new' => true,
'img' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Products'
}
It can be fixed? Tell me at least about. Thank you.
When patching/creating an entity, the data is bein marshalled according to the respective columns data type, as you can see for other properties like price, which is converted from a string to a float.
Your img column is probably of the type string, causing the marshaller to convert the data accordingly (see \Cake\Database\Type\StringType::marshal()).
There are various ways to avoid that, for example using a different property name that doesn't map to an existing column, like img_upload, and then after moving the upload, manually set the resulting filesystem path to the img property and save that.
That could also be done in the beforeMarshal event in your ProductsTable class, so that the view template can continue to use the img property:
public function beforeMarshal(
\Cake\Event\Event $event,
\ArrayObject $data,
\ArrayObject $options
) {
if (isset($data['img'])) {
$data['img_upload'] = $data['img'];
unset($data['img']);
}
}
You could also create a custom database type for the img column, one which doesn't marshal the data to a string, but just passes it on:
namespace App\Database\Type;
use Cake\Database\Type;
class FileType extends Type
{
public function marshal($value)
{
return $value;
}
}
You'd have to assign the filesystem path anyways though, you'd basically just avoid using a separate/temporary property.
See also
Cookbook > Database Access & ORM > Saving Data > Modifying Request Data Before Building Entities
Cookbook > Database Access & ORM > Database Basics > Adding Custom Types
I do not know how much this is correct, but in the end I did the following and everything works as I need:
In ProductsController:
public function add()
{
$product = $this->Products->newEntity();
if ($this->request->is('post')) {
$data = $this->request->getData();
$product = $this->Products->patchEntity($product, $data);
// If there is a picture that we checked (by the method of validationDefault, when calling patchEntity) and have already uploaded to the server in a temporary folder, then
if($product->img_upload['name']){
// We call the user method of processing the downloaded image
$product = $this->_customUploadImg($product);
// Leave only the name of the new file, adding it to the new property, with the name corresponding to the name of the table in the database
$product->img = $product->img_upload['name'];
}
// Delete an unnecessary property
unset($product->img_upload);
if ($this->Products->save($product)) {
// ...
}
// ...
}
In Product.php:
class Product extends Entity{
protected $_accessible = [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'is_new' => true,
'created' => true,
'modified' => true,
'category' => true,
// 'img' => true,
// Here we specify not 'img' as in the database table, but 'img_upload', in order for ProductsController not to delete our data file about the uploaded file when patchEntity was called.
'img_upload' => true,
];
}
In ProductsTable.php:
public function validationDefault(Validator $validator)
{
//...
$validator
->allowEmpty('img_upload', 'create')
->add('img_upload', [
'uploadError' => [
'rule' => 'uploadError',
'message' => 'Error loading picture'
],
'mimeType' => [
'rule' => ['mimeType', ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']],
'message' => 'Only image files are allowed to be uploaded: JPG, PNG и GIF'
],
'fileSize' => [
'rule' => ['fileSize', '<=', '2MB'],
'message' => 'The maximum file size should be no more than 2 MB'
]
]);
//...
}
In add.ctp:
echo $this->Form->create($product, ['type' => 'file']) ?>
// ...
echo $this->Form->control('img_upload', ['type' => 'file', 'label'=>'Product photo']);
//...
Thanks "ndm" and "mark"!
I am trying to list public status posts and friends posts
getting friends post
$friendsPosts= $this->Posts->find('all')
->contain(['Users', 'Languages', 'PostStates'])
->matching('Users.Dusers', function ($q) {
return $q->where(['Dusers.id' => $this->Auth->user('id')]);
});
getting public post
$posts= $this->Posts->find('all')
->where(['Posts.post_state_id' => 3])
->contain(['Users', 'Languages', 'PostStates']);
$posts->union($friendsPosts);
dd($posts->toArray());
error message: The used SELECT statements have a different number of columns
Here is solution
$posts = $this->Posts->find()
->where(['Posts.post_state_id' => 3])
->contain(['Users', 'PostStates', 'Languages', 'Tags', 'Translations' => ['Users', 'Languages', 'conditions' => ['post_state_id' => 3]]]);
$friendsPosts = $this->Posts->find('all')
->where(['Posts.post_state_id' => 2])
->contain(['PostStates', 'Languages', 'Users', 'Tags', 'Translations' => ['Users', 'Languages', 'conditions' => ['post_state_id' => 3]]])
->innerJoinWith('Users.Dusers', function ($q) {
return $q->where(['Dusers.id' => $this->Auth->user('id'), 'UsersUsers.status' => 1])->select($this->Posts->Users);
});
$posts->union($friendsPosts);
Events hasMany TicketTypes
TicketTypes belogsTo Events
I am trying to retrieve all events with associated ticket types:
$query= $this->Events
->find()
->select(['id', 'name'])
->autoFields(false)
->contain(['TicketTypes' => function($q) {
return $q->select(['TicketTypes.id', 'TicketTypes.name']); }])
;
SQL query generated:
SELECT Events.id AS `Events__id`, Events.name AS `Events__name` FROM events Events
But what I expected is:
SELECT Events.id AS `Events__id`, Events.name AS `Events__name`, TicketTypes.id AS `TicketTypes__id`, TicketTypes.name AS `TicketTypes__name` FROM events Events LEFT JOIN ticket_types TicketTypes ON Events.id = (TicketTypes.event_id)
This is how my models are configured:
class EventsTable extends Table
{
public function initialize(array $config)
{
$this->displayField('name');
$this->addAssociations([
'hasMany'=> ['TicketTypes']
]);
}
}
class TicketTypesTable extends Table
{
public function initialize(array $config)
{
$this->displayField('name');
$this->addAssociations([
'belongsTo' => ['Events']
]);
}
}
Here is the result of debugging my find query:
object(Cake\ORM\Query) {
'(help)' => 'This is a Query object, to get the results execute or iterate it.',
'sql' => 'SELECT Events.id AS `Events__id`, Events.name AS `Events__name` FROM events Events',
'params' => [],
'defaultTypes' => [
'Events.id' => 'integer',
'id' => 'integer',
'Events.name' => 'string',
'name' => 'string',
'Events.datetime_start' => 'datetime',
'datetime_start' => 'datetime',
'Events.datetime_end' => 'datetime',
'datetime_end' => 'datetime',
'Events.created' => 'datetime',
'created' => 'datetime',
'Events.modified' => 'datetime',
'modified' => 'datetime',
'Events.slug' => 'string',
'slug' => 'string',
'TicketTypes.id' => 'integer',
'TicketTypes.event_id' => 'integer',
'event_id' => 'integer',
'TicketTypes.name' => 'string',
'TicketTypes.description' => 'text'
],
'decorators' => (int) 0,
'executed' => false,
'hydrate' => true,
'buffered' => true,
'formatters' => (int) 0,
'mapReducers' => (int) 0,
'contain' => [
'TicketTypes' => [
'queryBuilder' => object(Closure) {
}
]
],
'matching' => [],
'extraOptions' => [],
'repository' => object(App\Model\Table\EventsTable) {
'registryAlias' => 'Events',
'table' => 'events',
'alias' => 'Events',
'entityClass' => 'App\Model\Entity\Event',
'associations' => [
(int) 0 => 'tickettypes'
],
'behaviors' => [],
'defaultConnection' => 'default',
'connectionName' => 'default'
}
}
And here is the result of debugging $query->all():
object(Cake\ORM\ResultSet) {
'items' => [
(int) 0 => object(App\Model\Entity\Event) {
'id' => (int) 101,
'name' => 'qwertyuiop',
'ticket_types' => [],
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[repository]' => 'Events'
},
...
As you can see in this line 'ticket_types' => [] ticket types are not being returned by the query.
What can I do to retrieve TicketTypes data?
Thanks.
hasMany associations are being retrieved in a separate query
Your assumption about how the CakePHP ORM retrieves associated data is incorrect.
Unlike hasOne and belongsTo assocaitions which are using joins in the main query, hasMany and belongsToMany asociated data is being retrieved in a separate query, which is being filtered using foreign key values collected from the main query, which in your case would be the Events.id column values.
Look at the rest of the SQL log, you shound find a query similar to
SELECT
TicketTypes.id AS `TicketTypes__id`, ...
FROM
ticket_types TicketTypes
WHERE
TicketTypes.event_id IN (1,2,3, ...)
The results of that query are being stitched together with the main results, and returned in a single result set.
Foreign keys need to be selected
A second problem is that your containments select() call is missing the foreign key column (TicketTypes.event_id), which is required, as without it, the ORM cannot stitch the results together, and thus the ticket types will not be present in the results.
Quote from the docs:
When you limit the fields that are fetched from an association, you
must ensure that the foreign key columns are selected. Failing to
select foreign key fields will cause associated data to not be present
in the final result.
See also
Cookbook > Database Access & ORM > Retrieving Associated Data
Cookbook > Database Access & ORM > Query Builder > Passing Conditions to Contain
I'm building an App with CakePHP 3.0. I have a FactsTable and an InterferencesTable. Interferences has this fields:
[id][changed_fact_id][influenced_fact_id][trend][modified_by][modified_at][created_by][created_at]
associations look like this:
FactsTable:
`$this->belongsToMany('InfluencedFacts', [
'through' => 'Interferences',
'className' => 'Facts',
'foreignKey' => 'changed_fact_id'
]);
$this->belongsToMany('ChangedFacts', [
'through' => 'Interferences',
'className' => 'Facts',
'foreignKey' => 'influenced_fact_id'
]);`
InterferencesTable:
`$this->belongsTo('ChangedFacts', [
'className' => 'Facts',
'foreignKey' => 'influenced_fact_id',
]);
$this->belongsTo('InfluencedFacts', [
'className' => 'Facts',
'foreignKey' => 'changed_fact_id',
]);`
I baked my controllers and views. Saving a new Fact works fine but the association isn't saved. I tried to save the association manually but it doesn't work, too.
If i made a mistake in model-association, please tell me ;)
saving code from FactsController.php:
`public function add() {
$fact = $this->Facts->newEntity($this->request->data);
if ($this->request->is('post')) {
if ($this->Facts->save($fact)) {
$id = $fact->get('id');
$this->Flash->success('The fact has been saved.');
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error('The fact could not be saved. Please, try again.');
}
}
$aggregates = $this->Facts->Aggregates->find('list');
$plants = $this->Facts->Plants->find('list');
$influencedFacts = $this->Facts->InfluencedFacts->find('list');
$this->set(compact('fact', 'aggregates', 'plants', 'influencedFacts'));
}`
Fact Entity:
`protected $_accessible = [
'name' => true,
'short' => true,
'description' => true,
'modified_by' => true,
'modified_at' => true,
'created_by' => true,
'created_at' => true,
'aggregates' => true,
'plants' => true,
];`
I have a CakePHP TestFixture that imports records from the real database table (into the test database table). However, based upon the CakePHP documentation, it appears that I can also have new records inserted along with the records import. This seems like a totally rational idea, yet it won't work no matter what way I try and structure the declarations.
class MemberFixture extends CakeTestFixture {
var $name = 'Member';
var $import = array('model' => 'Member', 'records' => true);
var $records = array(
array(
'id' => 1999997,
'last_name' => 'John',
'first_name' => 'Smith',
'member_occupation_id' => 0,
'zip' => '',
'age' => 30,
'created' => '2010-10-17 23:18:15',
'modified' => '2011-10-16 23:13:48',
),
array(
'id' => 1999998,
'last_name' => 'Jim',
'first_name' => 'Jones',
'member_occupation_id' => 1,
'zip' => '',
'age' => 25,
'created' => '2010-10-17 23:18:15',
'modified' => '2011-10-16 23:13:48',
),
array(
'id' => 1999999,
'last_name' => 'Dan',
'first_name' => 'Johnson',
'member_occupation_id' => 0,
'zip' => '',
'age' => 41,
'created' => '2010-10-17 23:18:15',
'modified' => '2011-10-16 23:13:48',
)
);
}
I've taken a peak under the hood to see how the record import process works in CakePHP. However, its still somewhat unclear why this functionality shouldn't work. Any thoughts?
'records' => true means that you are importing data from the table, and thus not the ones you have in your fixture
Replace
var $import = array('model' => 'Member', 'records' => true);
With
var $import = 'Member';