Add E2E Test (#258)

This commit is contained in:
Rico Berger
2025-05-02 18:26:06 +02:00
committed by GitHub
parent 9ef3ce60ca
commit 7d85dff1f7
4 changed files with 715 additions and 7 deletions

View File

@@ -40,23 +40,44 @@ jobs:
deno:
name: Deno
runs-on: ubuntu-latest
defaults:
run:
working-directory: "supabase/functions"
env:
FEEDDECK_SUPABASE_URL: http://localhost:54321
FEEDDECK_SUPABASE_ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0
FEEDDECK_LOG_LEVEL: debug
FEEDDECK_SUPABASE_SERVICE_ROLE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Supabase
uses: supabase/setup-cli@v1
- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v1.45.2
- name: Start Supabase
run: |
echo "FEEDDECK_LOG_LEVEL=${FEEDDECK_LOG_LEVEL}" >> ./supabase/.env.test
echo "FEEDDECK_SUPABASE_URL=${FEEDDECK_SUPABASE_URL}" >> ./supabase/.env.test
echo "FEEDDECK_SUPABASE_ANON_KEY=${FEEDDECK_SUPABASE_ANON_KEY}" >> ./supabase/.env.test
echo "FEEDDECK_SUPABASE_SERVICE_ROLE_KEY=${FEEDDECK_SUPABASE_SERVICE_ROLE_KEY}" >> ./supabase/.env.test
supabase start
supabase db reset
supabase functions serve --no-verify-jwt --env-file supabase/.env.test &
psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "UPDATE settings SET value='http://kong:8000' WHERE name='supabase_api_url'"
psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "UPDATE settings SET value='${FEEDDECK_SUPABASE_SERVICE_ROLE_KEY}' WHERE name='supabase_service_role_key'"
- name: Lint
working-directory: "supabase/functions"
run: |
deno task lint
- name: Test
working-directory: "supabase/functions"
run: |
deno task test

View File

@@ -0,0 +1,688 @@
import { createClient, SupabaseClient } from "jsr:@supabase/supabase-js@2";
import {
assertEquals,
assertNotEquals,
} from "https://deno.land/std@0.208.0/assert/mod.ts";
import {
FEEDDECK_SUPABASE_URL,
FEEDDECK_SUPABASE_ANON_KEY,
FEEDDECK_SUPABASE_SERVICE_ROLE_KEY,
} from "../_shared/utils/constants.ts";
import {
assertEqualsItems,
assertEqualsSource,
} from "../_shared/feed/utils/test.ts";
interface IUser {
id: string;
email: string;
password: string;
deckId: string;
columnId: string;
client?: SupabaseClient;
}
const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
const supabaseAdmin = createClient(
FEEDDECK_SUPABASE_URL,
FEEDDECK_SUPABASE_SERVICE_ROLE_KEY,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
},
);
const createUser = async (user: IUser): Promise<void> => {
user.client = createClient(
FEEDDECK_SUPABASE_URL,
FEEDDECK_SUPABASE_ANON_KEY,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
},
);
const { data, error } = await user.client.auth.signUp({
email: user.email,
password: user.password,
});
assertEquals(error, null);
assertNotEquals(data, null);
user.id = data.user!.id;
};
Deno.test("E2E Tests", async (t) => {
const testUser1: IUser = {
id: "",
email: "testuser1@feeddeck.app",
password: "testuser1",
deckId: "",
columnId: "",
};
const testUser2: IUser = {
id: "",
email: "testuser2@feeddeck.app",
password: "testuser2",
deckId: "",
columnId: "",
};
await t.step("should create users", async () => {
await createUser(testUser1);
await createUser(testUser2);
});
await t.step("should not return settings", async () => {
const { data, error } = await testUser1.client!.from("settings").select();
assertEquals(error, null);
assertEquals(data, []);
});
await t.step("should not return profiles", async () => {
const { data, error } = await testUser1.client!.from("settings").select();
assertEquals(error, null);
assertEquals(data, []);
});
await t.step("should select / create / update / delete decks", async (t) => {
let tmpDeckId = "";
await t.step("should create decks", async () => {
const res1 = await testUser1
.client!.from("decks")
.insert({ name: "testuser1 deck1", userId: testUser1.id })
.select()
.single();
assertEquals(res1.error, null);
assertNotEquals(res1.data, null);
testUser1.deckId = res1.data.id;
const res2 = await testUser1
.client!.from("decks")
.insert({ name: "testuser1 deck2", userId: testUser1.id })
.select()
.single();
assertEquals(res2.error, null);
assertNotEquals(res2.data, null);
tmpDeckId = res2.data.id;
});
await t.step("should not create deck for other user", async () => {
const res1 = await testUser2
.client!.from("decks")
.insert({ name: "testuser1 deck1 by testuser2", userId: testUser1.id })
.select()
.single();
assertNotEquals(res1.error, null);
});
await t.step("should select deck", async () => {
const res1 = await testUser1
.client!.from("decks")
.select()
.eq("id", tmpDeckId)
.single();
assertEquals(res1.error, null);
assertNotEquals(res1.data, null);
assertEquals(res1.data.name, "testuser1 deck2");
});
await t.step("should not select deck of other user", async () => {
const res1 = await testUser2
.client!.from("decks")
.select()
.eq("id", tmpDeckId)
.single();
assertNotEquals(res1.error, null);
});
await t.step("should update deck", async () => {
const res1 = await testUser1
.client!.from("decks")
.update({ name: "testuser1 deck2 updated" })
.eq("id", tmpDeckId);
assertEquals(res1.error, null);
assertEquals(res1.data, null);
const res2 = await testUser1
.client!.from("decks")
.select()
.eq("id", tmpDeckId)
.single();
assertEquals(res2.error, null);
assertNotEquals(res2.data, null);
assertEquals(res2.data.name, "testuser1 deck2 updated");
});
await t.step("should not update deck of other user", async () => {
const res1 = await testUser2
.client!.from("decks")
.update({ name: "testuser1 deck2 updated by testuser2" })
.eq("id", tmpDeckId);
assertEquals(res1.error, null);
assertEquals(res1.data, null);
const res2 = await testUser1
.client!.from("decks")
.select()
.eq("id", tmpDeckId)
.single();
assertEquals(res2.error, null);
assertNotEquals(res2.data, null);
assertEquals(res2.data.name, "testuser1 deck2 updated");
});
await t.step("should not delete deck of other user", async () => {
const res1 = await testUser2
.client!.from("decks")
.delete()
.eq("id", tmpDeckId);
assertEquals(res1.error, null);
assertEquals(res1.data, null);
const res2 = await testUser1
.client!.from("decks")
.select()
.eq("id", tmpDeckId)
.single();
assertEquals(res2.error, null);
assertNotEquals(res2.data, null);
assertEquals(res2.data.name, "testuser1 deck2 updated");
});
await t.step("should delete deck", async () => {
const res1 = await testUser1
.client!.from("decks")
.delete()
.eq("id", tmpDeckId);
assertEquals(res1.error, null);
assertEquals(res1.data, null);
const res2 = await testUser1
.client!.from("decks")
.select()
.eq("id", tmpDeckId)
.single();
assertNotEquals(res2.error, null);
});
});
await t.step(
"should select / create / update / delete columns",
async (t) => {
let tmpColumnId = "";
await t.step("should create columns", async () => {
const res1 = await testUser1
.client!.from("columns")
.insert({
name: "testuser1 column1",
position: 0,
userId: testUser1.id,
deckId: testUser1.deckId,
})
.select()
.single();
assertEquals(res1.error, null);
assertNotEquals(res1.data, null);
testUser1.columnId = res1.data.id;
const res2 = await testUser1
.client!.from("columns")
.insert({
name: "testuser1 column2",
position: 0,
userId: testUser1.id,
deckId: testUser1.deckId,
})
.select()
.single();
assertEquals(res2.error, null);
assertNotEquals(res2.data, null);
tmpColumnId = res2.data.id;
});
await t.step("should not create column for other user", async () => {
const res1 = await testUser2
.client!.from("columns")
.insert({
name: "testuser1 column1 by testuser2",
postion: 0,
userId: testUser1.id,
deckId: testUser1.deckId,
})
.select()
.single();
assertNotEquals(res1.error, null);
});
await t.step("should select column", async () => {
const res1 = await testUser1
.client!.from("columns")
.select()
.eq("id", tmpColumnId)
.single();
assertEquals(res1.error, null);
assertNotEquals(res1.data, null);
assertEquals(res1.data.name, "testuser1 column2");
});
await t.step("should not select column of other user", async () => {
const res1 = await testUser2
.client!.from("columns")
.select()
.eq("id", tmpColumnId)
.single();
assertNotEquals(res1.error, null);
});
await t.step("should update column", async () => {
const res1 = await testUser1
.client!.from("columns")
.update({ name: "testuser1 column2 updated" })
.eq("id", tmpColumnId);
assertEquals(res1.error, null);
assertEquals(res1.data, null);
const res2 = await testUser1
.client!.from("columns")
.select()
.eq("id", tmpColumnId)
.single();
assertEquals(res2.error, null);
assertNotEquals(res2.data, null);
assertEquals(res2.data.name, "testuser1 column2 updated");
});
await t.step("should not update column of other user", async () => {
const res1 = await testUser2
.client!.from("columns")
.update({ name: "testuser1 column2 updated by testuser2" })
.eq("id", tmpColumnId);
assertEquals(res1.error, null);
assertEquals(res1.data, null);
const res2 = await testUser1
.client!.from("columns")
.select()
.eq("id", tmpColumnId)
.single();
assertEquals(res2.error, null);
assertNotEquals(res2.data, null);
assertEquals(res2.data.name, "testuser1 column2 updated");
});
await t.step("should not delete column of other user", async () => {
const res1 = await testUser2
.client!.from("columns")
.delete()
.eq("id", tmpColumnId);
assertEquals(res1.error, null);
assertEquals(res1.data, null);
const res2 = await testUser1
.client!.from("columns")
.select()
.eq("id", tmpColumnId)
.single();
assertEquals(res2.error, null);
assertNotEquals(res2.data, null);
assertEquals(res2.data.name, "testuser1 column2 updated");
});
await t.step("should delete column", async () => {
const res1 = await testUser1
.client!.from("columns")
.delete()
.eq("id", tmpColumnId);
assertEquals(res1.error, null);
assertEquals(res1.data, null);
const res2 = await testUser1
.client!.from("columns")
.select()
.eq("id", tmpColumnId)
.single();
assertNotEquals(res2.error, null);
});
},
);
await t.step(
"should select / create / update / delete sources and items",
async (t) => {
const sourceId = `rss-${testUser1.id}-${testUser1.columnId}-a08a0344cbce92eb2655d0a3f14e883c`;
const sourceIcon = `${testUser1.id}/rss-${testUser1.id}-${testUser1.columnId}-a08a0344cbce92eb2655d0a3f14e883c.png`;
const itemId = `rss-${testUser1.id}-${testUser1.columnId}-a08a0344cbce92eb2655d0a3f14e883c-38751244b5b754e61b9114cce1a1a091`;
await t.step("should not be able to create sources", async () => {
const res1 = await testUser1
.client!.from("sources")
.insert({
id: `rss-${testUser1.id}-${testUser1.columnId}-5581d70708fcfd1ae5039550429aa675`,
columnId: testUser1.columnId,
userId: testUser1.id,
type: "rss",
title: "FeedDeck",
options: { rss: "https://feeddeck.app/testdata/feed.xml" },
link: "https://feeddeck.app/",
icon: `${testUser1.id}/rss-${testUser1.id}-${testUser1.columnId}-5581d70708fcfd1ae5039550429aa675.png`,
})
.select()
.single();
assertNotEquals(res1.error, null);
});
await t.step("should not be able to create items", async () => {
const res1 = await testUser1
.client!.from("items")
.insert({
id: `rss-${testUser1.id}-${testUser1.columnId}-5581d70708fcfd1ae5039550429aa675-`,
userId: testUser1.id,
columnId: testUser1.columnId,
sourceId: `rss-${testUser1.id}-${testUser1.columnId}-5581d70708fcfd1ae5039550429aa675`,
title: "Test Data",
link: "https://feeddeck.app/testdata/feed.xml",
media: "https://feeddeck.app/testdata/image.jpg",
description:
'<p><img src="https://feeddeck.app/testdata/image.jpg" /><br/><br/>Test Data for the FeedDeck E2E Tests with an Image and <b>HTML Formatted Content</b>.</p>',
publishedAt: 1746187200,
})
.select()
.single();
assertNotEquals(res1.error, null);
});
await t.step("should create source and items via function", async () => {
const res1 = await testUser1.client!.functions.invoke(
"add-or-update-source-v1",
{
body: {
source: {
id: "",
columnId: testUser1.columnId,
userId: "",
type: "rss",
title: "",
options: {
rss: "https://feeddeck.app/testdata/feed.xml",
},
},
},
},
);
assertEquals(res1.error, null);
assertEquals(res1.data, {
columnId: testUser1.columnId,
icon: sourceIcon,
id: sourceId,
link: "https://feeddeck.app/",
options: {
rss: "https://feeddeck.app/testdata/feed.xml",
},
title: "FeedDeck",
type: "rss",
userId: testUser1.id,
});
});
await t.step("should select source", async () => {
const res1 = await testUser1
.client!.from("sources")
.select()
.eq("id", sourceId)
.single();
assertEquals(res1.error, null);
assertEqualsSource(res1.data, {
columnId: testUser1.columnId,
icon: sourceIcon,
id: sourceId,
link: "https://feeddeck.app/",
options: {
rss: "https://feeddeck.app/testdata/feed.xml",
},
title: "FeedDeck",
type: "rss",
userId: testUser1.id,
});
});
await t.step(
"should not be able to select source of other user",
async () => {
const res1 = await testUser2
.client!.from("sources")
.select()
.eq("id", sourceId)
.single();
assertNotEquals(res1.error, null);
},
);
await t.step("should select item", async () => {
const res1 = await testUser1
.client!.from("items")
.select()
.eq("id", itemId)
.single();
assertEquals(res1.error, null);
assertEqualsItems(
[res1.data],
[
{
id: itemId,
columnId: testUser1.columnId,
userId: testUser1.id,
sourceId: sourceId,
title: "Test Data",
link: "https://feeddeck.app/testdata/feed.xml",
media: "https://feeddeck.app/testdata/image.jpg",
description:
'<p><img src="https://feeddeck.app/testdata/image.jpg" /><br/><br/>Test Data for the FeedDeck E2E Tests with an Image and <b>HTML Formatted Content</b>.</p>',
author: null,
options: null,
publishedAt: 1746187200,
},
],
);
assertEquals(res1.data.publishedAt, 1746187200);
});
await t.step(
"should not be able to select item of other user",
async () => {
const res1 = await testUser2
.client!.from("items")
.select()
.eq("id", itemId)
.single();
assertNotEquals(res1.error, null);
},
);
await t.step("should get source icon from bucket", async () => {
const res1 = await testUser2
.client!.storage.from("sources")
.download(sourceIcon);
assertEquals(res1.error, null);
assertNotEquals(res1.data, null);
const res2 = await supabaseAdmin.storage
.from("sources")
.list(testUser1.id);
assertEquals(res2.error, null);
assertEquals(res2.data!.length, 1);
});
await t.step("should update source", async () => {
const res1 = await testUser1
.client!.from("sources")
.update({ position: 1 })
.eq("id", sourceId);
assertEquals(res1.error, null);
const res2 = await testUser1
.client!.from("sources")
.select()
.eq("id", sourceId)
.single();
assertEquals(res2.error, null);
assertEquals(res2.data.position, 1);
});
await t.step(
"should not be able to update source of other user",
async () => {
const res1 = await testUser2
.client!.from("sources")
.update({ position: 2 })
.eq("id", sourceId);
assertEquals(res1.error, null);
const res2 = await testUser1
.client!.from("sources")
.select()
.eq("id", sourceId)
.single();
assertEquals(res2.error, null);
assertEquals(res2.data.position, 1);
},
);
await t.step("should update item", async () => {
const res1 = await testUser1
.client!.from("items")
.update({ isRead: true, isBookmarked: false })
.eq("id", itemId);
assertEquals(res1.error, null);
const res2 = await testUser1
.client!.from("items")
.select()
.eq("id", itemId)
.single();
assertEquals(res2.error, null);
assertEquals(res2.data.isRead, true);
assertEquals(res2.data.isBookmarked, false);
});
await t.step(
"should not be able to update item of other user",
async () => {
const res1 = await testUser2
.client!.from("items")
.update({ isRead: true, isBookmarked: true })
.eq("id", itemId);
assertEquals(res1.error, null);
const res2 = await testUser1
.client!.from("items")
.select()
.eq("id", itemId)
.single();
assertEquals(res2.error, null);
assertEquals(res2.data.isRead, true);
assertEquals(res2.data.isBookmarked, false);
},
);
await t.step(
"should not be able to delete item of other user",
async () => {
const res1 = await testUser2
.client!.from("items")
.delete()
.eq("id", itemId);
assertEquals(res1.error, null);
const res2 = await testUser1
.client!.from("items")
.select()
.eq("id", itemId)
.single();
assertEquals(res2.error, null);
assertEquals(res2.data.isRead, true);
assertEquals(res2.data.isBookmarked, false);
},
);
await t.step("should delete item", async () => {
const res1 = await testUser1
.client!.from("items")
.delete()
.eq("id", itemId);
assertEquals(res1.error, null);
const res2 = await testUser1
.client!.from("items")
.select()
.eq("id", itemId)
.single();
assertNotEquals(res2.error, null);
});
await t.step(
"should not be able to delete source of other user",
async () => {
const res1 = await testUser2
.client!.from("sources")
.delete()
.eq("id", sourceId);
assertEquals(res1.error, null);
const res2 = await testUser1
.client!.from("sources")
.select()
.eq("id", sourceId)
.single();
assertEquals(res2.error, null);
assertEquals(res2.data.position, 1);
},
);
await t.step("should delete source", async () => {
const res1 = await testUser1
.client!.from("sources")
.delete()
.eq("id", sourceId);
assertEquals(res1.error, null);
const res2 = await testUser1
.client!.from("sources")
.select()
.eq("id", sourceId)
.single();
assertNotEquals(res2.error, null);
});
await t.step(
"should have deleted source icon from bucket when source was deleted",
async () => {
await sleep(3000);
const res1 = await supabaseAdmin.storage
.from("sources")
.list(testUser1.id);
assertEquals(res1.error, null);
assertEquals(res1.data!.length, 0);
},
);
},
);
await t.step("should delete users", async () => {
const res1 = await supabaseAdmin.auth.admin.deleteUser(testUser1.id);
assertEquals(res1.error, null);
const res2 = await supabaseAdmin.auth.admin.deleteUser(testUser2.id);
assertEquals(res2.error, null);
});
});

View File

@@ -7,9 +7,9 @@ export interface IItem {
link: string;
media?: string;
description?: string;
author?: string;
author?: null | string;
// deno-lint-ignore no-explicit-any
options?: Record<string, any>;
options?: null | Record<string, any>;
publishedAt: number;
isRead?: boolean;
isBookmarked?: boolean;

View File

@@ -1,7 +1,6 @@
{
"tasks": {
"start": "deno run --allow-net --watch=static/,routes/,data/ dev.ts",
"test": "deno test --allow-env",
"test": "deno test --allow-net --allow-env",
"lint": "deno lint"
}
}