Merge pull request #25 from zitadel/setup-unit-integration-tests

test: setup unit tests
This commit is contained in:
Max Peintner
2023-06-08 09:12:51 +02:00
committed by GitHub
33 changed files with 3922 additions and 1244 deletions

12
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,12 @@
### Definition of Ready
- [ ] I am happy with the code
- [ ] Short description of the feature/issue is added in the pr description
- [ ] PR is linked to the corresponding user story
- [ ] Acceptance criteria are met
- [ ] All open todos and follow ups are defined in a new ticket and justified
- [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented.
- [ ] Jest unit tests ensure that components produce expected outputs on different inputs.
- [ ] Cypress integration tests ensure that login app pages work as expected. The ZITADEL API is mocked.
- [ ] No debug or dead code
- [ ] My code has no repetitions

26
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Test
on:
pull_request:
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v2
- name: Setup pnpm 7
uses: pnpm/action-setup@v2
with:
version: 7
- name: Setup Node.js 16.x
uses: actions/setup-node@v2
with:
node-version: 16.x
- name: Install Dependencies
id: deps
run: pnpm install
- name: Test
id: test
run: pnpm test

4
.gitignore vendored
View File

@@ -15,5 +15,5 @@ public/dist
.turbo
packages/zitadel-server/src/app/proto
packages/zitadel-client/src/app/proto
apps/login/.vscode
.vscode
.idea

View File

@@ -38,6 +38,25 @@ 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.
### Testing
You can execute the following commands in the following directories:
- apps/login
- packages/zitadel-client
- packages/zitadel-server
- packages/zitadel-react
- packages/zitadel-next
- The projects root directory: all tests in the project are executed
```sh
# Run all once
pnpm test
# Rerun tests on file changes
pnpm test:watch
```
### Developing Against Your Local ZITADEL Instance
```sh

View File

@@ -44,6 +44,8 @@ Each package and app is 100% [TypeScript](https://www.typescriptlang.org/).
- `pnpm generate` - Build proto stubs for server and client package
- `pnpm build` - Build all packages and the login app
- `pnpm test` - Test all packages and the login app
- `pnpm test:watch` - Rerun tests on file change
- `pnpm dev` - Develop all packages and the login app
- `pnpm lint` - Lint all packages
- `pnpm changeset` - Generate a changeset

View File

View File

@@ -1,4 +1,4 @@
module.exports = {
extends: "next/core-web-vitals",
extends: ["next/core-web-vitals"],
ignorePatterns: ["external/**/*.ts"],
};

View File

@@ -0,0 +1,57 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import PasswordComplexity from "../ui/PasswordComplexity";
// TODO: Why does this not compile?
// import { ResourceOwnerType } from '@zitadel/server';
const matchesTitle = `Matches`;
const doesntMatchTitle = `Doesn't match`;
describe("<PasswordComplexity/>", () => {
describe.each`
settingsMinLength | password | expectSVGTitle
${5} | ${"Password1!"} | ${matchesTitle}
${30} | ${"Password1!"} | ${doesntMatchTitle}
${0} | ${"Password1!"} | ${matchesTitle}
${undefined} | ${"Password1!"} | ${false}
`(
`With settingsMinLength=$settingsMinLength, password=$password, expectSVGTitle=$expectSVGTitle`,
({ settingsMinLength, password, expectSVGTitle }) => {
const feedbackElementLabel = /password length/i;
beforeEach(() => {
render(
<PasswordComplexity
password={password}
equals
passwordComplexitySettings={{
minLength: settingsMinLength,
requiresLowercase: false,
requiresUppercase: false,
requiresNumber: false,
requiresSymbol: false,
resourceOwnerType: 0, // ResourceOwnerType.RESOURCE_OWNER_TYPE_UNSPECIFIED,
}}
/>
);
});
if (expectSVGTitle === false) {
it(`should not render the feedback element`, async () => {
await waitFor(() => {
expect(
screen.queryByText(feedbackElementLabel)
).not.toBeInTheDocument();
});
});
} else {
it(`Should show one SVG with title ${expectSVGTitle}`, async () => {
await waitFor(async () => {
const svg = within(
screen.getByText(feedbackElementLabel)
.parentElement as HTMLElement
).findByRole("img");
expect(await svg).toHaveTextContent(expectSVGTitle);
});
});
}
}
);
});

View File

@@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsxdev",
"types": ["node", "jest", "@testing-library/jest-dom"]
}
}

19
apps/login/jest.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { Config } from "@jest/types";
import { pathsToModuleNameMapper } from "ts-jest";
import { compilerOptions } from "./tsconfig.json";
export default async (): Promise<Config.InitialOptions> => {
return {
preset: "ts-jest",
transform: {
"^.+\\.tsx?$": ["ts-jest", { tsconfig: "./__test__/tsconfig.json" }],
},
setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: "<rootDir>/",
}),
testEnvironment: "jsdom",
testRegex: "/__test__/.*\\.test\\.tsx?$",
modulePathIgnorePatterns: ["cypress"],
};
};

View File

@@ -2,13 +2,15 @@
"name": "@zitadel/login",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"test": "jest",
"test:watch": "jest --watch",
"lint": "next lint && prettier --check .",
"lint:fix": "prettier --write .",
"lint-staged": "lint-staged",
"build": "next build",
"prestart": "build",
"start": "next start",
"test": "yarn prettier:check &nexarn lint",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next"
},
"git": {
@@ -39,24 +41,34 @@
},
"devDependencies": {
"@bufbuild/buf": "^1.14.0",
"@jest/types": "^29.5.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.1",
"@types/ms": "0.7.31",
"@types/node": "18.11.9",
"@types/react": "18.0.25",
"@types/react": "18.2.8",
"@types/react-dom": "18.0.9",
"@types/testing-library__jest-dom": "^5.14.6",
"@types/tinycolor2": "1.4.3",
"@types/uuid": "^9.0.1",
"@vercel/git-hooks": "1.0.0",
"@zitadel/tsconfig": "workspace:*",
"autoprefixer": "10.4.13",
"del-cli": "5.0.0",
"eslint-config-zitadel": "workspace:*",
"grpc-tools": "1.11.3",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"lint-staged": "13.0.3",
"make-dir-cli": "3.0.0",
"postcss": "8.4.21",
"prettier-plugin-tailwindcss": "0.1.13",
"tailwindcss": "3.2.4",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"ts-proto": "^1.139.0",
"typescript": "4.8.4",
"typescript": "5.0.4",
"zitadel-tailwind-config": "workspace:*"
}
}

View File

@@ -1,11 +1,17 @@
{
"extends": "@zitadel/tsconfig/nextjs.json",
"compilerOptions": {
"jsx": "preserve",
"rootDir": ".",
"baseUrl": ".",
"paths": {
"#/*": ["./*"]
},
"plugins": [{ "name": "next" }]
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]

View File

@@ -20,7 +20,9 @@ const check = (
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6 las la-check text-green-500 dark:text-green-500 mr-2 text-lg"
role="img"
>
<title>Matches</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
@@ -36,7 +38,9 @@ const cross = (
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
role="img"
>
<title>Doesn't match</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
@@ -60,12 +64,16 @@ export default function PasswordComplexity({
return (
<div className="mb-4 grid grid-cols-2 gap-x-8 gap-y-2">
{passwordComplexitySettings.minLength != undefined ? (
<div className="flex flex-row items-center">
{hasMinLength ? check : cross}
<span className={desc}>
Password length {passwordComplexitySettings.minLength}
</span>
</div>
) : (
<span />
)}
<div className="flex flex-row items-center">
{hasSymbol ? check : cross}
<span className={desc}>has Symbol</span>

View File

@@ -28,7 +28,9 @@ export default function VerifyEmailForm({ userId, code, submit }: Props) {
useEffect(() => {
if (submit && code && userId) {
submitCode({ code });
// When we navigate to this page, we always want to be redirected if submit is true and the parameters are valid.
// For programmatic verification, the /verifyemail API should be used.
submitCodeAndContinue({ code });
}
}, []);
@@ -53,12 +55,11 @@ export default function VerifyEmailForm({ userId, code, submit }: Props) {
const response = await res.json();
if (!res.ok) {
setLoading(false);
if (!res.ok) {
setError(response.details);
return Promise.reject(response);
} else {
setLoading(false);
return response;
}
}

View File

@@ -1,16 +1,17 @@
{
"private": true,
"scripts": {
"generate": "turbo run generate",
"build": "turbo run build",
"test": "turbo run test",
"test:watch": "turbo run test:watch",
"dev": "turbo run dev --no-cache --continue",
"lint": "turbo run lint",
"clean": "turbo run clean && rm -rf node_modules",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "turbo run build --filter=login^... && changeset publish",
"prebuild": "turbo run generate",
"generate": "turbo run generate"
"release": "turbo run build --filter=login^... && changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.22.0",

View File

@@ -0,0 +1,8 @@
import type { JestConfigWithTsJest } from 'ts-jest'
const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'node'
}
export default jestConfig

View File

@@ -10,20 +10,27 @@
"dist/**"
],
"scripts": {
"generate": "buf generate https://github.com/zitadel/zitadel.git --path ./proto/zitadel",
"prebuild": "pnpm run generate",
"build": "tsup src/index.ts --format esm,cjs --dts",
"test": "jest",
"test:watch": "jest --watch",
"dev": "tsup src/index.ts --format esm,cjs --watch --dts",
"lint": "eslint \"src/**/*.ts*\"",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"generate": "buf generate https://github.com/zitadel/zitadel.git --path ./proto/zitadel"
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"devDependencies": {
"@bufbuild/buf": "^1.14.0",
"@types/jest": "^29.5.1",
"@zitadel/tsconfig": "workspace:*",
"eslint": "^7.32.0",
"eslint-config-zitadel": "workspace:*",
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"ts-proto": "^1.139.0",
"tsup": "^5.10.1",
"typescript": "^4.5.3"
"typescript": "^4.9.3"
},
"publishConfig": {
"access": "public"

View File

@@ -0,0 +1,57 @@
import { CallOptions, ClientMiddlewareCall, Metadata, MethodDescriptor } from "nice-grpc-web";
import { authMiddleware } from "./middleware";
describe('authMiddleware', () => {
const scenarios = [
{
name: 'should add authorization if metadata is undefined',
initialMetadata: undefined,
expectedMetadata: new Metadata().set("authorization", "Bearer mock-token"),
token: "mock-token"
},
{
name: 'should add authorization if metadata exists but no authorization',
initialMetadata: new Metadata().set("other-key", "other-value"),
expectedMetadata: new Metadata().set("other-key", "other-value").set("authorization", "Bearer mock-token"),
token: "mock-token"
},
{
name: 'should not modify authorization if it already exists',
initialMetadata: new Metadata().set("authorization", "Bearer initial-token"),
expectedMetadata: new Metadata().set("authorization", "Bearer initial-token"),
token: "mock-token"
},
];
scenarios.forEach(({ name, initialMetadata, expectedMetadata, token }) => {
it(name, async () => {
const mockNext = jest.fn().mockImplementation(async function*() { });
const mockRequest = {};
const mockMethodDescriptor: MethodDescriptor = {
options: {idempotencyLevel: undefined},
path: '',
requestStream: false,
responseStream: false,
};
const mockCall: ClientMiddlewareCall<unknown, unknown> = {
method: mockMethodDescriptor,
requestStream: false,
responseStream: false,
request: mockRequest,
next: mockNext,
};
const options: CallOptions = {
metadata: initialMetadata
};
await authMiddleware(token)(mockCall, options).next();
expect(mockNext).toHaveBeenCalledTimes(1);
const actualMetadata = mockNext.mock.calls[0][1].metadata;
expect(actualMetadata?.get('authorization')).toEqual(expectedMetadata.get('authorization'));
});
});
});

View File

@@ -0,0 +1,8 @@
import type { JestConfigWithTsJest } from 'ts-jest'
const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'node'
}
export default jestConfig

View File

@@ -11,16 +11,22 @@
],
"scripts": {
"build": "tsup src/index.tsx --format esm,cjs --dts --external react",
"test": "jest",
"test:watch": "jest --watch",
"dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react",
"lint": "eslint \"src/**/*.ts*\"",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"devDependencies": {
"@types/jest": "^29.5.1",
"@zitadel/tsconfig": "workspace:*",
"eslint": "^7.32.0",
"eslint-config-zitadel": "workspace:*",
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"tsup": "^5.10.1",
"typescript": "^4.5.3"
"typescript": "^4.9.3"
},
"peerDependencies": {
"next": "^13"

View File

@@ -0,0 +1,5 @@
describe('slug', () => {
it('this is not a real test', () => { })
})
export { }

View File

@@ -0,0 +1,9 @@
import type { JestConfigWithTsJest } from 'ts-jest'
const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: [ '@testing-library/jest-dom/extend-expect' ]
}
export default jestConfig

View File

@@ -13,23 +13,33 @@
},
"scripts": {
"build": "tsup",
"test": "jest",
"test:watch": "jest --watch",
"dev": "tsup --watch",
"lint": "eslint \"src/**/*.ts*\"",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"copy-files": "cp -R ./src/public/ ./dist/"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.1",
"@types/react": "^17.0.13",
"@types/react-dom": "^17.0.8",
"@types/testing-library__jest-dom": "^5.14.6",
"@zitadel/tsconfig": "workspace:*",
"autoprefixer": "10.4.13",
"eslint": "^7.32.0",
"eslint-config-zitadel": "workspace:*",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"postcss": "8.4.21",
"sass": "^1.62.0",
"tailwindcss": "3.2.4",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"tsup": "^5.10.1",
"typescript": "^4.5.3",
"typescript": "^4.9.3",
"zitadel-tailwind-config": "workspace:*"
},
"publishConfig": {

View File

@@ -0,0 +1,15 @@
import { render, screen } from '@testing-library/react';
import { SignInWithGoogle } from './SignInWithGoogle';
describe('<SignInWithGoogle />', () => {
it('renders without crashing', () => {
const { container } = render(<SignInWithGoogle />);
expect(container.firstChild).toBeDefined();
});
it('displays the correct text', () => {
render(<SignInWithGoogle />);
const signInText = screen.getByText(/Sign in with Google/i);
expect(signInText).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,15 @@
import { render, screen } from '@testing-library/react';
import { SignInWithGitlab } from './SignInWithGitlab';
describe('<SignInWithGitlab />', () => {
it('renders without crashing', () => {
const { container } = render(<SignInWithGitlab />);
expect(container.firstChild).toBeDefined();
});
it('displays the correct text', () => {
render(<SignInWithGitlab />);
const signInText = screen.getByText(/Sign in with Gitlab/i);
expect(signInText).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,8 @@
import type { JestConfigWithTsJest } from 'ts-jest'
const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'node'
}
export default jestConfig

View File

@@ -11,21 +11,27 @@
"dist/**"
],
"scripts": {
"generate": "buf generate https://github.com/zitadel/zitadel.git --path ./proto/zitadel",
"prebuild": "pnpm run generate",
"build": "tsup --dts",
"test": "jest",
"test:watch": "jest --watch",
"dev": "tsup --dts --watch",
"lint": "eslint \"src/**/*.ts*\"",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"prebuild": "pnpm run generate",
"generate": "buf generate https://github.com/zitadel/zitadel.git --path ./proto/zitadel"
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"devDependencies": {
"@bufbuild/buf": "^1.14.0",
"@types/jest": "^29.5.1",
"@zitadel/tsconfig": "workspace:*",
"eslint": "^7.32.0",
"eslint-config-zitadel": "workspace:*",
"jest": "^29.5.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"ts-proto": "^1.139.0",
"tsup": "^5.10.1",
"typescript": "^4.5.3"
"typescript": "^4.9.3"
},
"publishConfig": {
"access": "public"

View File

@@ -32,6 +32,7 @@ export {
export { type LegalAndSupportSettings } from "./proto/server/zitadel/settings/v2alpha/legal_settings";
export { type PasswordComplexitySettings } from "./proto/server/zitadel/settings/v2alpha/password_settings";
export { type ResourceOwnerType } from "./proto/server/zitadel/settings/v2alpha/settings";
import {
getServers,

View File

@@ -0,0 +1,57 @@
import { CallOptions, ClientMiddlewareCall, Metadata, MethodDescriptor } from "nice-grpc";
import { authMiddleware } from "./middleware";
describe('authMiddleware', () => {
const scenarios = [
{
name: 'should add authorization if metadata is undefined',
initialMetadata: undefined,
expectedMetadata: new Metadata().set("authorization", "Bearer mock-token"),
token: "mock-token"
},
{
name: 'should add authorization if metadata exists but no authorization',
initialMetadata: new Metadata().set("other-key", "other-value"),
expectedMetadata: new Metadata().set("other-key", "other-value").set("authorization", "Bearer mock-token"),
token: "mock-token"
},
{
name: 'should not modify authorization if it already exists',
initialMetadata: new Metadata().set("authorization", "Bearer initial-token"),
expectedMetadata: new Metadata().set("authorization", "Bearer initial-token"),
token: "mock-token"
},
];
scenarios.forEach(({ name, initialMetadata, expectedMetadata, token }) => {
it(name, async () => {
const mockNext = jest.fn().mockImplementation(async function*() { });
const mockRequest = {};
const mockMethodDescriptor: MethodDescriptor = {
options: {idempotencyLevel: undefined},
path: '',
requestStream: false,
responseStream: false,
};
const mockCall: ClientMiddlewareCall<unknown, unknown> = {
method: mockMethodDescriptor,
requestStream: false,
responseStream: false,
request: mockRequest,
next: mockNext,
};
const options: CallOptions = {
metadata: initialMetadata
};
await authMiddleware(token)(mockCall, options).next();
expect(mockNext).toHaveBeenCalledTimes(1);
const actualMetadata = mockNext.mock.calls[0][1].metadata;
expect(actualMetadata?.get('authorization')).toEqual(expectedMetadata.get('authorization'));
});
});
});

View File

@@ -1,7 +1,7 @@
import { CallOptions, ClientMiddlewareCall, Metadata } from "nice-grpc";
import { CallOptions, ClientMiddleware, ClientMiddlewareCall, Metadata } from "nice-grpc";
export const authMiddleware = (token: string) =>
async function* <Request, Response>(
export function authMiddleware (token: string): ClientMiddleware {
return async function* <Request, Response>(
call: ClientMiddlewareCall<Request, Response>,
options: CallOptions
) {
@@ -12,6 +12,7 @@ export const authMiddleware = (token: string) =>
return yield* call.next(call.request, options);
};
}
export const orgMetadata = (orgId: string) =>
new Metadata({ "x-zitadel-orgid": orgId });

4663
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,34 @@
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"generate": {
"outputs": ["src/proto/**"],
"outputs": [
"src/proto/**"
],
"cache": true
},
"build": {
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"dependsOn": ["generate", "^build"]
"outputs": [
"dist/**",
".next/**",
"!.next/cache/**"
],
"dependsOn": [
"lint",
"generate",
"^build"
]
},
"test": {
"outputs": ["coverage/**"],
"dependsOn": []
"dependsOn": [
"generate",
"@zitadel/server#build"
]
},
"test:watch": {
"dependsOn": [
"generate",
"@zitadel/server#build"
]
},
"lint": {},
"dev": {
@@ -22,6 +40,16 @@
"cache": false
}
},
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["ZITADEL_API_URL", "ZITADEL_SERVICE_USER_TOKEN"]
"globalDependencies": [
"**/.env.*local"
],
"globalEnv": [
"ZITADEL_API_URL",
"ZITADEL_SERVICE_USER_TOKEN",
"ZITADEL_SYSTEM_API_URL",
"ZITADEL_SYSTEM_API_USERID",
"ZITADEL_SYSTEM_API_KEY",
"ZITADEL_ISSUER",
"ZITADEL_ADMIN_TOKEN"
]
}