mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 07:17:34 +00:00
contributing
This commit is contained in:
135
CONTRIBUTING.md
135
CONTRIBUTING.md
@@ -28,39 +28,6 @@ Please consider the following guidelines when creating a pull request.
|
||||
- We use ESLint/Prettier for linting/formatting, so please run `pnpm lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
|
||||
- If you add new functionality, please provide the corresponding documentation as well and make it part of the pull request
|
||||
|
||||
## Setting Up The ZITADEL API
|
||||
|
||||
If you want to have a one-liner to get you up and running,
|
||||
or if you want to develop against a ZITADEL API with the latest features,
|
||||
or even add changes to ZITADEL itself at the same time,
|
||||
you should develop against your local ZITADEL process.
|
||||
However, it might be easier to develop against your ZITADEL Cloud instance
|
||||
if you don't have docker installed
|
||||
or have limited resources on your local machine.
|
||||
|
||||
### Developing Against Your Local ZITADEL Instance
|
||||
|
||||
```sh
|
||||
# To have your service user key and environment file written with the correct ownership, export your current users ID.
|
||||
export ZITADEL_DEV_UID="$(id -u)"
|
||||
|
||||
# Pull images
|
||||
docker compose --file ./acceptance/docker-compose.yaml pull
|
||||
|
||||
# Run ZITADEL with local notification sink and configure ./apps/login/.env.local
|
||||
pnpm run-sink
|
||||
```
|
||||
|
||||
### Developing Against Your ZITADEL Cloud Instance
|
||||
|
||||
Configure your shell by exporting the following environment variables:
|
||||
|
||||
```sh
|
||||
export ZITADEL_API_URL=<your cloud instance URL here>
|
||||
export ZITADEL_ORG_ID=<your service accounts organization id here>
|
||||
export ZITADEL_SERVICE_USER_TOKEN=<your service account personal access token here>
|
||||
```
|
||||
|
||||
### Setting up local environment
|
||||
|
||||
```sh
|
||||
@@ -76,36 +43,94 @@ pnpm dev
|
||||
|
||||
The application is now available at `http://localhost:3000`
|
||||
|
||||
### Adding applications and IDPs
|
||||
Configure apps/login/.env.local to target the Zitadel instance of your choice.
|
||||
The login app live-reloads on changes, so you can start developing right away.
|
||||
|
||||
<!-- Console doesn't load
|
||||
|
||||
### Developing Against Your Local ZITADEL Instance
|
||||
|
||||
The following command uses Docker to run a local ZITADEL instance and the login application in live-reloading dev mode.
|
||||
Additionally, it runs a Traefik reverse proxy that exposes the login at https://localhost with a self-signed certificate.
|
||||
|
||||
```sh
|
||||
# OPTIONAL Run SAML SP
|
||||
pnpm run-samlsp
|
||||
pnpm test:acceptance:setup
|
||||
```
|
||||
-->
|
||||
|
||||
# OPTIONAL Run OIDC RP
|
||||
pnpm run-oidcrp
|
||||
### Quality Assurance
|
||||
|
||||
# OPTIONAL Run SAML IDP
|
||||
pnpm run-samlidp
|
||||
|
||||
# OPTIONAL Run OIDC OP
|
||||
pnpm run-oidcop
|
||||
Use `make` commands to test the quality of your code without installing any dependencies besides Docker.
|
||||
Using `make` commands, you can reproduce and debug the CI pipelines locally.
|
||||
```sh
|
||||
# Reproduce the whole CI pipeline in docker
|
||||
make login-quality
|
||||
# Show other options with make
|
||||
make help
|
||||
```
|
||||
|
||||
### Testing
|
||||
Use `pnpm` commands to run the tests in dev mode with live reloading and debugging capabilities.
|
||||
|
||||
To test the quality of your code, make sure
|
||||
#### Linting and formatting
|
||||
|
||||
You can execute the following commands `pnpm test` for a single test run or `pnpm test:watch` in the following directories:
|
||||
Check the formatting and linting of the code in docker
|
||||
|
||||
- apps/login
|
||||
- packages/zitadel-proto
|
||||
- packages/zitadel-client
|
||||
- packages/zitadel-node
|
||||
- The projects root directory: all tests in the project are executed
|
||||
```sh
|
||||
make login-lint
|
||||
```
|
||||
|
||||
In apps/login, these commands also spin up the application and a ZITADEL gRPC API mock server to run integration tests using [Cypress](https://www.cypress.io/) against them.
|
||||
If you want to run the integration tests standalone against an environment of your choice, navigate to ./apps/login, [configure your shell as you like](# Developing Against Your ZITADEL Cloud Instance) and run `pnpm test:integration:run` or `pnpm test:integration:open`.
|
||||
Then you need to lifecycle the mock process using the command `pnpm mock` or the more fine grained commands `pnpm mock:build`, `pnpm mock:build:nocache`, `pnpm mock:run` and `pnpm mock:destroy`.
|
||||
Check the linting of the code using pnpm
|
||||
|
||||
That's it! 🎉
|
||||
```sh
|
||||
pnpm lint
|
||||
pnpm format
|
||||
```
|
||||
|
||||
Fix the linting of your code
|
||||
|
||||
```sh
|
||||
pnpm lint:fix
|
||||
pnpm format:fix
|
||||
```
|
||||
|
||||
#### Running Unit Tests
|
||||
|
||||
Run the tests in docker
|
||||
|
||||
```sh
|
||||
make login-test-unit
|
||||
```
|
||||
|
||||
Run unit tests with live-reloading
|
||||
|
||||
```sh
|
||||
pnpm test:unit
|
||||
```
|
||||
|
||||
#### Running Integration Tests
|
||||
|
||||
Run the test in docker
|
||||
|
||||
```sh
|
||||
make login-test-integration
|
||||
```
|
||||
|
||||
Open the Cypress test suite to run the integration tests in interactive mode.
|
||||
First, set up your local test environment.
|
||||
This runs a mock server in docker and the login application in dev mode with live-reloading enabled.
|
||||
|
||||
```sh
|
||||
pnpm test:integration:setup
|
||||
```
|
||||
|
||||
Now, in another terminal session, open the interactive Cypress integration test suite.
|
||||
|
||||
```sh
|
||||
pnpm test:integration open
|
||||
```
|
||||
|
||||
Show more options with Cypress
|
||||
|
||||
```sh
|
||||
pnpm test:integration help
|
||||
```
|
||||
|
6
Makefile
6
Makefile
@@ -23,7 +23,7 @@ export LOGIN_TEST_ACCEPTANCE_SAMLSP_TAG := login-test-acceptance-samlsp:${DOCKER
|
||||
export LOGIN_TEST_ACCEPTANCE_SAMLIDP_TAG := login-test-acceptance-samlidp:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
export POSTGRES_TAG := postgres:17.0-alpine3.19
|
||||
export GOLANG_TAG := golang:1.24-alpine
|
||||
export ZITADEL_TAG ?= ghcr.io/zitadel/zitadel:02617cf17fdde849378c1a6b5254bbfb2745b164
|
||||
export ZITADEL_TAG ?= ghcr.io/zitadel/zitadel:v3.3.0
|
||||
export CORE_MOCK_TAG := core-mock:${DOCKER_METADATA_OUTPUT_VERSION}
|
||||
|
||||
.PHONY: login-help
|
||||
@@ -95,6 +95,10 @@ login-test-acceptance: login-test-acceptance-build
|
||||
$(LOGIN_TEST_ACCEPTANCE_OIDCRP_TAG) \
|
||||
$(LOGIN_TEST_ACCEPTANCE_SAMLSP_TAG)"
|
||||
|
||||
.PHONY: login-quality
|
||||
login-quality: login-lint login-test-unit login-test-integration
|
||||
@:
|
||||
|
||||
.PHONY: login-standalone-build
|
||||
login-standalone-build:
|
||||
$(BAKE_CLI_WITH_COMMON_ARGS) login-standalone
|
||||
|
@@ -2,7 +2,7 @@ services:
|
||||
|
||||
zitadel:
|
||||
user: "${UID:-1000}:${GID:-1000}"
|
||||
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}"
|
||||
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v3.3.0}"
|
||||
container_name: acceptance-zitadel
|
||||
pull_policy: always
|
||||
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
|
||||
@@ -12,7 +12,6 @@ services:
|
||||
# - "traefik.http.middlewares.zitadel.headers.customrequestheaders.Host=localhost"
|
||||
# - "traefik.http.routers.zitadel.middlewares=zitadel@docker"
|
||||
- "traefik.http.services.zitadel-service.loadbalancer.server.scheme=h2c"
|
||||
- "traefik.http.services.zitadel-service.loadbalancer.passHostHeader=false"
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
|
@@ -21,7 +21,7 @@ export default defineConfig({
|
||||
timeout: 300 * 1000, // 5 minutes
|
||||
globalTimeout: 30 * 60_000, // 30 minutes
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [["line"], ["html", { open: process.env.CI ? "never" : "on-failure", host: "0.0.0.0" }]],
|
||||
reporter: [["line"], ["html", { open: process.env.CI ? "never" : "on-failure", host: "0.0.0.0", outputFolder: "./playwright-report/html" }]],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
@@ -32,7 +32,7 @@ export default defineConfig({
|
||||
video: "retain-on-failure",
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
outputDir: "test-results",
|
||||
outputDir: "test-results/results",
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
|
@@ -45,7 +45,8 @@ test("user email not verified, resend, verify", async ({ user, page }) => {
|
||||
await emailVerifyResend(page);
|
||||
const c = await getCodeFromSink(user.getUsername());
|
||||
// wait for resend of the code
|
||||
await page.waitForTimeout(2000); await emailVerify(page, c);
|
||||
await page.waitForTimeout(2000);
|
||||
await emailVerify(page, c);
|
||||
await loginScreenExpect(page, user.getFullName());
|
||||
});
|
||||
|
||||
|
@@ -1,39 +1,39 @@
|
||||
import {Gaxios, GaxiosResponse} from 'gaxios';
|
||||
import { Gaxios, GaxiosResponse } from "gaxios";
|
||||
|
||||
const awaitNotification = new Gaxios({
|
||||
url: process.env.SINK_NOTIFICATION_URL,
|
||||
method: 'POST',
|
||||
retryConfig: {
|
||||
httpMethodsToRetry: ['POST'],
|
||||
statusCodesToRetry: [[404, 404]],
|
||||
retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
|
||||
totalTimeout: 10000, // 10 seconds
|
||||
onRetryAttempt: (error) => {
|
||||
console.warn(`Retrying request to sink notification service: ${error.message}`);
|
||||
}
|
||||
}
|
||||
url: process.env.SINK_NOTIFICATION_URL,
|
||||
method: "POST",
|
||||
retryConfig: {
|
||||
httpMethodsToRetry: ["POST"],
|
||||
statusCodesToRetry: [[404, 404]],
|
||||
retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
|
||||
totalTimeout: 10000, // 10 seconds
|
||||
onRetryAttempt: (error) => {
|
||||
console.warn(`Retrying request to sink notification service: ${error.message}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export async function getOtpFromSink(recipient: string): Promise<any> {
|
||||
return awaitNotification.request({data: {recipient}}).then((response) => {
|
||||
return awaitNotification.request({ data: { recipient } }).then((response) => {
|
||||
expectSuccess(response);
|
||||
const otp = response?.data?.args?.otp
|
||||
const otp = response?.data?.args?.otp;
|
||||
if (!otp) {
|
||||
throw new Error(`Response does not contain an otp property: ${JSON.stringify(response.data, null, 2)}`);
|
||||
throw new Error(`Response does not contain an otp property: ${JSON.stringify(response.data, null, 2)}`);
|
||||
}
|
||||
return otp;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCodeFromSink(recipient: string): Promise<any> {
|
||||
return awaitNotification.request({data: {recipient}}).then((response) => {
|
||||
return awaitNotification.request({ data: { recipient } }).then((response) => {
|
||||
expectSuccess(response);
|
||||
const code = response?.data?.args?.code
|
||||
const code = response?.data?.args?.code;
|
||||
if (!code) {
|
||||
throw new Error(`Response does not contain a code property: ${JSON.stringify(response.data, null, 2)}`);
|
||||
}
|
||||
return code;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function expectSuccess(response: GaxiosResponse): void {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import { registerWithPasskey } from "./register";
|
||||
import {activateOTP, addTOTP, addUser, eventualNewUser, getUserByUsername, removeUser} from "./zitadel";
|
||||
import {request} from 'gaxios';
|
||||
import { activateOTP, addTOTP, addUser, eventualNewUser, getUserByUsername, removeUser } from "./zitadel";
|
||||
|
||||
export interface userProps {
|
||||
email: string;
|
||||
@@ -112,7 +111,7 @@ export class PasswordUserWithOTP extends User {
|
||||
async ensure(page: Page) {
|
||||
await super.ensure(page);
|
||||
await activateOTP(this.getUserId(), this.type);
|
||||
await eventualNewUser(this.getUserId())
|
||||
await eventualNewUser(this.getUserId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +121,7 @@ export class PasswordUserWithTOTP extends User {
|
||||
async ensure(page: Page) {
|
||||
await super.ensure(page);
|
||||
this.secret = await addTOTP(this.getUserId());
|
||||
await eventualNewUser(this.getUserId())
|
||||
await eventualNewUser(this.getUserId());
|
||||
}
|
||||
|
||||
public getSecret(): string {
|
||||
|
@@ -3,11 +3,11 @@ import { createDigest, createRandomBytes } from "@otplib/plugin-crypto";
|
||||
import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two"; // use your chosen base32 plugin
|
||||
import axios from "axios";
|
||||
import dotenv from "dotenv";
|
||||
import { request } from "gaxios";
|
||||
import path from "path";
|
||||
import { OtpType, userProps } from "./user";
|
||||
import {request} from "gaxios";
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, "../env/.env") })
|
||||
dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
|
||||
|
||||
export async function addUser(props: userProps) {
|
||||
const body = {
|
||||
@@ -173,10 +173,10 @@ export function totp(secret: string) {
|
||||
export async function eventualNewUser(id: string) {
|
||||
return request({
|
||||
url: `${process.env.ZITADEL_API_URL}/v2/users/${id}`,
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
retryConfig: {
|
||||
statusCodesToRetry: [[404, 404]],
|
||||
@@ -184,7 +184,7 @@ export async function eventualNewUser(id: string) {
|
||||
totalTimeout: 10000, // 10 seconds
|
||||
onRetryAttempt: (error) => {
|
||||
console.warn(`Retrying to query new user ${id}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -46,6 +46,15 @@ DefaultInstance:
|
||||
HelpLink: "https://zitadel.com/docs"
|
||||
SupportEmail: "support@zitadel.com"
|
||||
DocsLink: "https://zitadel.com/docs"
|
||||
Features:
|
||||
LoginV2:
|
||||
Required: true
|
||||
|
||||
OIDC:
|
||||
DefaultLoginURLV2: "/ui/v2/login/login?authRequest="
|
||||
|
||||
SAML:
|
||||
DefaultLoginURLV2: "/ui/v2/login/login?authRequest="
|
||||
|
||||
Database:
|
||||
EventPushConnRatio: 0.2 # 4
|
||||
|
@@ -93,7 +93,7 @@ describe("verify invite", () => {
|
||||
stub("zitadel.user.v2.UserService", "VerifyInviteCode");
|
||||
|
||||
cy.visit("/verify?userId=221394658884845598&code=abc&invite=true");
|
||||
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl +"/authenticator/set");
|
||||
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/authenticator/set");
|
||||
});
|
||||
|
||||
it("shows an error if invite code validation failed", () => {
|
||||
|
@@ -95,7 +95,7 @@ describe("login", () => {
|
||||
});
|
||||
it("should redirect a user with password authentication to /password", () => {
|
||||
cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
|
||||
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl +"/password");
|
||||
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/password");
|
||||
});
|
||||
describe("with passkey prompt", () => {
|
||||
beforeEach(() => {
|
||||
@@ -166,7 +166,7 @@ describe("login", () => {
|
||||
|
||||
it("should redirect a user with passwordless authentication to /passkey", () => {
|
||||
cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
|
||||
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl +"/passkey");
|
||||
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/passkey");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -68,6 +68,6 @@ describe("register", () => {
|
||||
cy.get('input[type="checkbox"][value="privacypolicy"]').check();
|
||||
cy.get('input[type="checkbox"][value="tos"]').check();
|
||||
cy.get('button[type="submit"]').click();
|
||||
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl +"/passkey/set");
|
||||
cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl + "/passkey/set");
|
||||
});
|
||||
});
|
||||
|
@@ -54,7 +54,7 @@ variable "CORE_MOCK_TAG" {
|
||||
}
|
||||
|
||||
target "core-mock" {
|
||||
context = "apps/core-mock"
|
||||
context = "apps/login-test-integration/core-mock"
|
||||
contexts = {
|
||||
protos = "target:proto-files"
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
*
|
||||
|
||||
!/apps/login-test-integration
|
||||
/apps/login-test-integration/core-mock
|
||||
|
||||
**/*.md
|
||||
**/*.png
|
||||
|
Reference in New Issue
Block a user