Browse Source
- Introduced Password Grant Flow for user authentication, allowing direct login with email and password. - Updated `useAuth` composable to manage login and logout processes, including Single Sign-Out from Cidaas. - Enhanced user interface with a new `UserMenu` component displaying user information and logout functionality. - Updated homepage to show personalized greetings for logged-in users and a login prompt for guests. - Added logout confirmation page with a countdown redirect to the homepage. - Documented the implementation details and future enhancements for OAuth2 flows in CLAUDE.md and other relevant documentation. - Added test credentials and guidelines for automated testing in the new TESTING.md file.main
40 changed files with 1843 additions and 31 deletions
@ -0,0 +1,90 @@ |
|||
<script setup lang="ts"> |
|||
import { User, LogOut } from 'lucide-vue-next' |
|||
import { |
|||
Avatar, |
|||
AvatarFallback, |
|||
AvatarImage, |
|||
} from './ui/avatar' |
|||
import { |
|||
DropdownMenu, |
|||
DropdownMenuContent, |
|||
DropdownMenuItem, |
|||
DropdownMenuLabel, |
|||
DropdownMenuSeparator, |
|||
DropdownMenuTrigger, |
|||
} from './ui/dropdown-menu' |
|||
|
|||
const { user, logout } = useAuth() |
|||
|
|||
/** |
|||
* Get user initials for Avatar fallback |
|||
* Example: "Bastian Masanek" → "BM" |
|||
*/ |
|||
const userInitials = computed(() => { |
|||
if (!user.value) return '?' |
|||
|
|||
const first = user.value.firstName?.charAt(0)?.toUpperCase() || '' |
|||
const last = user.value.lastName?.charAt(0)?.toUpperCase() || '' |
|||
|
|||
return first + last || user.value.email?.charAt(0)?.toUpperCase() || '?' |
|||
}) |
|||
|
|||
/** |
|||
* Handle logout click |
|||
*/ |
|||
async function handleLogout() { |
|||
try { |
|||
await logout() |
|||
} catch (error) { |
|||
console.error('Logout error:', error) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenu> |
|||
<DropdownMenuTrigger as-child> |
|||
<button |
|||
class="flex items-center gap-2 rounded-full focus:outline-none focus:ring-2 focus:ring-experimenta-accent focus:ring-offset-2 focus:ring-offset-experimenta-purple transition-all hover:scale-105 hover:shadow-lg" |
|||
aria-label="Benutzermenü öffnen" |
|||
> |
|||
<Avatar class="h-12 w-12 border-3 border-experimenta-accent shadow-md bg-experimenta-accent"> |
|||
<AvatarImage :src="undefined" :alt="user?.firstName" /> |
|||
<AvatarFallback class="bg-experimenta-accent text-white font-bold text-base flex items-center justify-center w-full h-full"> |
|||
{{ userInitials }} |
|||
</AvatarFallback> |
|||
</Avatar> |
|||
</button> |
|||
</DropdownMenuTrigger> |
|||
|
|||
<DropdownMenuContent align="end" class="w-56 z-[200]"> |
|||
<!-- User email (non-interactive) --> |
|||
<DropdownMenuLabel class="font-normal"> |
|||
<div class="flex flex-col space-y-1"> |
|||
<p class="text-sm font-medium leading-none"> |
|||
{{ user?.firstName }} {{ user?.lastName }} |
|||
</p> |
|||
<p class="text-xs leading-none text-muted-foreground"> |
|||
{{ user?.email }} |
|||
</p> |
|||
</div> |
|||
</DropdownMenuLabel> |
|||
|
|||
<DropdownMenuSeparator /> |
|||
|
|||
<!-- Profile (disabled for now, placeholder for Phase 2+) --> |
|||
<DropdownMenuItem disabled> |
|||
<User class="mr-2 h-4 w-4" /> |
|||
<span>Profil</span> |
|||
</DropdownMenuItem> |
|||
|
|||
<DropdownMenuSeparator /> |
|||
|
|||
<!-- Logout --> |
|||
<DropdownMenuItem @click="handleLogout"> |
|||
<LogOut class="mr-2 h-4 w-4" /> |
|||
<span>Abmelden</span> |
|||
</DropdownMenuItem> |
|||
</DropdownMenuContent> |
|||
</DropdownMenu> |
|||
</template> |
|||
@ -0,0 +1,22 @@ |
|||
<script setup lang="ts"> |
|||
import type { HTMLAttributes } from "vue" |
|||
import type { AvatarVariants } from "." |
|||
import { AvatarRoot } from "reka-ui" |
|||
import { cn } from '~/lib/utils' |
|||
import { avatarVariant } from "." |
|||
|
|||
const props = withDefaults(defineProps<{ |
|||
class?: HTMLAttributes["class"] |
|||
size?: AvatarVariants["size"] |
|||
shape?: AvatarVariants["shape"] |
|||
}>(), { |
|||
size: "sm", |
|||
shape: "circle", |
|||
}) |
|||
</script> |
|||
|
|||
<template> |
|||
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)"> |
|||
<slot /> |
|||
</AvatarRoot> |
|||
</template> |
|||
@ -0,0 +1,12 @@ |
|||
<script setup lang="ts"> |
|||
import type { AvatarFallbackProps } from "reka-ui" |
|||
import { AvatarFallback } from "reka-ui" |
|||
|
|||
const props = defineProps<AvatarFallbackProps>() |
|||
</script> |
|||
|
|||
<template> |
|||
<AvatarFallback v-bind="props"> |
|||
<slot /> |
|||
</AvatarFallback> |
|||
</template> |
|||
@ -0,0 +1,12 @@ |
|||
<script setup lang="ts"> |
|||
import type { AvatarImageProps } from "reka-ui" |
|||
import { AvatarImage } from "reka-ui" |
|||
|
|||
const props = defineProps<AvatarImageProps>() |
|||
</script> |
|||
|
|||
<template> |
|||
<AvatarImage v-bind="props" class="h-full w-full object-cover"> |
|||
<slot /> |
|||
</AvatarImage> |
|||
</template> |
|||
@ -0,0 +1,25 @@ |
|||
import type { VariantProps } from "class-variance-authority" |
|||
import { cva } from "class-variance-authority" |
|||
|
|||
export { default as Avatar } from "./Avatar.vue" |
|||
export { default as AvatarFallback } from "./AvatarFallback.vue" |
|||
export { default as AvatarImage } from "./AvatarImage.vue" |
|||
|
|||
export const avatarVariant = cva( |
|||
"inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden", |
|||
{ |
|||
variants: { |
|||
size: { |
|||
sm: "h-10 w-10 text-xs", |
|||
base: "h-16 w-16 text-2xl", |
|||
lg: "h-32 w-32 text-5xl", |
|||
}, |
|||
shape: { |
|||
circle: "rounded-full", |
|||
square: "rounded-md", |
|||
}, |
|||
}, |
|||
}, |
|||
) |
|||
|
|||
export type AvatarVariants = VariantProps<typeof avatarVariant> |
|||
@ -0,0 +1,15 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuRootEmits, DropdownMenuRootProps } from "reka-ui" |
|||
import { DropdownMenuRoot, useForwardPropsEmits } from "reka-ui" |
|||
|
|||
const props = defineProps<DropdownMenuRootProps>() |
|||
const emits = defineEmits<DropdownMenuRootEmits>() |
|||
|
|||
const forwarded = useForwardPropsEmits(props, emits) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuRoot v-bind="forwarded"> |
|||
<slot /> |
|||
</DropdownMenuRoot> |
|||
</template> |
|||
@ -0,0 +1,37 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuCheckboxItemEmits, DropdownMenuCheckboxItemProps } from "reka-ui" |
|||
import type { HTMLAttributes } from "vue" |
|||
import { reactiveOmit } from "@vueuse/core" |
|||
import { Check } from "lucide-vue-next" |
|||
import { |
|||
DropdownMenuCheckboxItem, |
|||
|
|||
DropdownMenuItemIndicator, |
|||
useForwardPropsEmits, |
|||
} from "reka-ui" |
|||
import { cn } from '~/lib/utils' |
|||
|
|||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }>() |
|||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>() |
|||
|
|||
const delegatedProps = reactiveOmit(props, "class") |
|||
|
|||
const forwarded = useForwardPropsEmits(delegatedProps, emits) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuCheckboxItem |
|||
v-bind="forwarded" |
|||
:class=" cn( |
|||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', |
|||
props.class, |
|||
)" |
|||
> |
|||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> |
|||
<DropdownMenuItemIndicator> |
|||
<Check class="w-4 h-4" /> |
|||
</DropdownMenuItemIndicator> |
|||
</span> |
|||
<slot /> |
|||
</DropdownMenuCheckboxItem> |
|||
</template> |
|||
@ -0,0 +1,35 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuContentEmits, DropdownMenuContentProps } from "reka-ui" |
|||
import type { HTMLAttributes } from "vue" |
|||
import { reactiveOmit } from "@vueuse/core" |
|||
import { |
|||
DropdownMenuContent, |
|||
|
|||
DropdownMenuPortal, |
|||
useForwardPropsEmits, |
|||
} from "reka-ui" |
|||
import { cn } from '~/lib/utils' |
|||
|
|||
const props = withDefaults( |
|||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(), |
|||
{ |
|||
sideOffset: 4, |
|||
}, |
|||
) |
|||
const emits = defineEmits<DropdownMenuContentEmits>() |
|||
|
|||
const delegatedProps = reactiveOmit(props, "class") |
|||
|
|||
const forwarded = useForwardPropsEmits(delegatedProps, emits) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuPortal> |
|||
<DropdownMenuContent |
|||
v-bind="forwarded" |
|||
:class="cn('z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)" |
|||
> |
|||
<slot /> |
|||
</DropdownMenuContent> |
|||
</DropdownMenuPortal> |
|||
</template> |
|||
@ -0,0 +1,12 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuGroupProps } from "reka-ui" |
|||
import { DropdownMenuGroup } from "reka-ui" |
|||
|
|||
const props = defineProps<DropdownMenuGroupProps>() |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuGroup v-bind="props"> |
|||
<slot /> |
|||
</DropdownMenuGroup> |
|||
</template> |
|||
@ -0,0 +1,26 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuItemProps } from "reka-ui" |
|||
import type { HTMLAttributes } from "vue" |
|||
import { reactiveOmit } from "@vueuse/core" |
|||
import { DropdownMenuItem, useForwardProps } from "reka-ui" |
|||
import { cn } from '~/lib/utils' |
|||
|
|||
const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes["class"], inset?: boolean }>() |
|||
|
|||
const delegatedProps = reactiveOmit(props, "class") |
|||
|
|||
const forwardedProps = useForwardProps(delegatedProps) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuItem |
|||
v-bind="forwardedProps" |
|||
:class="cn( |
|||
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0', |
|||
inset && 'pl-8', |
|||
props.class, |
|||
)" |
|||
> |
|||
<slot /> |
|||
</DropdownMenuItem> |
|||
</template> |
|||
@ -0,0 +1,22 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuLabelProps } from "reka-ui" |
|||
import type { HTMLAttributes } from "vue" |
|||
import { reactiveOmit } from "@vueuse/core" |
|||
import { DropdownMenuLabel, useForwardProps } from "reka-ui" |
|||
import { cn } from '~/lib/utils' |
|||
|
|||
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes["class"], inset?: boolean }>() |
|||
|
|||
const delegatedProps = reactiveOmit(props, "class") |
|||
|
|||
const forwardedProps = useForwardProps(delegatedProps) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuLabel |
|||
v-bind="forwardedProps" |
|||
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)" |
|||
> |
|||
<slot /> |
|||
</DropdownMenuLabel> |
|||
</template> |
|||
@ -0,0 +1,19 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuRadioGroupEmits, DropdownMenuRadioGroupProps } from "reka-ui" |
|||
import { |
|||
DropdownMenuRadioGroup, |
|||
|
|||
useForwardPropsEmits, |
|||
} from "reka-ui" |
|||
|
|||
const props = defineProps<DropdownMenuRadioGroupProps>() |
|||
const emits = defineEmits<DropdownMenuRadioGroupEmits>() |
|||
|
|||
const forwarded = useForwardPropsEmits(props, emits) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuRadioGroup v-bind="forwarded"> |
|||
<slot /> |
|||
</DropdownMenuRadioGroup> |
|||
</template> |
|||
@ -0,0 +1,38 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuRadioItemEmits, DropdownMenuRadioItemProps } from "reka-ui" |
|||
import type { HTMLAttributes } from "vue" |
|||
import { reactiveOmit } from "@vueuse/core" |
|||
import { Circle } from "lucide-vue-next" |
|||
import { |
|||
DropdownMenuItemIndicator, |
|||
DropdownMenuRadioItem, |
|||
|
|||
useForwardPropsEmits, |
|||
} from "reka-ui" |
|||
import { cn } from '~/lib/utils' |
|||
|
|||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }>() |
|||
|
|||
const emits = defineEmits<DropdownMenuRadioItemEmits>() |
|||
|
|||
const delegatedProps = reactiveOmit(props, "class") |
|||
|
|||
const forwarded = useForwardPropsEmits(delegatedProps, emits) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuRadioItem |
|||
v-bind="forwarded" |
|||
:class="cn( |
|||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', |
|||
props.class, |
|||
)" |
|||
> |
|||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> |
|||
<DropdownMenuItemIndicator> |
|||
<Circle class="h-2 w-2 fill-current" /> |
|||
</DropdownMenuItemIndicator> |
|||
</span> |
|||
<slot /> |
|||
</DropdownMenuRadioItem> |
|||
</template> |
|||
@ -0,0 +1,20 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuSeparatorProps } from "reka-ui" |
|||
import type { HTMLAttributes } from "vue" |
|||
import { reactiveOmit } from "@vueuse/core" |
|||
import { |
|||
DropdownMenuSeparator, |
|||
|
|||
} from "reka-ui" |
|||
import { cn } from '~/lib/utils' |
|||
|
|||
const props = defineProps<DropdownMenuSeparatorProps & { |
|||
class?: HTMLAttributes["class"] |
|||
}>() |
|||
|
|||
const delegatedProps = reactiveOmit(props, "class") |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" /> |
|||
</template> |
|||
@ -0,0 +1,14 @@ |
|||
<script setup lang="ts"> |
|||
import type { HTMLAttributes } from "vue" |
|||
import { cn } from '~/lib/utils' |
|||
|
|||
const props = defineProps<{ |
|||
class?: HTMLAttributes["class"] |
|||
}>() |
|||
</script> |
|||
|
|||
<template> |
|||
<span :class="cn('ml-auto text-xs tracking-widest opacity-60', props.class)"> |
|||
<slot /> |
|||
</span> |
|||
</template> |
|||
@ -0,0 +1,19 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuSubEmits, DropdownMenuSubProps } from "reka-ui" |
|||
import { |
|||
DropdownMenuSub, |
|||
|
|||
useForwardPropsEmits, |
|||
} from "reka-ui" |
|||
|
|||
const props = defineProps<DropdownMenuSubProps>() |
|||
const emits = defineEmits<DropdownMenuSubEmits>() |
|||
|
|||
const forwarded = useForwardPropsEmits(props, emits) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuSub v-bind="forwarded"> |
|||
<slot /> |
|||
</DropdownMenuSub> |
|||
</template> |
|||
@ -0,0 +1,27 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuSubContentEmits, DropdownMenuSubContentProps } from "reka-ui" |
|||
import type { HTMLAttributes } from "vue" |
|||
import { reactiveOmit } from "@vueuse/core" |
|||
import { |
|||
DropdownMenuSubContent, |
|||
|
|||
useForwardPropsEmits, |
|||
} from "reka-ui" |
|||
import { cn } from '~/lib/utils' |
|||
|
|||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }>() |
|||
const emits = defineEmits<DropdownMenuSubContentEmits>() |
|||
|
|||
const delegatedProps = reactiveOmit(props, "class") |
|||
|
|||
const forwarded = useForwardPropsEmits(delegatedProps, emits) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuSubContent |
|||
v-bind="forwarded" |
|||
:class="cn('z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)" |
|||
> |
|||
<slot /> |
|||
</DropdownMenuSubContent> |
|||
</template> |
|||
@ -0,0 +1,31 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuSubTriggerProps } from "reka-ui" |
|||
import type { HTMLAttributes } from "vue" |
|||
import { reactiveOmit } from "@vueuse/core" |
|||
import { ChevronRight } from "lucide-vue-next" |
|||
import { |
|||
DropdownMenuSubTrigger, |
|||
|
|||
useForwardProps, |
|||
} from "reka-ui" |
|||
import { cn } from '~/lib/utils' |
|||
|
|||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"] }>() |
|||
|
|||
const delegatedProps = reactiveOmit(props, "class") |
|||
|
|||
const forwardedProps = useForwardProps(delegatedProps) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuSubTrigger |
|||
v-bind="forwardedProps" |
|||
:class="cn( |
|||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent', |
|||
props.class, |
|||
)" |
|||
> |
|||
<slot /> |
|||
<ChevronRight class="ml-auto h-4 w-4" /> |
|||
</DropdownMenuSubTrigger> |
|||
</template> |
|||
@ -0,0 +1,14 @@ |
|||
<script setup lang="ts"> |
|||
import type { DropdownMenuTriggerProps } from "reka-ui" |
|||
import { DropdownMenuTrigger, useForwardProps } from "reka-ui" |
|||
|
|||
const props = defineProps<DropdownMenuTriggerProps>() |
|||
|
|||
const forwardedProps = useForwardProps(props) |
|||
</script> |
|||
|
|||
<template> |
|||
<DropdownMenuTrigger class="outline-none" v-bind="forwardedProps"> |
|||
<slot /> |
|||
</DropdownMenuTrigger> |
|||
</template> |
|||
@ -0,0 +1,16 @@ |
|||
export { default as DropdownMenu } from "./DropdownMenu.vue" |
|||
|
|||
export { default as DropdownMenuCheckboxItem } from "./DropdownMenuCheckboxItem.vue" |
|||
export { default as DropdownMenuContent } from "./DropdownMenuContent.vue" |
|||
export { default as DropdownMenuGroup } from "./DropdownMenuGroup.vue" |
|||
export { default as DropdownMenuItem } from "./DropdownMenuItem.vue" |
|||
export { default as DropdownMenuLabel } from "./DropdownMenuLabel.vue" |
|||
export { default as DropdownMenuRadioGroup } from "./DropdownMenuRadioGroup.vue" |
|||
export { default as DropdownMenuRadioItem } from "./DropdownMenuRadioItem.vue" |
|||
export { default as DropdownMenuSeparator } from "./DropdownMenuSeparator.vue" |
|||
export { default as DropdownMenuShortcut } from "./DropdownMenuShortcut.vue" |
|||
export { default as DropdownMenuSub } from "./DropdownMenuSub.vue" |
|||
export { default as DropdownMenuSubContent } from "./DropdownMenuSubContent.vue" |
|||
export { default as DropdownMenuSubTrigger } from "./DropdownMenuSubTrigger.vue" |
|||
export { default as DropdownMenuTrigger } from "./DropdownMenuTrigger.vue" |
|||
export { DropdownMenuPortal } from "reka-ui" |
|||
@ -0,0 +1,71 @@ |
|||
<script setup lang="ts"> |
|||
import { CheckCircle } from 'lucide-vue-next' |
|||
import { ref, onMounted, onUnmounted } from 'vue' |
|||
|
|||
// Countdown state (3 seconds) |
|||
const countdown = ref(3) |
|||
let countdownTimer: NodeJS.Timeout | null = null |
|||
|
|||
/** |
|||
* Start countdown and auto-redirect to homepage after 3 seconds |
|||
*/ |
|||
onMounted(() => { |
|||
countdownTimer = setInterval(() => { |
|||
countdown.value-- |
|||
|
|||
if (countdown.value <= 0) { |
|||
// Redirect to homepage |
|||
navigateTo('/') |
|||
} |
|||
}, 1000) // Update every 1 second |
|||
}) |
|||
|
|||
/** |
|||
* Cleanup timer on component unmount |
|||
*/ |
|||
onUnmounted(() => { |
|||
if (countdownTimer) { |
|||
clearInterval(countdownTimer) |
|||
} |
|||
}) |
|||
|
|||
/** |
|||
* Handle "Skip" button click - redirect immediately |
|||
*/ |
|||
function goToHomepage() { |
|||
navigateTo('/') |
|||
} |
|||
</script> |
|||
|
|||
<template> |
|||
<NuxtLayout name="default"> |
|||
<div class="flex items-center justify-center min-h-[60vh] px-4"> |
|||
<div class="max-w-md w-full text-center"> |
|||
<!-- Success Icon --> |
|||
<div class="mb-6 flex justify-center"> |
|||
<CheckCircle class="w-20 h-20 text-green-500" stroke-width="1.5" /> |
|||
</div> |
|||
|
|||
<!-- Heading --> |
|||
<h1 class="text-3xl font-bold mb-3 text-white"> |
|||
Erfolgreich abgemeldet |
|||
</h1> |
|||
|
|||
<!-- Message with countdown --> |
|||
<p class="text-lg text-white/80 mb-8"> |
|||
Du wirst in <span class="font-semibold text-experimenta-accent">{{ countdown }}</span> Sekunde{{ countdown !== 1 ? 'n' : '' }} zur Startseite weitergeleitet... |
|||
</p> |
|||
|
|||
<!-- Skip Button --> |
|||
<Button |
|||
variant="experimenta" |
|||
size="experimenta" |
|||
@click="goToHomepage" |
|||
class="min-w-[200px]" |
|||
> |
|||
Jetzt zur Startseite |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</NuxtLayout> |
|||
</template> |
|||
@ -0,0 +1,246 @@ |
|||
# Testing Guide |
|||
|
|||
This document provides testing credentials, test data, and guidelines for automated testing. |
|||
|
|||
--- |
|||
|
|||
## Test User Credentials (Staging) |
|||
|
|||
**⚠️ Important:** These credentials are **ONLY** for the **staging environment**. **NEVER** use them in production! |
|||
|
|||
### Cidaas Staging Test User |
|||
|
|||
- **Email:** `bm@noxware.de` |
|||
- **Password:** `%654321qQ!` |
|||
- **Environment:** `https://experimenta-staging.cidaas.de` |
|||
- **User ID (experimenta_id):** `97dcde33-d12e-4275-a0d5-e01cfbea37c2` |
|||
|
|||
**Usage:** |
|||
- Used by automated tests (Playwright E2E, Vitest integration tests) |
|||
- Manual testing during development |
|||
- Authentication flow validation |
|||
|
|||
**User Profile:** |
|||
- First Name: Bastian |
|||
- Last Name: Masanek |
|||
- Email verified: Yes |
|||
|
|||
--- |
|||
|
|||
## Setting Up Automated Tests |
|||
|
|||
### 1. Environment Variables |
|||
|
|||
Add these to your `.env` file for automated testing: |
|||
|
|||
```bash |
|||
# Test Credentials (Staging only - for automated testing) |
|||
TEST_USER_EMAIL=bm@noxware.de |
|||
TEST_USER_PASSWORD=%654321qQ! |
|||
``` |
|||
|
|||
### 2. Playwright E2E Tests |
|||
|
|||
Playwright tests use these credentials to test the complete authentication flow. |
|||
|
|||
**Example test:** |
|||
```typescript |
|||
// tests/e2e/auth.spec.ts |
|||
import { test, expect } from '@playwright/test' |
|||
|
|||
test('user can login with valid credentials', async ({ page }) => { |
|||
const email = process.env.TEST_USER_EMAIL! |
|||
const password = process.env.TEST_USER_PASSWORD! |
|||
|
|||
await page.goto('http://localhost:3000/auth') |
|||
await page.fill('input[type="email"]', email) |
|||
await page.fill('input[type="password"]', password) |
|||
await page.click('button[type="submit"]') |
|||
|
|||
// Verify successful login |
|||
await expect(page).toHaveURL('http://localhost:3000/') |
|||
await expect(page.locator('text=Willkommen zurück')).toBeVisible() |
|||
}) |
|||
``` |
|||
|
|||
**Run Playwright tests:** |
|||
```bash |
|||
pnpm test:e2e |
|||
``` |
|||
|
|||
### 3. Vitest Integration Tests |
|||
|
|||
Vitest tests use these credentials for API endpoint testing. |
|||
|
|||
**Example test:** |
|||
```typescript |
|||
// tests/integration/auth.test.ts |
|||
import { describe, it, expect } from 'vitest' |
|||
import { setup, $fetch } from '@nuxt/test-utils' |
|||
|
|||
describe('Authentication API', async () => { |
|||
await setup() |
|||
|
|||
it('POST /api/auth/login - successful login', async () => { |
|||
const response = await $fetch('/api/auth/login', { |
|||
method: 'POST', |
|||
body: { |
|||
email: process.env.TEST_USER_EMAIL, |
|||
password: process.env.TEST_USER_PASSWORD, |
|||
}, |
|||
}) |
|||
|
|||
expect(response.success).toBe(true) |
|||
}) |
|||
}) |
|||
``` |
|||
|
|||
**Run Vitest tests:** |
|||
```bash |
|||
pnpm test |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Test Data |
|||
|
|||
### Test Products (Mock Data for Development) |
|||
|
|||
For local development and testing, you can use these mock product IDs: |
|||
|
|||
```typescript |
|||
// Mock Makerspace Annual Pass |
|||
{ |
|||
navProductId: 'MAK-001', |
|||
name: 'Makerspace Jahreskarte', |
|||
description: 'Unbegrenzter Zugang zum Makerspace für 1 Jahr', |
|||
price: 120.00, |
|||
category: 'annual-pass', |
|||
stock: 100, |
|||
} |
|||
``` |
|||
|
|||
### Test Orders (Mock Data) |
|||
|
|||
```typescript |
|||
// Mock completed order |
|||
{ |
|||
orderNumber: 'TEST-2025-0001', |
|||
userId: '...', |
|||
status: 'completed', |
|||
totalAmount: 120.00, |
|||
paymentMethod: 'paypal', |
|||
paymentId: 'PAYPAL-TEST-12345', |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Testing Workflows |
|||
|
|||
### Complete Checkout Flow (E2E) |
|||
|
|||
1. **Login** with test credentials |
|||
2. **Browse products** and add to cart |
|||
3. **Proceed to checkout** |
|||
4. **Fill billing address** (pre-filled from test user profile) |
|||
5. **Complete PayPal payment** (sandbox) |
|||
6. **Verify order creation** in database |
|||
7. **Verify order submission** to X-API (staging) |
|||
|
|||
### Authentication Flow (Integration) |
|||
|
|||
1. **Register new user** via Cidaas API (staging) |
|||
2. **Verify email** (manual step in staging) |
|||
3. **Login** with new credentials |
|||
4. **Create session** and verify JWT token |
|||
5. **Access protected endpoints** with session |
|||
6. **Logout** and verify session cleared |
|||
|
|||
--- |
|||
|
|||
## CI/CD Integration |
|||
|
|||
### GitLab CI Environment Variables |
|||
|
|||
Add these secrets to GitLab CI/CD settings: |
|||
|
|||
- `TEST_USER_EMAIL` (Protected, Masked) |
|||
- `TEST_USER_PASSWORD` (Protected, Masked) |
|||
|
|||
**GitLab CI configuration:** |
|||
```yaml |
|||
test: |
|||
stage: test |
|||
script: |
|||
- pnpm install |
|||
- pnpm test |
|||
- pnpm test:e2e |
|||
variables: |
|||
TEST_USER_EMAIL: $TEST_USER_EMAIL |
|||
TEST_USER_PASSWORD: $TEST_USER_PASSWORD |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Security Best Practices |
|||
|
|||
### ✅ Do's |
|||
- Use test credentials **only** in staging environment |
|||
- Store credentials in environment variables (`.env`), never hardcode |
|||
- Use separate test user accounts (not real user accounts) |
|||
- Rotate test credentials regularly |
|||
- Add test credentials to GitLab CI/CD as protected, masked variables |
|||
|
|||
### ❌ Don'ts |
|||
- **Never** commit `.env` file to git (already in `.gitignore`) |
|||
- **Never** use test credentials in production environment |
|||
- **Never** use real user credentials for automated testing |
|||
- **Never** hardcode credentials in test files |
|||
- **Never** share test credentials publicly (GitHub, Slack, etc.) |
|||
|
|||
--- |
|||
|
|||
## Troubleshooting |
|||
|
|||
### Test User Login Fails |
|||
|
|||
**Problem:** Automated tests fail with "Invalid credentials" error |
|||
|
|||
**Solutions:** |
|||
1. Verify `TEST_USER_EMAIL` and `TEST_USER_PASSWORD` are set in `.env` |
|||
2. Check Cidaas staging environment is accessible |
|||
3. Verify test user account still exists in Cidaas |
|||
4. Check if password was changed in Cidaas Admin Panel |
|||
|
|||
### Session Tests Fail |
|||
|
|||
**Problem:** Session-related tests fail unexpectedly |
|||
|
|||
**Solutions:** |
|||
1. Verify `NUXT_SESSION_SECRET` is set in `.env` |
|||
2. Clear Redis cache: `docker-compose -f docker-compose.dev.yml restart redis` |
|||
3. Check session expiration settings in `nuxt.config.ts` |
|||
|
|||
### E2E Tests Time Out |
|||
|
|||
**Problem:** Playwright tests time out waiting for elements |
|||
|
|||
**Solutions:** |
|||
1. Increase timeout in `playwright.config.ts` |
|||
2. Check if dev server is running (`pnpm dev`) |
|||
3. Verify network connectivity to staging environment |
|||
4. Check browser console for JavaScript errors |
|||
|
|||
--- |
|||
|
|||
## Related Documentation |
|||
|
|||
- [CIDAAS_INTEGRATION.md](./CIDAAS_INTEGRATION.md) - Authentication implementation details |
|||
- [ARCHITECTURE.md](./ARCHITECTURE.md) - System architecture and data flows |
|||
- [PRD.md](./PRD.md) - Product requirements and user stories |
|||
- Main README: [../tests/README.md](../tests/README.md) - Test suite overview |
|||
|
|||
--- |
|||
|
|||
**Last Updated:** 2025-11-01 |
|||
@ -0,0 +1,18 @@ |
|||
/** |
|||
* GET /api/test/credentials |
|||
* |
|||
* Returns test user credentials for automated testing |
|||
* |
|||
* ⚠️ SECURITY: This endpoint is ONLY available in development mode. |
|||
* It returns 404 in production to prevent credential exposure. |
|||
* |
|||
* Usage in tests: |
|||
* ```typescript
|
|||
* const response = await fetch('http://localhost:3000/api/test/credentials') |
|||
* const { email, password } = await response.json() |
|||
* ``` |
|||
*/ |
|||
|
|||
import { createTestCredentialsEndpoint } from '../../utils/test-helpers' |
|||
|
|||
export default createTestCredentialsEndpoint() |
|||
@ -0,0 +1,103 @@ |
|||
/** |
|||
* Test Helper Utilities |
|||
* |
|||
* Provides utilities for automated testing (Playwright, Vitest E2E) |
|||
* |
|||
* ⚠️ IMPORTANT: These utilities should ONLY be used in test environments. |
|||
* Never use test credentials in production! |
|||
*/ |
|||
|
|||
/** |
|||
* Get test user credentials from environment variables |
|||
* |
|||
* @throws Error if test credentials are not configured |
|||
* @returns Test user email and password |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* // In a Playwright test
|
|||
* import { getTestCredentials } from '~/server/utils/test-helpers' |
|||
* |
|||
* const { email, password } = getTestCredentials() |
|||
* await page.fill('[name="email"]', email) |
|||
* await page.fill('[name="password"]', password) |
|||
* ``` |
|||
*/ |
|||
export function getTestCredentials() { |
|||
const config = useRuntimeConfig() |
|||
|
|||
const email = config.testUser.email |
|||
const password = config.testUser.password |
|||
|
|||
if (!email || !password) { |
|||
throw new Error( |
|||
'Test credentials not configured. Please set TEST_USER_EMAIL and TEST_USER_PASSWORD in .env' |
|||
) |
|||
} |
|||
|
|||
// Security check: Warn if used in production
|
|||
if (process.env.NODE_ENV === 'production') { |
|||
console.warn( |
|||
'⚠️ WARNING: Test credentials are being used in production environment! This is a security risk.' |
|||
) |
|||
} |
|||
|
|||
return { |
|||
email, |
|||
password, |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Check if test credentials are configured |
|||
* |
|||
* @returns true if test credentials are available |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* if (hasTestCredentials()) { |
|||
* // Run authenticated tests
|
|||
* const { email, password } = getTestCredentials() |
|||
* // ... test login flow
|
|||
* } else { |
|||
* console.log('Skipping authenticated tests - no test credentials configured') |
|||
* } |
|||
* ``` |
|||
*/ |
|||
export function hasTestCredentials(): boolean { |
|||
const config = useRuntimeConfig() |
|||
return !!(config.testUser.email && config.testUser.password) |
|||
} |
|||
|
|||
/** |
|||
* API endpoint to get test credentials |
|||
* ⚠️ ONLY available in development mode |
|||
* |
|||
* This endpoint allows tests to fetch credentials dynamically. |
|||
* It's automatically disabled in production. |
|||
* |
|||
* GET /api/test/credentials |
|||
* Returns: { email: string, password: string } |
|||
*/ |
|||
export function createTestCredentialsEndpoint() { |
|||
return defineEventHandler((event) => { |
|||
// SECURITY: Only allow in development
|
|||
if (process.env.NODE_ENV === 'production') { |
|||
throw createError({ |
|||
statusCode: 404, |
|||
statusMessage: 'Not Found', |
|||
}) |
|||
} |
|||
|
|||
try { |
|||
const credentials = getTestCredentials() |
|||
return credentials |
|||
} catch (error) { |
|||
throw createError({ |
|||
statusCode: 500, |
|||
statusMessage: |
|||
error instanceof Error ? error.message : 'Test credentials not configured', |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
@ -0,0 +1,302 @@ |
|||
# Testing Guide |
|||
|
|||
## Overview |
|||
|
|||
This document describes how to run tests for my.experimenta.science and how to use test credentials. |
|||
|
|||
--- |
|||
|
|||
## Test Credentials |
|||
|
|||
Test credentials are stored in environment variables to keep them out of the codebase while making them accessible for automated tests. |
|||
|
|||
**📖 For complete credentials and testing guide, see: [`docs/TESTING.md`](../docs/TESTING.md)** |
|||
|
|||
### Setup |
|||
|
|||
1. **Copy .env.example to .env:** |
|||
```bash |
|||
cp .env.example .env |
|||
``` |
|||
|
|||
2. **Set test credentials in .env:** |
|||
|
|||
See [`docs/TESTING.md`](../docs/TESTING.md) for current test credentials. |
|||
|
|||
```env |
|||
TEST_USER_EMAIL=<see-docs-TESTING-md> |
|||
TEST_USER_PASSWORD=<see-docs-TESTING-md> |
|||
``` |
|||
|
|||
3. **Verify configuration:** |
|||
```bash |
|||
# Start dev server |
|||
pnpm dev |
|||
|
|||
# In another terminal, test the credentials endpoint |
|||
curl http://localhost:3000/api/test/credentials |
|||
``` |
|||
|
|||
Expected output: |
|||
```json |
|||
{ |
|||
"email": "bm@noxware.de", |
|||
"password": "%654321qQ!" |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Using Test Credentials in Tests |
|||
|
|||
### Method 1: Via API Endpoint (Recommended for E2E tests) |
|||
|
|||
```typescript |
|||
// tests/e2e/example.spec.ts |
|||
import { test, expect } from '@playwright/test' |
|||
|
|||
test('login flow', async ({ page }) => { |
|||
// Fetch test credentials from API |
|||
const response = await fetch('http://localhost:3000/api/test/credentials') |
|||
|
|||
if (!response.ok) { |
|||
test.skip() // Skip if credentials not configured |
|||
return |
|||
} |
|||
|
|||
const { email, password } = await response.json() |
|||
|
|||
// Use credentials in test |
|||
await page.goto('/auth') |
|||
await page.fill('input[type="email"]', email) |
|||
await page.fill('input[type="password"]', password) |
|||
await page.click('button[type="submit"]') |
|||
|
|||
// Assert login success |
|||
await expect(page).toHaveURL('/') |
|||
}) |
|||
``` |
|||
|
|||
### Method 2: Via Server Utility (For server-side tests) |
|||
|
|||
```typescript |
|||
// tests/unit/auth.test.ts |
|||
import { describe, it, expect } from 'vitest' |
|||
import { getTestCredentials } from '~/server/utils/test-helpers' |
|||
|
|||
describe('Authentication', () => { |
|||
it('should login with test credentials', async () => { |
|||
const { email, password } = getTestCredentials() |
|||
|
|||
// Use credentials in test |
|||
const response = await $fetch('/api/auth/login', { |
|||
method: 'POST', |
|||
body: { email, password }, |
|||
}) |
|||
|
|||
expect(response.success).toBe(true) |
|||
}) |
|||
}) |
|||
``` |
|||
|
|||
### Method 3: Via Environment Variables (Direct access) |
|||
|
|||
```typescript |
|||
// For tests that need direct access |
|||
const email = process.env.TEST_USER_EMAIL |
|||
const password = process.env.TEST_USER_PASSWORD |
|||
|
|||
if (!email || !password) { |
|||
console.warn('Test credentials not configured, skipping test') |
|||
return |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Security Notes |
|||
|
|||
### ⚠️ Important Security Considerations |
|||
|
|||
1. **Development Only:** |
|||
- Test credentials should ONLY be used in development/staging |
|||
- The `/api/test/credentials` endpoint returns 404 in production |
|||
- Never commit real credentials to Git |
|||
|
|||
2. **Environment Variables:** |
|||
- `.env` is in `.gitignore` and never committed |
|||
- `.env.example` contains example/documentation only (not real passwords) |
|||
- Each developer should create their own `.env` file locally |
|||
|
|||
3. **Production Safety:** |
|||
- The test credentials endpoint is automatically disabled in production |
|||
- Server utility warns if used in production environment |
|||
- Test files are excluded from production builds |
|||
|
|||
4. **Credential Rotation:** |
|||
- Test credentials can be changed at any time in `.env` |
|||
- All tests will automatically use the updated credentials |
|||
- No code changes required when credentials change |
|||
|
|||
--- |
|||
|
|||
## Running Tests |
|||
|
|||
### Unit Tests (Vitest) |
|||
|
|||
```bash |
|||
# Run all unit tests |
|||
pnpm test |
|||
|
|||
# Run with coverage |
|||
pnpm test:coverage |
|||
|
|||
# Watch mode |
|||
pnpm test:watch |
|||
``` |
|||
|
|||
### E2E Tests (Playwright) |
|||
|
|||
```bash |
|||
# Run all E2E tests |
|||
pnpm test:e2e |
|||
|
|||
# Run specific test file |
|||
pnpm test:e2e tests/e2e/auth-login.example.spec.ts |
|||
|
|||
# Run in UI mode (visual debugger) |
|||
pnpm test:e2e --ui |
|||
|
|||
# Run in headed mode (see browser) |
|||
pnpm test:e2e --headed |
|||
``` |
|||
|
|||
### Before Running Tests |
|||
|
|||
1. ✅ Ensure `.env` is configured with test credentials |
|||
2. ✅ Start the dev server: `pnpm dev` |
|||
3. ✅ Start database: `docker-compose up -d` (if using Docker) |
|||
4. ✅ Run migrations: `pnpm db:migrate` |
|||
|
|||
--- |
|||
|
|||
## Test Structure |
|||
|
|||
``` |
|||
tests/ |
|||
├── README.md # This file |
|||
├── e2e/ # End-to-end tests (Playwright) |
|||
│ └── auth-login.example.spec.ts # Example E2E test with credentials |
|||
└── unit/ # Unit tests (Vitest) |
|||
└── (to be added) |
|||
``` |
|||
|
|||
--- |
|||
|
|||
## Example Tests |
|||
|
|||
See `tests/e2e/auth-login.example.spec.ts` for a complete example of using test credentials in E2E tests. |
|||
|
|||
--- |
|||
|
|||
## Troubleshooting |
|||
|
|||
### "Test credentials not configured" Error |
|||
|
|||
**Cause:** `TEST_USER_EMAIL` or `TEST_USER_PASSWORD` not set in `.env` |
|||
|
|||
**Solution:** |
|||
```bash |
|||
# 1. Check if .env exists |
|||
ls -la .env |
|||
|
|||
# 2. If not, copy from example |
|||
cp .env.example .env |
|||
|
|||
# 3. Edit .env and set credentials |
|||
# TEST_USER_EMAIL=bm@noxware.de |
|||
# TEST_USER_PASSWORD=%654321qQ! |
|||
|
|||
# 4. Restart dev server |
|||
``` |
|||
|
|||
### API Endpoint Returns 404 |
|||
|
|||
**Cause:** Server is running in production mode |
|||
|
|||
**Solution:** Test credentials endpoint is only available in development. Set: |
|||
```bash |
|||
NODE_ENV=development |
|||
``` |
|||
|
|||
### Tests Fail with "Invalid Credentials" |
|||
|
|||
**Possible causes:** |
|||
1. Wrong credentials in `.env` |
|||
2. Test user doesn't exist in Cidaas |
|||
3. Cidaas staging environment is down |
|||
|
|||
**Solution:** |
|||
- Verify credentials work manually by logging in at `/auth` |
|||
- Check Cidaas staging status |
|||
- Contact team lead for valid test account |
|||
|
|||
--- |
|||
|
|||
## Best Practices |
|||
|
|||
1. **Always check if credentials are configured:** |
|||
```typescript |
|||
if (!response.ok) { |
|||
test.skip() // Don't fail, just skip |
|||
return |
|||
} |
|||
``` |
|||
|
|||
2. **Use the API endpoint for E2E tests:** |
|||
- Cleaner separation of concerns |
|||
- Easier to mock/stub in CI/CD |
|||
- Works across different test frameworks |
|||
|
|||
3. **Clean up test data:** |
|||
```typescript |
|||
test.afterEach(async () => { |
|||
// Logout after each test |
|||
await $fetch('/api/auth/logout', { method: 'POST' }) |
|||
}) |
|||
``` |
|||
|
|||
4. **Isolate tests:** |
|||
- Each test should be independent |
|||
- Don't rely on state from previous tests |
|||
- Use `test.beforeEach` to reset state |
|||
|
|||
--- |
|||
|
|||
## CI/CD Integration |
|||
|
|||
For GitLab CI/CD, add test credentials as environment variables: |
|||
|
|||
```yaml |
|||
# .gitlab-ci.yml |
|||
test: |
|||
stage: test |
|||
variables: |
|||
TEST_USER_EMAIL: $CI_TEST_USER_EMAIL # Set in GitLab CI/CD settings |
|||
TEST_USER_PASSWORD: $CI_TEST_USER_PASSWORD |
|||
script: |
|||
- pnpm install |
|||
- pnpm test |
|||
- pnpm test:e2e |
|||
``` |
|||
|
|||
Set `CI_TEST_USER_EMAIL` and `CI_TEST_USER_PASSWORD` as protected variables in: |
|||
**GitLab → Settings → CI/CD → Variables** |
|||
|
|||
--- |
|||
|
|||
## Questions? |
|||
|
|||
- Check `CLAUDE.md` for authentication patterns |
|||
- Check `docs/CIDAAS_INTEGRATION.md` for Cidaas setup |
|||
- Ask in team chat or create an issue |
|||
@ -0,0 +1,81 @@ |
|||
/** |
|||
* E2E Test: Authentication - Login Flow |
|||
* |
|||
* This is an EXAMPLE test showing how to use test credentials. |
|||
* To run this test: |
|||
* |
|||
* 1. Copy .env.example to .env |
|||
* 2. Set TEST_USER_EMAIL and TEST_USER_PASSWORD |
|||
* 3. Run: pnpm test:e2e |
|||
* |
|||
* @example |
|||
* ```bash
|
|||
* # .env |
|||
* TEST_USER_EMAIL=bm@noxware.de |
|||
* TEST_USER_PASSWORD=%654321qQ! |
|||
* ``` |
|||
*/ |
|||
|
|||
import { test, expect } from '@playwright/test' |
|||
|
|||
test.describe('Authentication - Login Flow', () => { |
|||
test.beforeEach(async ({ page }) => { |
|||
// Navigate to auth page before each test
|
|||
await page.goto('/auth') |
|||
}) |
|||
|
|||
test('should login with test credentials from environment', async ({ page }) => { |
|||
// Fetch test credentials from API endpoint
|
|||
const response = await fetch('http://localhost:3000/api/test/credentials') |
|||
|
|||
// Skip test if credentials not configured
|
|||
if (!response.ok) { |
|||
test.skip() |
|||
return |
|||
} |
|||
|
|||
const { email, password } = await response.json() |
|||
|
|||
// Fill in login form
|
|||
await page.fill('input[type="email"]', email) |
|||
await page.fill('input[type="password"]', password) |
|||
|
|||
// Submit form
|
|||
await page.click('button[type="submit"]') |
|||
|
|||
// Wait for navigation to complete
|
|||
await page.waitForURL('/') |
|||
|
|||
// Verify we're on homepage
|
|||
expect(page.url()).toBe('http://localhost:3000/') |
|||
|
|||
// Optional: Verify user is logged in (check for user menu, etc.)
|
|||
// await expect(page.locator('[data-testid="user-menu"]')).toBeVisible()
|
|||
}) |
|||
|
|||
test('should show error with invalid credentials', async ({ page }) => { |
|||
// Fill in login form with invalid credentials
|
|||
await page.fill('input[type="email"]', 'invalid@example.com') |
|||
await page.fill('input[type="password"]', 'wrongpassword') |
|||
|
|||
// Submit form
|
|||
await page.click('button[type="submit"]') |
|||
|
|||
// Verify error message is shown
|
|||
await expect(page.locator('[role="alert"]')).toBeVisible() |
|||
await expect(page.locator('[role="alert"]')).toContainText('Invalid credentials') |
|||
}) |
|||
|
|||
test('should validate email format', async ({ page }) => { |
|||
// Fill in invalid email
|
|||
await page.fill('input[type="email"]', 'not-an-email') |
|||
await page.fill('input[type="password"]', 'somepassword') |
|||
|
|||
// Submit form
|
|||
await page.click('button[type="submit"]') |
|||
|
|||
// Verify validation error
|
|||
// Note: Exact selector depends on your validation error display
|
|||
await expect(page.locator('text=Invalid email')).toBeVisible() |
|||
}) |
|||
}) |
|||
Loading…
Reference in new issue