Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Service/Order/Quote/CustomerHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public function assignCustomer(Quote $quote, array $orderData)
} catch (NoSuchEntityException $exception) {
$customer = $this->customerFactory->create();
$customer->setWebsiteId($websiteId);
$customer->setStoreId((int)$storeId);
$customer->setFirstname($this->validateName($orderData['customer']['first_name'], 'first_name'));
$customer->setMiddlename($this->validateName($orderData['customer']['middle_name'], 'middle_name'));
$customer->setLastname($this->validateName($orderData['customer']['last_name'], 'last_name'));
Expand Down
85 changes: 85 additions & 0 deletions Test/End-2-end/support/services/ChannableApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import BaseApi from './BaseApi';

export default class ChannableApi extends BaseApi {
Expand Down Expand Up @@ -63,6 +64,31 @@ export default class ChannableApi extends BaseApi {
return response.json();
}

/**
* GET a customer by email via the Magento REST API.
*/
async getCustomerByEmail(baseURL: string, email: string): Promise<any> {
const token = process.env.admin_token;
const searchUrl = `${baseURL}rest/V1/customers/search?` +
`searchCriteria[filterGroups][0][filters][0][field]=email&` +
`searchCriteria[filterGroups][0][filters][0][value]=${encodeURIComponent(email)}`;

const response = await fetch(searchUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
},
});

const result = await response.json();
if (result.items && result.items.length > 0) {
return result.items[0];
}

throw new Error(`Customer not found for email: ${email}`);
}

/**
* Build order data by merging overrides into the base template.
*/
Expand All @@ -84,6 +110,7 @@ export default class ChannableApi extends BaseApi {
companyName?: string;
channelName?: string;
shipmentMethod?: string;
email?: string;
} = {}): any {
const channableId = overrides.channableId || String(Math.floor(Math.random() * 900000) + 100000);
const country = overrides.country || 'NL';
Expand Down Expand Up @@ -148,6 +175,12 @@ export default class ChannableApi extends BaseApi {
data.shipping.company = overrides.companyName;
}

if (overrides.email) {
data.customer.email = overrides.email;
data.billing.email = overrides.email;
data.shipping.email = overrides.email;
}

if (overrides.channelName) {
data.channel_name = overrides.channelName;
}
Expand All @@ -169,6 +202,58 @@ export default class ChannableApi extends BaseApi {
return data;
}

/**
* Ensure a second store view exists for multi-store tests.
* Uses Magento bootstrap to create the store properly (triggers all observers/indexers).
* Returns the store ID, or null if no container is available.
*/
ensureSecondStoreView(storeCode: string): number | null {
if (!this.container) return null;

const phpScript = [
'<?php',
'error_reporting(0);',
"require 'app/bootstrap.php';",
'$bootstrap = \\Magento\\Framework\\App\\Bootstrap::create(BP, $_SERVER);',
'$om = $bootstrap->getObjectManager();',
'$repo = $om->get(\\Magento\\Store\\Api\\StoreRepositoryInterface::class);',
'try {',
` $store = $repo->get('${storeCode}');`,
'} catch (\\Magento\\Framework\\Exception\\NoSuchEntityException $e) {',
' $store = $om->get(\\Magento\\Store\\Model\\StoreFactory::class)->create();',
` $store->setCode('${storeCode}');`,
" $store->setName('Second Store');",
' $store->setWebsiteId(1);',
' $store->setGroupId(1);',
' $store->setIsActive(1);',
' $store->setSortOrder(10);',
' $store->save();',
'}',
'echo $store->getId();',
].join('\n');

const tmpFile = '/tmp/e2e-create-store.php';
execSync(`docker exec -i ${this.container} tee ${tmpFile}`, {
input: phpScript,
stdio: ['pipe', 'pipe', 'pipe'],
});

const result = execSync(
`docker exec ${this.container} php ${tmpFile}`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 120000 }
).trim();

const storeId = parseInt(result, 10);

execSync(`docker exec ${this.container} bin/magento config:set --scope=stores --scope-code=${storeCode} magmodules_channable/general/enable 1`, { stdio: 'pipe' });
execSync(`docker exec ${this.container} bin/magento config:set --scope=stores --scope-code=${storeCode} magmodules_channable_marketplace/general/enable 1`, { stdio: 'pipe' });
execSync(`docker exec ${this.container} bin/magento indexer:reindex`, { stdio: 'pipe', timeout: 120000 });
this.flushAllCaches();
console.log(`Second store view '${storeCode}' ensured (ID: ${storeId}).`);

return storeId;
}

/**
* Ensure a currency rate exists in Magento (needed for multi-currency orders).
*/
Expand Down
119 changes: 119 additions & 0 deletions Test/End-2-end/tests/feed/feed-generation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright Magmodules.eu. All rights reserved.
* See COPYING.txt for license details.
*/

import { test, expect } from '@playwright/test';
import ChannableApi from 'Services/ChannableApi';

const api = new ChannableApi();

const FEED_TOKEN = process.env.CHANNABLE_TOKEN || 'e2e-test-token';
const STORE_ID = 1;

const CONFIG = {
'magmodules_channable/general/enable': '1',
};

test.describe('Feed Generation', () => {
test.beforeAll(async ({}, testInfo) => {
const baseURL = testInfo.project.use.baseURL!;
await api.setMagentoConfig(baseURL, CONFIG);
});

test('feed endpoint returns valid JSON without errors', async ({ request }) => {
const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&page=1`);

expect(response.status()).toBe(200);

const body = await response.json();
expect(body).not.toHaveProperty('error');
});

test('feed endpoint returns products array', async ({ request }) => {
const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&page=1`);

const body = await response.json();
expect(body).toHaveProperty('products');
expect(Array.isArray(body.products)).toBe(true);
});

test('feed products contain required id and title fields', async ({ request }) => {
const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&page=1`);

const body = await response.json();
const products = body.products ?? [];

// Skip if no products in feed (empty catalog)
if (products.length === 0) return;

for (const product of products) {
// Every product row must have an id and title
expect(product).toHaveProperty('id');
expect(product).toHaveProperty('title');
expect(product.id).toBeTruthy();
}
});

test('feed does not crash with visibility filter enabled', async ({ request }, testInfo) => {
const baseURL = testInfo.project.use.baseURL!;

// Enable visibility filter and include "Not Visible Individually" (value 1)
await api.setMagentoConfig(baseURL, {
...CONFIG,
'magmodules_channable/filter/visbility_enabled': '1',
'magmodules_channable/filter/visbility': '1',
});

const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&page=1`);

expect(response.status()).toBe(200);

const body = await response.json();
expect(body).not.toHaveProperty('error');

// Reset visibility filter
await api.setMagentoConfig(baseURL, {
...CONFIG,
'magmodules_channable/filter/visbility_enabled': '0',
});
});

test('feed returns empty for invalid token', async ({ request }) => {
const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=wrong-token&page=1`);

expect(response.status()).toBe(200);

const body = await response.json();
// Should return empty array, not an error page
expect(Array.isArray(body) || (typeof body === 'object' && Object.keys(body).length === 0)).toBe(true);
});

test('feed returns empty when module is disabled', async ({ request }, testInfo) => {
const baseURL = testInfo.project.use.baseURL!;

await api.setMagentoConfig(baseURL, {
'magmodules_channable/general/enable': '0',
'magmodules_channable/general/token': FEED_TOKEN,
});

const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&page=1`);

expect(response.status()).toBe(200);

const body = await response.json();
expect(Array.isArray(body) || (typeof body === 'object' && Object.keys(body).length === 0)).toBe(true);

// Re-enable
await api.setMagentoConfig(baseURL, CONFIG);
});

test('single product feed via pid parameter', async ({ request }) => {
const response = await request.get(`/channable/feed/json?id=${STORE_ID}&token=${FEED_TOKEN}&pid=1`);

expect(response.status()).toBe(200);

const body = await response.json();
expect(body).not.toHaveProperty('error');
});
});
37 changes: 32 additions & 5 deletions Test/End-2-end/tests/order/order-import.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ const orderViewPage = new OrderViewPage();
const customerViewPage = new CustomerViewPage();

const PRODUCT_ID = parseInt(process.env.PRODUCT_ID || '1', 10);
const SECOND_STORE_CODE = 'second_store';

let secondStoreId: number | null = null;

const CONFIG_BASE = 'magmodules_channable_marketplace/order';

Expand Down Expand Up @@ -210,11 +213,29 @@ const testCases = [
expect(baseCurrencyTotal).toBeGreaterThan(0);
},
},
{
title: 'Customer created in correct store view',
config: {
[`${CONFIG_BASE}/import_customer`]: '1',
},
orderOverrides: {
email: `e2e-storeview-${Date.now()}@magmodules.eu`,
},
secondStore: true,
skipOrderView: true,
setup: async () => {
secondStoreId = channableApi.ensureSecondStoreView(SECOND_STORE_CODE);
},
assert: async (page, incrementId, orderData, baseURL) => {
const customer = await channableApi.getCustomerByEmail(baseURL, orderData.customer.email);
expect(customer.store_id).toBe(secondStoreId);
},
},
];

for (const testCase of testCases) {
test(`Order import: ${testCase.title}`, async ({ page, baseURL }) => {
// 0. Run optional setup (e.g. currency rates)
// 0. Run optional setup (e.g. currency rates, store views)
if (testCase.setup) {
await testCase.setup();
}
Expand All @@ -231,14 +252,20 @@ for (const testCase of testCases) {
...testCase.orderOverrides,
});

const response = await channableApi.postOrder(baseURL, orderData);
let storeId = 1;
if (testCase.secondStore && secondStoreId) {
storeId = secondStoreId;
}
const response = await channableApi.postOrder(baseURL, orderData, storeId);
const incrementId = getOrderIncrementId(response);
console.log(`Order created: ${incrementId} (${testCase.title})`);

// 3. Open order in admin
await orderViewPage.openByIncrementId(page, incrementId);
// 3. Open order in admin (skip for tests that only need API assertions)
if (!testCase.skipOrderView) {
await orderViewPage.openByIncrementId(page, incrementId);
}

// 4. Run test-specific assertions
await testCase.assert(page, incrementId);
await testCase.assert(page, incrementId, orderData, baseURL);
});
}
Loading