feat: adding tests..
This commit is contained in:
parent
d72c076658
commit
e48ba8b521
21 changed files with 1537 additions and 625 deletions
2
.env.ci
Normal file
2
.env.ci
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
APP_ENV=testing
|
||||
APP_KEY=base64:Zfo+2dkSaHvem5LCiZS/baYHv2Pv1vrSc0F2NaF29Ec=
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
<img src="public/images/trypost/logo-dark.png" alt="TryPost" width="200">
|
||||
</p>
|
||||
|
||||
<h1 align="center">TryPost</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>The open-source social media scheduling platform</strong>
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,10 @@ public function store(StoreWorkspaceRequest $request): RedirectResponse
|
|||
->with('message', 'Subscribe to create more workspaces.');
|
||||
}
|
||||
|
||||
$workspace = $user->workspaces()->create($request->validated());
|
||||
$workspace = $user->workspaces()->create([
|
||||
...$request->validated(),
|
||||
'timezone' => config('app.timezone', 'UTC'),
|
||||
]);
|
||||
|
||||
// Set as current workspace
|
||||
$user->switchWorkspace($workspace);
|
||||
|
|
@ -156,7 +159,7 @@ public function destroy(Request $request, Workspace $workspace): RedirectRespons
|
|||
$user->decrementWorkspaceQuantity();
|
||||
}
|
||||
|
||||
return redirect()->route('dashboard')
|
||||
return redirect()->route('workspaces.index')
|
||||
->with('success', 'Workspace deleted successfully!');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@
|
|||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'no-reply@trypost.it'),
|
||||
'name' => env('MAIL_FROM_NAME', 'TryPost'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -28,18 +28,9 @@ public function definition(): array
|
|||
'token' => Str::random(64),
|
||||
'role' => Role::Member,
|
||||
'status' => InviteStatus::Pending,
|
||||
'expires_at' => now()->addDays(7),
|
||||
];
|
||||
}
|
||||
|
||||
public function expired(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'expires_at' => now()->subDay(),
|
||||
'status' => InviteStatus::Expired,
|
||||
]);
|
||||
}
|
||||
|
||||
public function accepted(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
|
|
@ -47,11 +38,4 @@ public function accepted(): static
|
|||
'accepted_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function cancelled(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => InviteStatus::Cancelled,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ public function up(): void
|
|||
$table->string('token', 64)->unique();
|
||||
$table->string('role')->default('member');
|
||||
$table->string('status')->default('pending');
|
||||
$table->timestamp('expires_at');
|
||||
$table->timestamp('accepted_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
import { home, privacy, terms } from '@/routes';
|
||||
|
||||
defineProps<{
|
||||
title?: string;
|
||||
}>();
|
||||
|
|
@ -12,18 +9,10 @@ defineProps<{
|
|||
<!-- Header -->
|
||||
<header class="border-b">
|
||||
<div class="mx-auto max-w-5xl px-6 py-4 flex items-center justify-between">
|
||||
<Link :href="home()" class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="/images/trypost/logo-light.png" alt="TryPost" class="dark:hidden h-7 w-auto" />
|
||||
<img src="/images/trypost/logo-dark.png" alt="TryPost" class="hidden dark:block h-7 w-auto" />
|
||||
</Link>
|
||||
<nav class="flex items-center gap-4">
|
||||
<Link
|
||||
:href="home()"
|
||||
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -36,25 +25,9 @@ defineProps<{
|
|||
<!-- Footer -->
|
||||
<footer class="border-t mt-auto">
|
||||
<div class="mx-auto max-w-5xl px-6 py-8">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
© {{ new Date().getFullYear() }} TryPost. All rights reserved.
|
||||
</p>
|
||||
<nav class="flex items-center gap-6">
|
||||
<Link
|
||||
:href="privacy.url()"
|
||||
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link
|
||||
:href="terms.url()"
|
||||
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground text-center">
|
||||
© {{ new Date().getFullYear() }} TryPost. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,229 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { CalendarDays, Clock, Share2, Sparkles, CheckCircle } from 'lucide-vue-next';
|
||||
|
||||
import { calendar, login, privacy, register, terms } from '@/routes';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
canRegister: boolean;
|
||||
}>(),
|
||||
{
|
||||
canRegister: true,
|
||||
},
|
||||
);
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: CalendarDays,
|
||||
title: 'Visual Calendar',
|
||||
description: 'Plan and visualize your content with an intuitive calendar interface.',
|
||||
},
|
||||
{
|
||||
icon: Clock,
|
||||
title: 'Smart Scheduling',
|
||||
description: 'Schedule posts for the perfect time across all your social platforms.',
|
||||
},
|
||||
{
|
||||
icon: Share2,
|
||||
title: 'Multi-Platform',
|
||||
description: 'Publish to LinkedIn, X, TikTok, YouTube, Facebook, Instagram, and Threads.',
|
||||
},
|
||||
{
|
||||
icon: Sparkles,
|
||||
title: 'Workspace Collaboration',
|
||||
description: 'Invite team members and collaborate on your social media strategy.',
|
||||
},
|
||||
];
|
||||
|
||||
const platforms = [
|
||||
{ name: 'LinkedIn', logo: '/images/accounts/linkedin.png' },
|
||||
{ name: 'X', logo: '/images/accounts/x.png' },
|
||||
{ name: 'TikTok', logo: '/images/accounts/tiktok.png' },
|
||||
{ name: 'YouTube', logo: '/images/accounts/youtube.png' },
|
||||
{ name: 'Facebook', logo: '/images/accounts/facebook.png' },
|
||||
{ name: 'Instagram', logo: '/images/accounts/instagram.png' },
|
||||
{ name: 'Threads', logo: '/images/accounts/threads.png' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Social Media Scheduling Made Simple">
|
||||
<meta name="description" content="Schedule and publish your social media content to multiple platforms with TryPost. Support for LinkedIn, X, TikTok, YouTube, Facebook, Instagram, and Threads." />
|
||||
</Head>
|
||||
|
||||
<div class="min-h-svh bg-background">
|
||||
<!-- Header -->
|
||||
<header class="border-b">
|
||||
<div class="mx-auto max-w-6xl px-6 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="/images/trypost/logo-light.png" alt="TryPost" class="dark:hidden h-7 w-auto" />
|
||||
<img src="/images/trypost/logo-dark.png" alt="TryPost" class="hidden dark:block h-7 w-auto" />
|
||||
</div>
|
||||
<nav class="flex items-center gap-3">
|
||||
<Link
|
||||
v-if="$page.props.auth.user"
|
||||
:href="calendar.url()"
|
||||
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Calendar
|
||||
</Link>
|
||||
<template v-else>
|
||||
<Link :href="login()">
|
||||
<Button variant="ghost" size="sm">Log in</Button>
|
||||
</Link>
|
||||
<Link v-if="canRegister" :href="register()">
|
||||
<Button size="sm">Get Started</Button>
|
||||
</Link>
|
||||
</template>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="py-20 md:py-32">
|
||||
<div class="mx-auto max-w-6xl px-6 text-center">
|
||||
<h1 class="text-4xl md:text-6xl font-bold tracking-tight mb-6">
|
||||
Social Media Scheduling<br />
|
||||
<span class="text-primary">Made Simple</span>
|
||||
</h1>
|
||||
<p class="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto mb-8">
|
||||
Plan, schedule, and publish your content across all major social platforms from one place.
|
||||
Save time and grow your audience.
|
||||
</p>
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<Link v-if="canRegister" :href="register()">
|
||||
<Button size="lg">Start Free Trial</Button>
|
||||
</Link>
|
||||
<Link :href="login()">
|
||||
<Button variant="outline" size="lg">Log in</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Platforms Section -->
|
||||
<section class="py-16 bg-muted/30">
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<p class="text-center text-sm text-muted-foreground mb-8">
|
||||
Publish to all your favorite platforms
|
||||
</p>
|
||||
<div class="flex items-center justify-center gap-8 flex-wrap">
|
||||
<div
|
||||
v-for="platform in platforms"
|
||||
:key="platform.name"
|
||||
class="flex items-center gap-2 text-muted-foreground"
|
||||
>
|
||||
<img :src="platform.logo" :alt="platform.name" class="h-8 w-8" />
|
||||
<span class="text-sm font-medium">{{ platform.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="py-20">
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<h2 class="text-3xl font-bold text-center mb-12">
|
||||
Everything you need to manage your social presence
|
||||
</h2>
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div
|
||||
v-for="feature in features"
|
||||
:key="feature.title"
|
||||
class="p-6 rounded-lg border bg-card"
|
||||
>
|
||||
<component :is="feature.icon" class="h-10 w-10 text-primary mb-4" />
|
||||
<h3 class="font-semibold mb-2">{{ feature.title }}</h3>
|
||||
<p class="text-sm text-muted-foreground">{{ feature.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Benefits Section -->
|
||||
<section class="py-20 bg-muted/30">
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<div class="grid md:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<h2 class="text-3xl font-bold mb-6">
|
||||
Focus on creating, not scheduling
|
||||
</h2>
|
||||
<ul class="space-y-4">
|
||||
<li class="flex items-start gap-3">
|
||||
<CheckCircle class="h-5 w-5 text-green-500 mt-0.5 shrink-0" />
|
||||
<span>Schedule weeks of content in minutes</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<CheckCircle class="h-5 w-5 text-green-500 mt-0.5 shrink-0" />
|
||||
<span>Customize content for each platform</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<CheckCircle class="h-5 w-5 text-green-500 mt-0.5 shrink-0" />
|
||||
<span>Upload images and videos with ease</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<CheckCircle class="h-5 w-5 text-green-500 mt-0.5 shrink-0" />
|
||||
<span>Collaborate with your team</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<CheckCircle class="h-5 w-5 text-green-500 mt-0.5 shrink-0" />
|
||||
<span>Track your publishing history</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bg-card border rounded-lg p-8 text-center">
|
||||
<p class="text-4xl font-bold text-primary mb-2">$20</p>
|
||||
<p class="text-muted-foreground mb-6">per workspace / month</p>
|
||||
<Link v-if="canRegister" :href="register()">
|
||||
<Button class="w-full" size="lg">Start Free Trial</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="py-20">
|
||||
<div class="mx-auto max-w-6xl px-6 text-center">
|
||||
<h2 class="text-3xl font-bold mb-4">Ready to streamline your social media?</h2>
|
||||
<p class="text-muted-foreground mb-8">
|
||||
Join thousands of creators and businesses who trust TryPost.
|
||||
</p>
|
||||
<Link v-if="canRegister" :href="register()">
|
||||
<Button size="lg">Get Started Now</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t py-12">
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="/images/trypost/logo-light.png" alt="TryPost" class="dark:hidden h-6 w-auto" />
|
||||
<img src="/images/trypost/logo-dark.png" alt="TryPost" class="hidden dark:block h-6 w-auto" />
|
||||
</div>
|
||||
<nav class="flex items-center gap-6">
|
||||
<Link
|
||||
:href="privacy.url()"
|
||||
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link
|
||||
:href="terms.url()"
|
||||
class="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
</nav>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
© {{ new Date().getFullYear() }} TryPost. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import GuestLayout from '@/layouts/GuestLayout.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Privacy Policy" />
|
||||
|
||||
<GuestLayout title="Privacy Policy">
|
||||
<div class="prose prose-gray dark:prose-invert max-w-none">
|
||||
<p class="text-muted-foreground">Last updated: January 15, 2025</p>
|
||||
|
||||
<h2>1. Introduction</h2>
|
||||
<p>
|
||||
Welcome to TryPost ("we," "our," or "us"). We are committed to protecting your personal information
|
||||
and your right to privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard
|
||||
your information when you use our social media scheduling service.
|
||||
</p>
|
||||
|
||||
<h2>2. Information We Collect</h2>
|
||||
<h3>2.1 Personal Information</h3>
|
||||
<p>We collect personal information that you voluntarily provide to us when you:</p>
|
||||
<ul>
|
||||
<li>Register for an account</li>
|
||||
<li>Connect your social media accounts</li>
|
||||
<li>Create and schedule posts</li>
|
||||
<li>Contact us for support</li>
|
||||
</ul>
|
||||
<p>This information may include:</p>
|
||||
<ul>
|
||||
<li>Name and email address</li>
|
||||
<li>Social media account credentials and tokens</li>
|
||||
<li>Content you create for posting (text, images, videos)</li>
|
||||
<li>Billing information (processed securely by Stripe)</li>
|
||||
</ul>
|
||||
|
||||
<h3>2.2 Social Media Data</h3>
|
||||
<p>
|
||||
When you connect your social media accounts (Facebook, Instagram, LinkedIn, X/Twitter, TikTok, YouTube, Threads),
|
||||
we access certain information as permitted by each platform's API, including:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Account profile information (name, username, profile picture)</li>
|
||||
<li>Page/channel information for business accounts</li>
|
||||
<li>Access tokens required to post on your behalf</li>
|
||||
</ul>
|
||||
|
||||
<h3>2.3 Automatically Collected Information</h3>
|
||||
<p>We automatically collect certain information when you visit our platform, including:</p>
|
||||
<ul>
|
||||
<li>IP address and browser type</li>
|
||||
<li>Device information</li>
|
||||
<li>Usage data and analytics</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. How We Use Your Information</h2>
|
||||
<p>We use the information we collect to:</p>
|
||||
<ul>
|
||||
<li>Provide and maintain our service</li>
|
||||
<li>Schedule and publish your content to connected social media platforms</li>
|
||||
<li>Process your transactions and manage your subscription</li>
|
||||
<li>Send you service-related communications</li>
|
||||
<li>Respond to your inquiries and provide customer support</li>
|
||||
<li>Improve our service and develop new features</li>
|
||||
<li>Protect against fraud and unauthorized access</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. How We Share Your Information</h2>
|
||||
<p>We may share your information in the following situations:</p>
|
||||
<ul>
|
||||
<li><strong>With Social Media Platforms:</strong> We share your content with the platforms you've connected to publish your scheduled posts.</li>
|
||||
<li><strong>With Service Providers:</strong> We work with third-party service providers (hosting, payment processing, analytics) who need access to your information to perform services for us.</li>
|
||||
<li><strong>For Legal Compliance:</strong> We may disclose your information if required by law or in response to valid legal requests.</li>
|
||||
<li><strong>Business Transfers:</strong> In connection with any merger, sale, or acquisition of our business.</li>
|
||||
</ul>
|
||||
<p>We do not sell your personal information to third parties.</p>
|
||||
|
||||
<h2>5. Data Security</h2>
|
||||
<p>
|
||||
We implement appropriate technical and organizational security measures to protect your personal information.
|
||||
This includes encryption of sensitive data, secure HTTPS connections, and regular security audits.
|
||||
However, no method of transmission over the Internet is 100% secure, and we cannot guarantee absolute security.
|
||||
</p>
|
||||
|
||||
<h2>6. Data Retention</h2>
|
||||
<p>
|
||||
We retain your personal information for as long as your account is active or as needed to provide you services.
|
||||
You may request deletion of your account and associated data at any time by contacting us.
|
||||
</p>
|
||||
|
||||
<h2>7. Your Privacy Rights</h2>
|
||||
<p>Depending on your location, you may have the right to:</p>
|
||||
<ul>
|
||||
<li>Access the personal information we hold about you</li>
|
||||
<li>Request correction of inaccurate information</li>
|
||||
<li>Request deletion of your personal information</li>
|
||||
<li>Object to or restrict processing of your information</li>
|
||||
<li>Data portability</li>
|
||||
<li>Withdraw consent where processing is based on consent</li>
|
||||
</ul>
|
||||
<p>To exercise these rights, please contact us at the email address provided below.</p>
|
||||
|
||||
<h2>8. Third-Party Services</h2>
|
||||
<p>
|
||||
Our service integrates with third-party social media platforms. Each platform has its own privacy policy,
|
||||
and we encourage you to review them:
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="https://www.facebook.com/privacy/policy" target="_blank" rel="noopener">Meta (Facebook, Instagram, Threads) Privacy Policy</a></li>
|
||||
<li><a href="https://www.linkedin.com/legal/privacy-policy" target="_blank" rel="noopener">LinkedIn Privacy Policy</a></li>
|
||||
<li><a href="https://twitter.com/privacy" target="_blank" rel="noopener">X (Twitter) Privacy Policy</a></li>
|
||||
<li><a href="https://www.tiktok.com/legal/privacy-policy" target="_blank" rel="noopener">TikTok Privacy Policy</a></li>
|
||||
<li><a href="https://policies.google.com/privacy" target="_blank" rel="noopener">Google (YouTube) Privacy Policy</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>9. Children's Privacy</h2>
|
||||
<p>
|
||||
Our service is not intended for individuals under the age of 18. We do not knowingly collect personal
|
||||
information from children. If you believe we have collected information from a child, please contact us immediately.
|
||||
</p>
|
||||
|
||||
<h2>10. International Data Transfers</h2>
|
||||
<p>
|
||||
Your information may be transferred to and processed in countries other than your country of residence.
|
||||
These countries may have different data protection laws. We ensure appropriate safeguards are in place
|
||||
for such transfers.
|
||||
</p>
|
||||
|
||||
<h2>11. Changes to This Privacy Policy</h2>
|
||||
<p>
|
||||
We may update this Privacy Policy from time to time. We will notify you of any changes by posting the
|
||||
new Privacy Policy on this page and updating the "Last updated" date. We encourage you to review this
|
||||
Privacy Policy periodically.
|
||||
</p>
|
||||
|
||||
<h2>12. Contact Us</h2>
|
||||
<p>If you have questions or concerns about this Privacy Policy, please contact us at:</p>
|
||||
<p>
|
||||
<strong>Email:</strong> privacy@trypost.com<br />
|
||||
<strong>Website:</strong> https://trypost.com
|
||||
</p>
|
||||
</div>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import GuestLayout from '@/layouts/GuestLayout.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Terms of Service" />
|
||||
|
||||
<GuestLayout title="Terms of Service">
|
||||
<div class="prose prose-gray dark:prose-invert max-w-none">
|
||||
<p class="text-muted-foreground">Last updated: January 15, 2025</p>
|
||||
|
||||
<h2>1. Agreement to Terms</h2>
|
||||
<p>
|
||||
By accessing or using TryPost ("Service"), you agree to be bound by these Terms of Service ("Terms").
|
||||
If you disagree with any part of these terms, you may not access the Service.
|
||||
</p>
|
||||
|
||||
<h2>2. Description of Service</h2>
|
||||
<p>
|
||||
TryPost is a social media scheduling platform that allows you to create, schedule, and publish content
|
||||
to multiple social media platforms including Facebook, Instagram, LinkedIn, X (Twitter), TikTok, YouTube, and Threads.
|
||||
</p>
|
||||
|
||||
<h2>3. Account Registration</h2>
|
||||
<p>To use our Service, you must:</p>
|
||||
<ul>
|
||||
<li>Be at least 18 years old</li>
|
||||
<li>Provide accurate and complete registration information</li>
|
||||
<li>Maintain the security of your account credentials</li>
|
||||
<li>Notify us immediately of any unauthorized account use</li>
|
||||
</ul>
|
||||
<p>
|
||||
You are responsible for all activities that occur under your account. We reserve the right to suspend
|
||||
or terminate accounts that violate these Terms.
|
||||
</p>
|
||||
|
||||
<h2>4. Subscription and Payments</h2>
|
||||
<h3>4.1 Pricing</h3>
|
||||
<p>
|
||||
TryPost operates on a subscription model. Pricing is based on the number of workspaces you maintain.
|
||||
Current pricing and features are available on our website.
|
||||
</p>
|
||||
|
||||
<h3>4.2 Billing</h3>
|
||||
<p>
|
||||
Subscriptions are billed in advance on a recurring basis (monthly or annually, depending on your plan).
|
||||
Payment is processed securely through Stripe. By providing payment information, you authorize us to
|
||||
charge your payment method for the applicable fees.
|
||||
</p>
|
||||
|
||||
<h3>4.3 Refunds</h3>
|
||||
<p>
|
||||
Subscription fees are generally non-refundable except where required by law. You may cancel your
|
||||
subscription at any time, and you will continue to have access until the end of your billing period.
|
||||
</p>
|
||||
|
||||
<h2>5. User Content</h2>
|
||||
<h3>5.1 Ownership</h3>
|
||||
<p>
|
||||
You retain all rights to the content you create and schedule through our Service. By using TryPost,
|
||||
you grant us a limited license to store, process, and transmit your content solely for the purpose
|
||||
of providing the Service.
|
||||
</p>
|
||||
|
||||
<h3>5.2 Content Responsibility</h3>
|
||||
<p>You are solely responsible for the content you publish through our Service. You agree not to post content that:</p>
|
||||
<ul>
|
||||
<li>Violates any applicable laws or regulations</li>
|
||||
<li>Infringes on intellectual property rights of others</li>
|
||||
<li>Contains malware, spam, or malicious code</li>
|
||||
<li>Is defamatory, harassing, or discriminatory</li>
|
||||
<li>Violates the terms of service of connected social media platforms</li>
|
||||
</ul>
|
||||
|
||||
<h3>5.3 Content Removal</h3>
|
||||
<p>
|
||||
We reserve the right to remove any content that violates these Terms or applicable laws.
|
||||
We may also remove content in response to valid legal requests or DMCA takedown notices.
|
||||
</p>
|
||||
|
||||
<h2>6. Social Media Platform Compliance</h2>
|
||||
<p>
|
||||
When using TryPost to connect to third-party social media platforms, you agree to comply with each
|
||||
platform's terms of service and community guidelines. We are not responsible for any actions taken
|
||||
by social media platforms against your accounts.
|
||||
</p>
|
||||
|
||||
<h2>7. Prohibited Uses</h2>
|
||||
<p>You agree not to use the Service to:</p>
|
||||
<ul>
|
||||
<li>Violate any laws or regulations</li>
|
||||
<li>Impersonate others or misrepresent your affiliation</li>
|
||||
<li>Engage in automated data collection without authorization</li>
|
||||
<li>Attempt to gain unauthorized access to our systems</li>
|
||||
<li>Interfere with the proper operation of the Service</li>
|
||||
<li>Circumvent any security measures</li>
|
||||
<li>Use the Service for spam or bulk unsolicited messaging</li>
|
||||
</ul>
|
||||
|
||||
<h2>8. Intellectual Property</h2>
|
||||
<p>
|
||||
The Service, including its original content, features, and functionality, is owned by TryPost and
|
||||
is protected by international copyright, trademark, and other intellectual property laws. Our
|
||||
trademarks may not be used without prior written consent.
|
||||
</p>
|
||||
|
||||
<h2>9. Disclaimer of Warranties</h2>
|
||||
<p>
|
||||
THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS
|
||||
OR IMPLIED. WE DO NOT WARRANT THAT THE SERVICE WILL BE UNINTERRUPTED, SECURE, OR ERROR-FREE.
|
||||
</p>
|
||||
<p>
|
||||
We do not guarantee that posts will be published successfully, as this depends on third-party
|
||||
platform availability and their API functionality.
|
||||
</p>
|
||||
|
||||
<h2>10. Limitation of Liability</h2>
|
||||
<p>
|
||||
TO THE MAXIMUM EXTENT PERMITTED BY LAW, TRYPOST SHALL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
|
||||
SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA,
|
||||
OR BUSINESS OPPORTUNITIES, ARISING FROM YOUR USE OF THE SERVICE.
|
||||
</p>
|
||||
<p>
|
||||
Our total liability for any claims arising from these Terms or your use of the Service shall not
|
||||
exceed the amount you paid us in the twelve (12) months preceding the claim.
|
||||
</p>
|
||||
|
||||
<h2>11. Indemnification</h2>
|
||||
<p>
|
||||
You agree to indemnify and hold harmless TryPost, its officers, directors, employees, and agents
|
||||
from any claims, damages, losses, or expenses (including reasonable attorney fees) arising from
|
||||
your use of the Service or violation of these Terms.
|
||||
</p>
|
||||
|
||||
<h2>12. Termination</h2>
|
||||
<p>
|
||||
We may terminate or suspend your account immediately, without prior notice, for any breach of
|
||||
these Terms. Upon termination, your right to use the Service will cease immediately.
|
||||
</p>
|
||||
<p>
|
||||
You may terminate your account at any time by contacting us or through your account settings.
|
||||
Upon termination, we will delete your data in accordance with our Privacy Policy.
|
||||
</p>
|
||||
|
||||
<h2>13. Changes to Terms</h2>
|
||||
<p>
|
||||
We reserve the right to modify these Terms at any time. We will notify you of significant changes
|
||||
by posting the new Terms on this page and updating the "Last updated" date. Your continued use
|
||||
of the Service after changes constitute acceptance of the new Terms.
|
||||
</p>
|
||||
|
||||
<h2>14. Governing Law</h2>
|
||||
<p>
|
||||
These Terms shall be governed by and construed in accordance with applicable laws, without regard
|
||||
to conflict of law principles.
|
||||
</p>
|
||||
|
||||
<h2>15. Dispute Resolution</h2>
|
||||
<p>
|
||||
Any disputes arising from these Terms or your use of the Service shall first be attempted to be
|
||||
resolved through good faith negotiation. If negotiation fails, disputes shall be resolved through
|
||||
binding arbitration or in the courts of competent jurisdiction.
|
||||
</p>
|
||||
|
||||
<h2>16. Severability</h2>
|
||||
<p>
|
||||
If any provision of these Terms is found to be unenforceable, the remaining provisions will
|
||||
continue in full force and effect.
|
||||
</p>
|
||||
|
||||
<h2>17. Contact Information</h2>
|
||||
<p>For questions about these Terms, please contact us at:</p>
|
||||
<p>
|
||||
<strong>Email:</strong> legal@trypost.com<br />
|
||||
<strong>Website:</strong> https://trypost.com
|
||||
</p>
|
||||
</div>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
|
|
@ -22,22 +22,10 @@
|
|||
use App\Http\Controllers\WorkspaceLabelController;
|
||||
use App\Http\Middleware\EnsureUserSetupIsComplete;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
Route::get('/', function () {
|
||||
return Inertia::render('Welcome', [
|
||||
'canRegister' => Features::enabled(Features::registration()),
|
||||
]);
|
||||
})->name('home');
|
||||
|
||||
Route::get('/privacy', function () {
|
||||
return Inertia::render('legal/Privacy');
|
||||
})->name('privacy');
|
||||
|
||||
Route::get('/terms', function () {
|
||||
return Inertia::render('legal/Terms');
|
||||
})->name('terms');
|
||||
return redirect()->route('calendar');
|
||||
})->name('home')->middleware(['auth', 'verified']);
|
||||
|
||||
// Subscription selection (requires auth but not subscription)
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
|
|
|
|||
96
tests/Feature/BillingControllerTest.php
Normal file
96
tests/Feature/BillingControllerTest.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\User\Setup;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->user->update(['current_workspace_id' => $this->workspace->id]);
|
||||
});
|
||||
|
||||
// Subscribe tests
|
||||
test('subscribe requires authentication', function () {
|
||||
$response = $this->get(route('subscribe'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('subscribe shows subscription page', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('subscribe'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('billing/Subscribe', false)
|
||||
->has('trialDays')
|
||||
);
|
||||
});
|
||||
|
||||
// Index tests
|
||||
test('billing index requires authentication', function () {
|
||||
$response = $this->get(route('billing.index'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('billing index shows billing dashboard', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('billing.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('billing/Index', false)
|
||||
->has('hasSubscription')
|
||||
->has('workspacesCount')
|
||||
);
|
||||
});
|
||||
|
||||
// Processing tests
|
||||
test('billing processing requires authentication', function () {
|
||||
$response = $this->get(route('billing.processing'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('billing processing shows processing page', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('billing.processing'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('billing/Processing', false)
|
||||
->has('userId')
|
||||
->has('status')
|
||||
);
|
||||
});
|
||||
|
||||
test('billing processing accepts status parameter', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('billing.processing', ['status' => 'success']));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->where('status', 'success')
|
||||
);
|
||||
});
|
||||
|
||||
test('billing processing validates status parameter', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('billing.processing', ['status' => 'invalid']));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->where('status', 'processing')
|
||||
);
|
||||
});
|
||||
|
||||
// Checkout tests
|
||||
test('checkout requires authentication', function () {
|
||||
$response = $this->post(route('billing.checkout'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
// Portal tests
|
||||
test('portal requires authentication', function () {
|
||||
$response = $this->get(route('billing.portal'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
|
@ -1,7 +1,17 @@
|
|||
<?php
|
||||
|
||||
test('returns a successful response', function () {
|
||||
use App\Models\User;
|
||||
|
||||
test('home redirects to login for guests', function () {
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('home redirects to calendar for authenticated users', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('home'));
|
||||
|
||||
$response->assertRedirect(route('calendar'));
|
||||
});
|
||||
|
|
|
|||
140
tests/Feature/MediaControllerTest.php
Normal file
140
tests/Feature/MediaControllerTest.php
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\User\Setup;
|
||||
use App\Models\Media;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
beforeEach(function () {
|
||||
Storage::fake('local');
|
||||
$this->user = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->user->update(['current_workspace_id' => $this->workspace->id]);
|
||||
|
||||
$this->socialAccount = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
$this->post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
$this->postPlatform = PostPlatform::factory()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
]);
|
||||
});
|
||||
|
||||
// Store tests
|
||||
test('store media requires authentication', function () {
|
||||
$response = $this->post(route('medias.store'), [
|
||||
'model' => 'postPlatform',
|
||||
'model_id' => $this->postPlatform->id,
|
||||
'media' => UploadedFile::fake()->image('test.jpg'),
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('store media uploads file', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('medias.store'), [
|
||||
'model' => 'App\Models\PostPlatform',
|
||||
'model_id' => $this->postPlatform->id,
|
||||
'media' => UploadedFile::fake()->image('test.jpg'),
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonStructure(['id', 'url', 'type', 'original_filename']);
|
||||
});
|
||||
|
||||
test('store media validates required fields', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('medias.store'), [
|
||||
'model' => '',
|
||||
'model_id' => '',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors(['model', 'model_id', 'media']);
|
||||
});
|
||||
|
||||
// Destroy tests
|
||||
test('destroy media requires authentication', function () {
|
||||
$media = Media::factory()->create([
|
||||
'mediable_id' => $this->postPlatform->id,
|
||||
'mediable_type' => 'postPlatform',
|
||||
]);
|
||||
|
||||
$response = $this->delete(route('medias.destroy', [$this->postPlatform->id, $media]));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('destroy media deletes the media', function () {
|
||||
$media = Media::factory()->create([
|
||||
'mediable_id' => $this->postPlatform->id,
|
||||
'mediable_type' => 'postPlatform',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('medias.destroy', [$this->postPlatform->id, $media]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJson(['success' => true]);
|
||||
expect(Media::find($media->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('destroy media returns 403 for mismatched model', function () {
|
||||
$otherPostPlatform = PostPlatform::factory()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
]);
|
||||
|
||||
$media = Media::factory()->create([
|
||||
'mediable_id' => $otherPostPlatform->id,
|
||||
'mediable_type' => 'postPlatform',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('medias.destroy', [$this->postPlatform->id, $media]));
|
||||
|
||||
$response->assertForbidden();
|
||||
});
|
||||
|
||||
// Duplicate tests
|
||||
test('duplicate media requires authentication', function () {
|
||||
$media = Media::factory()->create([
|
||||
'mediable_id' => $this->postPlatform->id,
|
||||
'mediable_type' => 'postPlatform',
|
||||
]);
|
||||
|
||||
$response = $this->post(route('medias.duplicate', $media), [
|
||||
'targets' => [],
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('duplicate media creates copies', function () {
|
||||
$media = Media::factory()->create([
|
||||
'mediable_id' => $this->postPlatform->id,
|
||||
'mediable_type' => 'postPlatform',
|
||||
]);
|
||||
|
||||
$otherPostPlatform = PostPlatform::factory()->create([
|
||||
'post_id' => $this->post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('medias.duplicate', $media), [
|
||||
'targets' => [
|
||||
[
|
||||
'model' => 'postPlatform',
|
||||
'model_id' => $otherPostPlatform->id,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonCount(1);
|
||||
|
||||
expect(Media::where('mediable_id', $otherPostPlatform->id)->count())->toBe(1);
|
||||
});
|
||||
141
tests/Feature/OnboardingControllerTest.php
Normal file
141
tests/Feature/OnboardingControllerTest.php
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Enums\User\Persona;
|
||||
use App\Enums\User\Setup;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create(['setup' => Setup::Role]);
|
||||
});
|
||||
|
||||
// Step 1 tests
|
||||
test('step1 requires authentication', function () {
|
||||
$response = $this->get(route('onboarding.step1'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('step1 shows persona selection', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('onboarding.step1'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('onboarding/Step1', false)
|
||||
->has('personas')
|
||||
);
|
||||
});
|
||||
|
||||
// Store Step 1 tests
|
||||
test('store step1 requires authentication', function () {
|
||||
$response = $this->post(route('onboarding.step1.store'), [
|
||||
'persona' => Persona::Founder->value,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('store step1 saves persona and redirects to step2', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('onboarding.step1.store'), [
|
||||
'persona' => Persona::Founder->value,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('onboarding.step2'));
|
||||
|
||||
$this->user->refresh();
|
||||
expect($this->user->persona)->toBe(Persona::Founder);
|
||||
expect($this->user->setup)->toBe(Setup::Connections);
|
||||
});
|
||||
|
||||
test('store step1 validates persona is required', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('onboarding.step1.store'), [
|
||||
'persona' => '',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('persona');
|
||||
});
|
||||
|
||||
test('store step1 validates persona is valid enum', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('onboarding.step1.store'), [
|
||||
'persona' => 'invalid',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('persona');
|
||||
});
|
||||
|
||||
// Step 2 tests
|
||||
test('step2 requires authentication', function () {
|
||||
$response = $this->get(route('onboarding.step2'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('step2 shows social accounts connection', function () {
|
||||
$this->user->update(['setup' => Setup::Connections]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('onboarding.step2'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('onboarding/Step2', false)
|
||||
->has('platforms')
|
||||
->has('hasWorkspace')
|
||||
);
|
||||
});
|
||||
|
||||
test('step2 shows connected accounts for workspace', function () {
|
||||
$this->user->update(['setup' => Setup::Connections]);
|
||||
$workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->user->update(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
SocialAccount::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'platform' => Platform::LinkedIn,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('onboarding.step2'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->where('hasWorkspace', true)
|
||||
);
|
||||
});
|
||||
|
||||
// Store Step 2 tests
|
||||
test('store step2 requires authentication', function () {
|
||||
$response = $this->post(route('onboarding.step2.store'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('store step2 completes setup in self-hosted mode', function () {
|
||||
config(['trypost.self_hosted' => true]);
|
||||
$this->user->update(['setup' => Setup::Connections]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('onboarding.step2.store'));
|
||||
|
||||
$response->assertRedirect(route('calendar'));
|
||||
|
||||
$this->user->refresh();
|
||||
expect($this->user->setup)->toBe(Setup::Completed);
|
||||
});
|
||||
|
||||
// Complete tests
|
||||
test('complete requires authentication', function () {
|
||||
$response = $this->get(route('onboarding.complete'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('complete marks setup as completed', function () {
|
||||
$this->user->update(['setup' => Setup::Subscription]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('onboarding.complete'));
|
||||
|
||||
$response->assertRedirect(route('calendar'));
|
||||
|
||||
$this->user->refresh();
|
||||
expect($this->user->setup)->toBe(Setup::Completed);
|
||||
});
|
||||
289
tests/Feature/PostControllerTest.php
Normal file
289
tests/Feature/PostControllerTest.php
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\Post\Status as PostStatus;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Enums\User\Setup;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostPlatform;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->user->update(['current_workspace_id' => $this->workspace->id]);
|
||||
|
||||
$this->socialAccount = SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::LinkedIn,
|
||||
]);
|
||||
});
|
||||
|
||||
// Index tests
|
||||
test('posts index requires authentication', function () {
|
||||
$response = $this->get(route('posts.index'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('posts index shows posts for current workspace', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('posts.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('posts/Index', false)
|
||||
->has('posts.data', 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('posts index redirects to create workspace if no workspace', function () {
|
||||
$this->user->update(['current_workspace_id' => null]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('posts.index'));
|
||||
|
||||
$response->assertRedirect(route('workspaces.create'));
|
||||
});
|
||||
|
||||
// Calendar tests
|
||||
test('calendar requires authentication', function () {
|
||||
$response = $this->get(route('calendar'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('calendar shows posts for current week', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('calendar'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('posts/Calendar')
|
||||
->has('workspace')
|
||||
->has('posts')
|
||||
->has('currentWeekStart')
|
||||
->has('view')
|
||||
);
|
||||
});
|
||||
|
||||
test('calendar supports month view', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('calendar', ['view' => 'month']));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->where('view', 'month')
|
||||
);
|
||||
});
|
||||
|
||||
// Create tests
|
||||
test('create post requires authentication', function () {
|
||||
$response = $this->get(route('posts.create'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('create post redirects to accounts if no social accounts connected', function () {
|
||||
$this->socialAccount->delete();
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('posts.create'));
|
||||
|
||||
$response->assertRedirect(route('accounts'));
|
||||
});
|
||||
|
||||
test('create post creates draft and redirects to edit', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('posts.create'));
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
$post = Post::where('workspace_id', $this->workspace->id)->first();
|
||||
expect($post)->not->toBeNull();
|
||||
expect($post->status)->toBe(PostStatus::Draft);
|
||||
expect($post->postPlatforms)->toHaveCount(1);
|
||||
});
|
||||
|
||||
// Edit tests
|
||||
test('edit post requires authentication', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->get(route('posts.edit', $post));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('edit post shows edit page', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'status' => PostStatus::Draft,
|
||||
]);
|
||||
|
||||
PostPlatform::factory()->create([
|
||||
'post_id' => $post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('posts.edit', $post));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('posts/Edit')
|
||||
->has('post')
|
||||
->has('socialAccounts')
|
||||
);
|
||||
});
|
||||
|
||||
test('edit post returns 404 for post from different workspace', function () {
|
||||
$otherWorkspace = Workspace::factory()->create();
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $otherWorkspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('posts.edit', $post));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
test('edit post redirects to show for published posts', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'status' => PostStatus::Published,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('posts.edit', $post));
|
||||
|
||||
$response->assertRedirect(route('posts.show', $post));
|
||||
});
|
||||
|
||||
// Show tests
|
||||
test('show post requires authentication', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->get(route('posts.show', $post));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('show post displays post details', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
PostPlatform::factory()->create([
|
||||
'post_id' => $post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('posts.show', $post));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('posts/Show')
|
||||
->has('post')
|
||||
);
|
||||
});
|
||||
|
||||
// Update tests
|
||||
test('update post requires authentication', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->put(route('posts.update', $post), []);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('update post saves changes', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'status' => PostStatus::Draft,
|
||||
]);
|
||||
|
||||
$postPlatform = PostPlatform::factory()->create([
|
||||
'post_id' => $post->id,
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
'content' => 'Original content',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->put(route('posts.update', $post), [
|
||||
'status' => 'draft',
|
||||
'platforms' => [
|
||||
[
|
||||
'id' => $postPlatform->id,
|
||||
'content' => 'Updated content',
|
||||
'content_type' => ContentType::LinkedInPost->value,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
$postPlatform->refresh();
|
||||
expect($postPlatform->content)->toBe('Updated content');
|
||||
});
|
||||
|
||||
test('update post cannot update published posts', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'status' => PostStatus::Published,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->put(route('posts.update', $post), [
|
||||
'status' => 'draft',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
});
|
||||
|
||||
// Destroy tests
|
||||
test('destroy post requires authentication', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->delete(route('posts.destroy', $post));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('destroy post deletes the post', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('posts.destroy', $post));
|
||||
|
||||
$response->assertRedirect(route('calendar'));
|
||||
expect(Post::find($post->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('destroy post returns 404 for post from different workspace', function () {
|
||||
$otherWorkspace = Workspace::factory()->create();
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $otherWorkspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('posts.destroy', $post));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
71
tests/Feature/SocialControllerTest.php
Normal file
71
tests/Feature/SocialControllerTest.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Enums\User\Setup;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->user->update(['current_workspace_id' => $this->workspace->id]);
|
||||
});
|
||||
|
||||
// Index tests
|
||||
test('accounts index requires authentication', function () {
|
||||
$response = $this->get(route('accounts'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('accounts index shows platforms and connected accounts', function () {
|
||||
SocialAccount::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'platform' => Platform::LinkedIn,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('accounts'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('accounts/Index', false)
|
||||
->has('workspace')
|
||||
->has('platforms')
|
||||
);
|
||||
});
|
||||
|
||||
test('accounts index redirects if no workspace', function () {
|
||||
$this->user->update(['current_workspace_id' => null]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('accounts'));
|
||||
|
||||
$response->assertRedirect(route('workspaces.create'));
|
||||
});
|
||||
|
||||
// Disconnect tests
|
||||
test('disconnect requires authentication', function () {
|
||||
$account = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->delete(route('accounts.disconnect', $account));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('disconnect removes social account', function () {
|
||||
$account = SocialAccount::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('accounts.disconnect', $account));
|
||||
|
||||
$response->assertRedirect();
|
||||
expect(SocialAccount::find($account->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('disconnect returns 403 for other workspace account', function () {
|
||||
$otherWorkspace = Workspace::factory()->create();
|
||||
$account = SocialAccount::factory()->create(['workspace_id' => $otherWorkspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('accounts.disconnect', $account));
|
||||
|
||||
$response->assertForbidden();
|
||||
});
|
||||
237
tests/Feature/WorkspaceControllerTest.php
Normal file
237
tests/Feature/WorkspaceControllerTest.php
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\User\Setup;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->user->update(['current_workspace_id' => $this->workspace->id]);
|
||||
});
|
||||
|
||||
// Index tests
|
||||
test('workspaces index requires authentication', function () {
|
||||
$response = $this->get(route('workspaces.index'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('workspaces index shows all workspaces for user', function () {
|
||||
Workspace::factory()->count(2)->create(['user_id' => $this->user->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('workspaces.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('workspaces/Index', false)
|
||||
->has('workspaces', 3)
|
||||
->has('currentWorkspaceId')
|
||||
);
|
||||
});
|
||||
|
||||
// Create tests
|
||||
test('create workspace requires authentication', function () {
|
||||
$response = $this->get(route('workspaces.create'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('create workspace shows form for user with no workspaces', function () {
|
||||
// Delete existing workspace so user has none
|
||||
$this->user->update(['current_workspace_id' => null]);
|
||||
$this->workspace->delete();
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('workspaces.create'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('workspaces/Create', false)
|
||||
);
|
||||
});
|
||||
|
||||
test('create workspace redirects to billing if user already has workspace and no subscription', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('workspaces.create'));
|
||||
|
||||
$response->assertRedirect(route('billing.index'));
|
||||
});
|
||||
|
||||
// Store tests
|
||||
test('store workspace requires authentication', function () {
|
||||
$response = $this->post(route('workspaces.store'), ['name' => 'Test Workspace']);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('store workspace creates first workspace', function () {
|
||||
// Delete existing workspace so user has none
|
||||
$this->user->update(['current_workspace_id' => null]);
|
||||
$this->workspace->delete();
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('workspaces.store'), [
|
||||
'name' => 'New Workspace',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('calendar'));
|
||||
|
||||
$this->assertDatabaseHas('workspaces', [
|
||||
'name' => 'New Workspace',
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('store workspace redirects to billing for second workspace without subscription', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('workspaces.store'), [
|
||||
'name' => 'Second Workspace',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('billing.index'));
|
||||
});
|
||||
|
||||
test('store workspace validates name is required', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('workspaces.store'), [
|
||||
'name' => '',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('name');
|
||||
});
|
||||
|
||||
test('store workspace sets new workspace as current', function () {
|
||||
// Delete existing workspace so user has none
|
||||
$this->user->update(['current_workspace_id' => null]);
|
||||
$this->workspace->delete();
|
||||
|
||||
$this->actingAs($this->user)->post(route('workspaces.store'), [
|
||||
'name' => 'New Workspace',
|
||||
]);
|
||||
|
||||
$this->user->refresh();
|
||||
$newWorkspace = Workspace::where('name', 'New Workspace')->first();
|
||||
|
||||
expect($this->user->current_workspace_id)->toBe($newWorkspace->id);
|
||||
});
|
||||
|
||||
// Switch tests
|
||||
test('switch workspace requires authentication', function () {
|
||||
$response = $this->post(route('workspaces.switch', $this->workspace));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('switch workspace changes current workspace', function () {
|
||||
$otherWorkspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('workspaces.switch', $otherWorkspace));
|
||||
|
||||
$response->assertRedirect(route('calendar'));
|
||||
|
||||
$this->user->refresh();
|
||||
expect($this->user->current_workspace_id)->toBe($otherWorkspace->id);
|
||||
});
|
||||
|
||||
test('switch workspace returns 403 for workspace user does not belong to', function () {
|
||||
$otherUser = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$otherWorkspace = Workspace::factory()->create(['user_id' => $otherUser->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('workspaces.switch', $otherWorkspace));
|
||||
|
||||
$response->assertForbidden();
|
||||
});
|
||||
|
||||
// Settings tests
|
||||
test('workspace settings requires authentication', function () {
|
||||
$response = $this->get(route('workspace.settings'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('workspace settings shows settings page', function () {
|
||||
$response = $this->actingAs($this->user)->get(route('workspace.settings'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('settings/Workspace', false)
|
||||
->has('workspace')
|
||||
->has('timezones')
|
||||
);
|
||||
});
|
||||
|
||||
test('workspace settings redirects to create if no workspace', function () {
|
||||
$this->user->update(['current_workspace_id' => null]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('workspace.settings'));
|
||||
|
||||
$response->assertRedirect(route('workspaces.create'));
|
||||
});
|
||||
|
||||
// Update settings tests
|
||||
test('update workspace settings requires authentication', function () {
|
||||
$response = $this->put(route('workspace.settings.update'), [
|
||||
'name' => 'Updated Name',
|
||||
'timezone' => 'America/New_York',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('update workspace settings updates workspace', function () {
|
||||
$response = $this->actingAs($this->user)->put(route('workspace.settings.update'), [
|
||||
'name' => 'Updated Name',
|
||||
'timezone' => 'America/New_York',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('workspace.settings'));
|
||||
|
||||
$this->workspace->refresh();
|
||||
expect($this->workspace->name)->toBe('Updated Name');
|
||||
expect($this->workspace->timezone)->toBe('America/New_York');
|
||||
});
|
||||
|
||||
test('update workspace settings validates required fields', function () {
|
||||
$response = $this->actingAs($this->user)->put(route('workspace.settings.update'), [
|
||||
'name' => '',
|
||||
'timezone' => '',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors(['name', 'timezone']);
|
||||
});
|
||||
|
||||
test('update workspace settings validates timezone', function () {
|
||||
$response = $this->actingAs($this->user)->put(route('workspace.settings.update'), [
|
||||
'name' => 'Valid Name',
|
||||
'timezone' => 'Invalid/Timezone',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('timezone');
|
||||
});
|
||||
|
||||
// Destroy tests
|
||||
test('destroy workspace requires authentication', function () {
|
||||
$response = $this->delete(route('workspaces.destroy', $this->workspace));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('destroy workspace deletes the workspace', function () {
|
||||
$workspaceId = $this->workspace->id;
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('workspaces.destroy', $this->workspace));
|
||||
|
||||
$response->assertRedirect(route('workspaces.index'));
|
||||
expect(Workspace::find($workspaceId))->toBeNull();
|
||||
});
|
||||
|
||||
test('destroy workspace clears current workspace if deleting current', function () {
|
||||
$this->actingAs($this->user)->delete(route('workspaces.destroy', $this->workspace));
|
||||
|
||||
$this->user->refresh();
|
||||
expect($this->user->current_workspace_id)->toBeNull();
|
||||
});
|
||||
|
||||
test('destroy workspace returns 403 for non-owner', function () {
|
||||
$otherUser = User::factory()->create(['setup' => Setup::Completed]);
|
||||
|
||||
$response = $this->actingAs($otherUser)->delete(route('workspaces.destroy', $this->workspace));
|
||||
|
||||
$response->assertForbidden();
|
||||
});
|
||||
140
tests/Feature/WorkspaceHashtagControllerTest.php
Normal file
140
tests/Feature/WorkspaceHashtagControllerTest.php
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\User\Setup;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceHashtag;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->user->update(['current_workspace_id' => $this->workspace->id]);
|
||||
});
|
||||
|
||||
// Index tests
|
||||
test('hashtags index requires authentication', function () {
|
||||
$response = $this->get(route('hashtags.index'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('hashtags index shows hashtags for workspace', function () {
|
||||
WorkspaceHashtag::factory()->count(3)->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('hashtags.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('hashtags/Index', false)
|
||||
->has('workspace')
|
||||
->has('hashtags', 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('hashtags index redirects if no workspace', function () {
|
||||
$this->user->update(['current_workspace_id' => null]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('hashtags.index'));
|
||||
|
||||
$response->assertRedirect(route('workspaces.create'));
|
||||
});
|
||||
|
||||
// Store tests
|
||||
test('store hashtag requires authentication', function () {
|
||||
$response = $this->post(route('hashtags.store'), [
|
||||
'name' => 'Marketing',
|
||||
'hashtags' => '#marketing #digital #growth',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('store hashtag creates hashtag group', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('hashtags.store'), [
|
||||
'name' => 'Marketing',
|
||||
'hashtags' => '#marketing #digital #growth',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('hashtags.index'));
|
||||
|
||||
$this->assertDatabaseHas('workspace_hashtags', [
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'name' => 'Marketing',
|
||||
'hashtags' => '#marketing #digital #growth',
|
||||
]);
|
||||
});
|
||||
|
||||
test('store hashtag validates required fields', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('hashtags.store'), [
|
||||
'name' => '',
|
||||
'hashtags' => '',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors(['name', 'hashtags']);
|
||||
});
|
||||
|
||||
// Update tests
|
||||
test('update hashtag requires authentication', function () {
|
||||
$hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->put(route('hashtags.update', $hashtag), [
|
||||
'name' => 'Updated Name',
|
||||
'hashtags' => '#updated #hashtags',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('update hashtag updates the hashtag group', function () {
|
||||
$hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->put(route('hashtags.update', $hashtag), [
|
||||
'name' => 'Updated Name',
|
||||
'hashtags' => '#updated #hashtags',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('hashtags.index'));
|
||||
|
||||
$hashtag->refresh();
|
||||
expect($hashtag->name)->toBe('Updated Name');
|
||||
expect($hashtag->hashtags)->toBe('#updated #hashtags');
|
||||
});
|
||||
|
||||
test('update hashtag returns 404 for other workspace hashtag', function () {
|
||||
$otherWorkspace = Workspace::factory()->create();
|
||||
$hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $otherWorkspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->put(route('hashtags.update', $hashtag), [
|
||||
'name' => 'Updated Name',
|
||||
'hashtags' => '#updated #hashtags',
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// Destroy tests
|
||||
test('destroy hashtag requires authentication', function () {
|
||||
$hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->delete(route('hashtags.destroy', $hashtag));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('destroy hashtag deletes the hashtag group', function () {
|
||||
$hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('hashtags.destroy', $hashtag));
|
||||
|
||||
$response->assertRedirect(route('hashtags.index'));
|
||||
expect(WorkspaceHashtag::find($hashtag->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('destroy hashtag returns 404 for other workspace hashtag', function () {
|
||||
$otherWorkspace = Workspace::factory()->create();
|
||||
$hashtag = WorkspaceHashtag::factory()->create(['workspace_id' => $otherWorkspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('hashtags.destroy', $hashtag));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
245
tests/Feature/WorkspaceInviteControllerTest.php
Normal file
245
tests/Feature/WorkspaceInviteControllerTest.php
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\User\Setup;
|
||||
use App\Enums\UserWorkspace\Role as WorkspaceRole;
|
||||
use App\Mail\WorkspaceInvite as WorkspaceInviteMail;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceInvite;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
beforeEach(function () {
|
||||
Mail::fake();
|
||||
$this->user = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->user->update(['current_workspace_id' => $this->workspace->id]);
|
||||
});
|
||||
|
||||
// Index tests
|
||||
test('members index requires authentication', function () {
|
||||
$response = $this->get(route('members'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('members index shows members and invites', function () {
|
||||
$invite = WorkspaceInvite::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'invited_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('members'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('settings/Members', false)
|
||||
->has('workspace')
|
||||
->has('invites')
|
||||
->has('members')
|
||||
->has('owner')
|
||||
->has('roles')
|
||||
);
|
||||
});
|
||||
|
||||
test('members index redirects if no workspace', function () {
|
||||
$this->user->update(['current_workspace_id' => null]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('members'));
|
||||
|
||||
$response->assertRedirect(route('workspaces.create'));
|
||||
});
|
||||
|
||||
// Store invite tests
|
||||
test('store invite requires authentication', function () {
|
||||
$response = $this->post(route('invites.store'), [
|
||||
'email' => 'test@example.com',
|
||||
'role' => WorkspaceRole::Member->value,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('store invite creates invite and sends email', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('invites.store'), [
|
||||
'email' => 'newmember@example.com',
|
||||
'role' => WorkspaceRole::Member->value,
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('workspace_invites', [
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'email' => 'newmember@example.com',
|
||||
]);
|
||||
|
||||
Mail::assertQueued(WorkspaceInviteMail::class);
|
||||
});
|
||||
|
||||
test('store invite fails if invite already exists', function () {
|
||||
WorkspaceInvite::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'invited_by' => $this->user->id,
|
||||
'email' => 'existing@example.com',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('invites.store'), [
|
||||
'email' => 'existing@example.com',
|
||||
'role' => WorkspaceRole::Member->value,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('store invite fails if user is already member', function () {
|
||||
$member = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace->members()->attach($member->id, ['role' => WorkspaceRole::Member->value]);
|
||||
|
||||
$response = $this->actingAs($this->user)->post(route('invites.store'), [
|
||||
'email' => $member->email,
|
||||
'role' => WorkspaceRole::Member->value,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
// Destroy invite tests
|
||||
test('destroy invite requires authentication', function () {
|
||||
$invite = WorkspaceInvite::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'invited_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->delete(route('invites.destroy', $invite));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('destroy invite deletes invite', function () {
|
||||
$invite = WorkspaceInvite::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'invited_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('invites.destroy', $invite));
|
||||
|
||||
$response->assertRedirect();
|
||||
expect(WorkspaceInvite::find($invite->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('destroy invite returns 404 for other workspace invite', function () {
|
||||
$otherWorkspace = Workspace::factory()->create();
|
||||
$invite = WorkspaceInvite::factory()->create([
|
||||
'workspace_id' => $otherWorkspace->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('invites.destroy', $invite));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// Show invite tests
|
||||
test('show invite displays invite details', function () {
|
||||
$invite = WorkspaceInvite::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'invited_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->get(route('invites.show', $invite->token));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('invites/Accept', false)
|
||||
->has('invite')
|
||||
);
|
||||
});
|
||||
|
||||
test('show invite redirects for accepted invite', function () {
|
||||
$invite = WorkspaceInvite::factory()->accepted()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'invited_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->get(route('invites.show', $invite->token));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
// Accept invite tests
|
||||
test('accept invite redirects to login if not authenticated', function () {
|
||||
$invite = WorkspaceInvite::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'invited_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$response = $this->post(route('invites.accept', $invite->token));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('accept invite adds user to workspace', function () {
|
||||
$invite = WorkspaceInvite::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'invited_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$newUser = User::factory()->create(['setup' => Setup::Completed]);
|
||||
|
||||
$response = $this->actingAs($newUser)->post(route('invites.accept', $invite->token));
|
||||
|
||||
$response->assertRedirect(route('calendar'));
|
||||
expect($this->workspace->hasMember($newUser))->toBeTrue();
|
||||
});
|
||||
|
||||
test('accept invite redirects for accepted invite', function () {
|
||||
$invite = WorkspaceInvite::factory()->accepted()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'invited_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$newUser = User::factory()->create(['setup' => Setup::Completed]);
|
||||
|
||||
$response = $this->actingAs($newUser)->post(route('invites.accept', $invite->token));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('accept invite handles already member', function () {
|
||||
$invite = WorkspaceInvite::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'invited_by' => $this->user->id,
|
||||
]);
|
||||
|
||||
$member = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace->members()->attach($member->id, ['role' => WorkspaceRole::Member->value]);
|
||||
|
||||
$response = $this->actingAs($member)->post(route('invites.accept', $invite->token));
|
||||
|
||||
$response->assertRedirect(route('calendar'));
|
||||
$response->assertSessionHas('flash.banner', 'You are already a member of this workspace.');
|
||||
});
|
||||
|
||||
// Remove member tests
|
||||
test('remove member requires authentication', function () {
|
||||
$member = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace->members()->attach($member->id, ['role' => WorkspaceRole::Member->value]);
|
||||
|
||||
$response = $this->delete(route('members.remove', $member));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('remove member removes user from workspace', function () {
|
||||
$member = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace->members()->attach($member->id, ['role' => WorkspaceRole::Member->value]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('members.remove', $member));
|
||||
|
||||
$response->assertRedirect();
|
||||
expect($this->workspace->hasMember($member))->toBeFalse();
|
||||
});
|
||||
|
||||
test('remove member fails for owner', function () {
|
||||
$response = $this->actingAs($this->user)->delete(route('members.remove', $this->user));
|
||||
|
||||
$response->assertSessionHasErrors('member');
|
||||
});
|
||||
149
tests/Feature/WorkspaceLabelControllerTest.php
Normal file
149
tests/Feature/WorkspaceLabelControllerTest.php
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\User\Setup;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceLabel;
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create(['setup' => Setup::Completed]);
|
||||
$this->workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
|
||||
$this->user->update(['current_workspace_id' => $this->workspace->id]);
|
||||
});
|
||||
|
||||
// Index tests
|
||||
test('labels index requires authentication', function () {
|
||||
$response = $this->get(route('labels.index'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('labels index shows labels for workspace', function () {
|
||||
WorkspaceLabel::factory()->count(3)->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('labels.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('labels/Index', false)
|
||||
->has('workspace')
|
||||
->has('labels', 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('labels index redirects if no workspace', function () {
|
||||
$this->user->update(['current_workspace_id' => null]);
|
||||
|
||||
$response = $this->actingAs($this->user)->get(route('labels.index'));
|
||||
|
||||
$response->assertRedirect(route('workspaces.create'));
|
||||
});
|
||||
|
||||
// Store tests
|
||||
test('store label requires authentication', function () {
|
||||
$response = $this->post(route('labels.store'), [
|
||||
'name' => 'Test Label',
|
||||
'color' => '#FF5733',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('store label creates label', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('labels.store'), [
|
||||
'name' => 'New Label',
|
||||
'color' => '#FF5733',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('labels.index'));
|
||||
|
||||
$this->assertDatabaseHas('workspace_labels', [
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'name' => 'New Label',
|
||||
'color' => '#FF5733',
|
||||
]);
|
||||
});
|
||||
|
||||
test('store label validates required fields', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('labels.store'), [
|
||||
'name' => '',
|
||||
'color' => '',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors(['name', 'color']);
|
||||
});
|
||||
|
||||
test('store label validates color format', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('labels.store'), [
|
||||
'name' => 'Valid Name',
|
||||
'color' => 'invalid-color',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('color');
|
||||
});
|
||||
|
||||
// Update tests
|
||||
test('update label requires authentication', function () {
|
||||
$label = WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->put(route('labels.update', $label), [
|
||||
'name' => 'Updated Label',
|
||||
'color' => '#00FF00',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('update label updates the label', function () {
|
||||
$label = WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->put(route('labels.update', $label), [
|
||||
'name' => 'Updated Label',
|
||||
'color' => '#00FF00',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('labels.index'));
|
||||
|
||||
$label->refresh();
|
||||
expect($label->name)->toBe('Updated Label');
|
||||
expect($label->color)->toBe('#00FF00');
|
||||
});
|
||||
|
||||
test('update label returns 404 for other workspace label', function () {
|
||||
$otherWorkspace = Workspace::factory()->create();
|
||||
$label = WorkspaceLabel::factory()->create(['workspace_id' => $otherWorkspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->put(route('labels.update', $label), [
|
||||
'name' => 'Updated Label',
|
||||
'color' => '#00FF00',
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// Destroy tests
|
||||
test('destroy label requires authentication', function () {
|
||||
$label = WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->delete(route('labels.destroy', $label));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('destroy label deletes the label', function () {
|
||||
$label = WorkspaceLabel::factory()->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('labels.destroy', $label));
|
||||
|
||||
$response->assertRedirect(route('labels.index'));
|
||||
expect(WorkspaceLabel::find($label->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('destroy label returns 404 for other workspace label', function () {
|
||||
$otherWorkspace = Workspace::factory()->create();
|
||||
$label = WorkspaceLabel::factory()->create(['workspace_id' => $otherWorkspace->id]);
|
||||
|
||||
$response = $this->actingAs($this->user)->delete(route('labels.destroy', $label));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
Loading…
Reference in a new issue