Skip to content

Commit

Permalink
Merge branch 'releases/4.20.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
Convly committed Mar 6, 2024
2 parents eaed17c + f37fdb4 commit 489f9b4
Show file tree
Hide file tree
Showing 93 changed files with 1,488 additions and 702 deletions.
2 changes: 1 addition & 1 deletion .github/actions/check-pr-status/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "check-pr-status",
"version": "4.20.3",
"version": "4.20.4",
"private": true,
"license": "MIT",
"main": "dist/index.js",
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ jobs:
if: failure()
with:
name: ce-playwright-trace
path: test-apps/e2e/**/test-results/**/trace.zip
path: test-apps/e2e/test-results/**/trace.zip
retention-days: 1

e2e_ee:
Expand Down Expand Up @@ -231,7 +231,7 @@ jobs:
if: failure()
with:
name: ee-playwright-trace
path: test-apps/e2e/**/test-results/**/trace.zip
path: test-apps/e2e/test-results/**/trace.zip
retention-days: 1

api_ce_pg:
Expand Down
12 changes: 12 additions & 0 deletions docs/docs/docs/01-core/content-releases/00-intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,15 @@ import { useCurrentSidebarCategory } from '@docusaurus/theme-common';
<DocCardList items={useCurrentSidebarCategory().items} />
```

### Release's status

Releases are assigned one of five statuses:

- **Ready**: Indicates that the release is fully prepared for publishing, with no invalid entries present.
- **Blocked**: Release has at least one invalid entry preventing publishing.
- **Empty**: Release contains no entries and cannot be published.
- **Failed**: Indicates that the publishing attempt for the release has encountered an error with no changes since then.
- **Done**: Confirms that the release has been successfully published without encountering any errors.

These statuses are dynamically updated based on actions such as creation, addition/removal of entries, updates, and publishing attempts. They provide a concise overview of release readiness and validity, ensuring smooth operations and data integrity.
28 changes: 28 additions & 0 deletions docs/docs/docs/01-core/content-releases/01-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,34 @@ Exposes methods to schedule release date for releases.
packages/core/content-releases/server/src/services/scheduling.ts
```
### Release status update triggers:
Considering that retrieving the status of all entries in a release is a heavy operation, we don't fetch it every time a user wants to access a release. Instead, we store the status in a field within the Release Content Type, and we only update it when an action that changes the status is triggered. These actions include:
#### Creating a release:
When creating a release, its status is automatically set to "Empty" as there are no entries initially.
#### Adding an entry to a release:
Upon adding an entry to a release, its status is recalculated to either "Ready" or "Blocked" based on the validity of the added entry.
#### Removing an entry from a release:
After removing an entry from a release, the status is recalculated to determine if the release is now "Ready", "Blocked", or "Empty".
#### Updating a release:
Whenever a release is updated, its status is recalculated based on the validity of the actions performed during the update.
#### Publishing a release:
During the publishing process, if successful, the status changes to "Done"; otherwise, it changes to "Failed".
#### Listening to events on entries:
When an entry is updated or deleted, the status of all releases containing that entry is recalculated to reflect any changes in validity.
## Migrations
We have two migrations that we run every time we sync the content types.
Expand Down
40 changes: 40 additions & 0 deletions docs/docs/guides/e2e/00-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,46 @@ This will spawn by default a Strapi instance per testing domain (e.g. content-ma

If you need to clean the test-apps folder because they are not working as expected, you can run `yarn test:e2e clean` which will clean said directory.

### Running specific tests

To run only one domain, meaning a top-level directory in e2e/tests such as "admin" or "content-manager", use the `--domains` option.

```shell
yarn test:e2e --domains admin
yarn test:e2e --domain admin
```

To run a specific file, you can pass arguments and options to playwright using `--` between the test:e2e options and the playwright options, such as:

```shell
# to run just the login.spec.ts file in the admin domain
yarn test:e2e --domains admin -- login.spec.ts
```

### Concurrency / parallellization

By default, every domain is run with its own test app in parallel with the other domains. The tests within a domain are run in series, one at a time.

If you need an easier way to view the output, or have problems running multiple apps at once on your system, you can use the `-c` option

```shell
# only run one domain at a time
yarn test:e2e -c 1
```

### Env Variables to Control Test Config

Some helpers have been added to allow you to modify the playwright configuration on your own system without touching the playwright config file used by the test runner.

| env var | Description | Default |
| ---------------------------- | -------------------------------------------- | ------------------ |
| PLAYWRIGHT_WEBSERVER_TIMEOUT | timeout for starting the Strapi server | 16000 (160s) |
| PLAYWRIGHT_ACTION_TIMEOUT | playwright action timeout (ie, click()) | 15000 (15s) |
| PLAYWRIGHT_EXPECT_TIMEOUT | playwright expect waitFor timeout | 10000 (10s) |
| PLAYWRIGHT_TIMEOUT | playwright timeout, for each individual test | 30000 (30s) |
| PLAYWRIGHT_OUTPUT_DIR | playwright output dir, such as trace files | '../test-results/' |
| PLAYWRIGHT_VIDEO | set 'true' to save videos on failed tests | false |

## Strapi Templates

The test-app you create uses a [template](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/installation/templates.html) found at `e2e/app-template` in this folder we can store our premade content schemas & any customisations we may need such as other plugins / custom fields / endpoints etc.
Expand Down
3 changes: 3 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## End-to-end Playwright Tests

See contributor docs in docs/docs/guides/e2e for more info
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { createTestTransferToken } = require('../../../create-transfer-token');

module.exports = {
rateLimitEnable(ctx) {
const { value } = ctx.request.body;
Expand All @@ -6,6 +8,11 @@ module.exports = {

configService.rateLimitEnable(value);

ctx.send(200);
},
async resetTransferToken(ctx) {
await createTestTransferToken(strapi);

ctx.send(200);
},
};
8 changes: 8 additions & 0 deletions e2e/app-template/template/src/api/config/routes/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,13 @@ module.exports = {
auth: false,
},
},
{
method: 'POST',
path: '/config/resettransfertoken',
handler: 'config.resetTransferToken',
config: {
auth: false,
},
},
],
};
27 changes: 27 additions & 0 deletions e2e/app-template/template/src/create-transfer-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { CUSTOM_TRANSFER_TOKEN_ACCESS_KEY } = require('./constants');

/**
* Make sure the test transfer token exists in the database
* @param {Strapi.Strapi} strapi
* @returns {Promise<void>}
*/
const createTestTransferToken = async (strapi) => {
const { token: transferTokenService } = strapi.admin.services.transfer;

const accessKeyHash = transferTokenService.hash(CUSTOM_TRANSFER_TOKEN_ACCESS_KEY);
const exists = await transferTokenService.exists({ accessKey: accessKeyHash });

if (!exists) {
await transferTokenService.create({
name: 'TestToken',
description: 'Transfer token used to seed the e2e database',
lifespan: null,
permissions: ['push'],
accessKey: CUSTOM_TRANSFER_TOKEN_ACCESS_KEY,
});
}
};

module.exports = {
createTestTransferToken,
};
24 changes: 1 addition & 23 deletions e2e/app-template/template/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { CUSTOM_TRANSFER_TOKEN_ACCESS_KEY } = require('./constants');
const { createTestTransferToken } = require('./create-transfer-token');

module.exports = {
/**
Expand All @@ -23,25 +23,3 @@ module.exports = {
await createTestTransferToken(strapi);
},
};

/**
* Make sure the test transfer token exists in the database
* @param {Strapi.Strapi} strapi
* @returns {Promise<void>}
*/
const createTestTransferToken = async (strapi) => {
const { token: transferTokenService } = strapi.admin.services.transfer;

const accessKeyHash = transferTokenService.hash(CUSTOM_TRANSFER_TOKEN_ACCESS_KEY);
const exists = await transferTokenService.exists({ accessKey: accessKeyHash });

if (!exists) {
await transferTokenService.create({
name: 'TestToken',
description: 'Transfer token used to seed the e2e database',
lifespan: null,
permissions: ['push'],
accessKey: CUSTOM_TRANSFER_TOKEN_ACCESS_KEY,
});
}
};
2 changes: 2 additions & 0 deletions e2e/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const ALLOWED_CONTENT_TYPES = [
'admin::user',
'admin::role',
'admin::permission',
'admin::api-token',
'admin::transfer-token',
'api::article.article',
'api::author.author',
'api::homepage.homepage',
Expand Down
13 changes: 13 additions & 0 deletions e2e/scripts/dts-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ export const resetDatabaseAndImportDataFromPath = async (filePath) => {

engine.diagnostics.onDiagnostic(console.log);

try {
// reset the transfer token to allow the transfer if it's been wiped (that is, not included in previous import data)
const res = await fetch(
`http://127.0.0.1:${process.env.PORT ?? 1337}/api/config/resettransfertoken`,
{
method: 'POST',
}
);
} catch (err) {
console.error('Token reset failed.' + JSON.stringify(err, null, 2));
process.exit(1);
}

try {
await engine.transfer();
} catch {
Expand Down
65 changes: 65 additions & 0 deletions e2e/tests/admin/transfer/tokens.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { test, expect } from '@playwright/test';
import { login } from '../../../utils/login';
import { resetDatabaseAndImportDataFromPath } from '../../../scripts/dts-import';
import { navToHeader, delay } from '../../../utils/shared';

const createTransferToken = async (page, tokenName, duration, type) => {
await navToHeader(
page,
['Settings', 'Transfer Tokens', 'Create new Transfer Token'],
'Create Transfer Token'
);

await page.getByLabel('Name*').click();
await page.getByLabel('Name*').fill(tokenName);

await page.getByLabel('Token duration').click();
await page.getByRole('option', { name: duration }).click();

await page.getByLabel('Token type').click();
await page.getByRole('option', { name: type }).click();

await page.getByRole('button', { name: 'Save' }).click();

await expect(page.getByText(/copy this token/)).toBeVisible();
await expect(page.getByText('Expiration date:')).toBeVisible();
};

test.describe('Transfer Tokens', () => {
test.beforeEach(async ({ page }) => {
await resetDatabaseAndImportDataFromPath('./e2e/data/with-admin.tar');
await page.goto('/admin');
await login({ page });
});

// Test token creation
const testCases = [
['30-day push token', '30 days', 'Push'],
['30-day pull token', '30 days', 'Pull'],
['30-day full-access token', '30 days', 'Full access'],
// if push+pull work generally that's good enough for e2e
['7-day token', '7 days', 'Full access'],
['90-day token', '90 days', 'Full access'],
['unlimited token', 'Unlimited', 'Full access'],
];
for (const [name, duration, type] of testCases) {
test(`A user should be able to create a ${name}`, async ({ page }) => {
await createTransferToken(page, name, duration, type);
});
}

test('Created tokens list page should be correct', async ({ page }) => {
await createTransferToken(page, 'my test token', 'unlimited', 'Full access');

// if we don't wait until createdAt is at least 1s, we see "NaN" for the timestamp
// TODO: fix the bug and remove this
await page.waitForTimeout(1100);

await navToHeader(page, ['Settings', 'Transfer Tokens'], 'Transfer Tokens');

const row = page.getByRole('gridcell', { name: 'my test token', exact: true });
await expect(row).toBeVisible();
await expect(page.getByText(/\d+ (second|minute)s? ago/)).toBeVisible();
// TODO: expand on this test, it could check edit and delete icons
});
});
12 changes: 11 additions & 1 deletion e2e/tests/content-releases/releases-page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,17 @@ describeOnCondition(edition === 'EE')('Releases page', () => {
name: 'Date',
})
.click();
await page.getByRole('gridcell', { name: 'Sunday, March 3, 2024' }).click();

const date = new Date();
date.setDate(date.getDate() + 1);
const formattedDate = date.toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
});

await page.getByRole('gridcell', { name: formattedDate }).click();

await page
.getByRole('combobox', {
Expand Down
20 changes: 19 additions & 1 deletion e2e/utils/shared.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { test } from '@playwright/test';
import { test, Page, expect } from '@playwright/test';

/**
* Execute a test suite only if the condition is true
*/
export const describeOnCondition = (shouldDescribe: boolean) =>
shouldDescribe ? test.describe : test.describe.skip;

/**
* Navigate to a page and confirm the header, awaiting each step
*/
export const navToHeader = async (page: Page, navItems: string[], headerText: string) => {
for (const navItem of navItems) {
// This does not use getByRole because sometimes "Settings" is "Settings 1" if there's a badge notification
// BUT if we don't match exact it conflicts with "Advanceed Settings"
// As a workaround, we implement our own startsWith with page.locator
const item = page.locator(`role=link[name^="${navItem}"]`);
await expect(item).toBeVisible();
await item.click();
}

const header = page.getByRole('heading', { name: headerText, exact: true });
await expect(header).toBeVisible();
return header;
};
24 changes: 12 additions & 12 deletions examples/getstarted/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "getstarted",
"version": "4.20.3",
"version": "4.20.4",
"private": true,
"description": "A Strapi application.",
"license": "SEE LICENSE IN LICENSE",
Expand All @@ -13,17 +13,17 @@
"strapi": "strapi"
},
"dependencies": {
"@strapi/icons": "1.14.1",
"@strapi/plugin-color-picker": "4.20.3",
"@strapi/plugin-documentation": "4.20.3",
"@strapi/plugin-graphql": "4.20.3",
"@strapi/plugin-i18n": "4.20.3",
"@strapi/plugin-sentry": "4.20.3",
"@strapi/plugin-users-permissions": "4.20.3",
"@strapi/provider-email-mailgun": "4.20.3",
"@strapi/provider-upload-aws-s3": "4.20.3",
"@strapi/provider-upload-cloudinary": "4.20.3",
"@strapi/strapi": "4.20.3",
"@strapi/icons": "1.15.0",
"@strapi/plugin-color-picker": "4.20.4",
"@strapi/plugin-documentation": "4.20.4",
"@strapi/plugin-graphql": "4.20.4",
"@strapi/plugin-i18n": "4.20.4",
"@strapi/plugin-sentry": "4.20.4",
"@strapi/plugin-users-permissions": "4.20.4",
"@strapi/provider-email-mailgun": "4.20.4",
"@strapi/provider-upload-aws-s3": "4.20.4",
"@strapi/provider-upload-cloudinary": "4.20.4",
"@strapi/strapi": "4.20.4",
"better-sqlite3": "8.6.0",
"lodash": "4.17.21",
"mysql": "2.18.1",
Expand Down
Loading

0 comments on commit 489f9b4

Please sign in to comment.