Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
go.mod text eol=lf
go.sum text eol=lf
go.sum text eol=lf

frontend/src/graphql/types.ts linguist-generated=true
internal/converter/gen/generated.go linguist-generated=true
internal/dataloader/*_gen.go linguist-generated=true
internal/models/generated_*.go linguist-generated=true

# some of the files in internal/queries/ are handwritten
internal/queries/*.sql.go linguist-generated=true
internal/queries/copyfrom.go linguist-generated=true
internal/queries/db.go linguist-generated=true
internal/queries/models.go linguist-generated=true
internal/queries/querier.go linguist-generated=true
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ generate-dataloaders:
go run github.com/vektah/dataloaden SceneAppearancesLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.PerformerScene"; \
go run github.com/vektah/dataloaden PerformerLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.Performer"; \
go run github.com/vektah/dataloaden ImageLoader github.com/gofrs/uuid.UUID "*github.com/stashapp/stash-box/internal/models.Image"; \
go run github.com/vektah/dataloaden ImageTypeAssignmentsLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.ImageTypeAssignment"; \
go run github.com/vektah/dataloaden ImageDatesLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.ImageDate"; \
go run github.com/vektah/dataloaden FingerprintsLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.Fingerprint"; \
go run github.com/vektah/dataloaden SubmittedFingerprintsLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.Fingerprint"; \
go run github.com/vektah/dataloaden BodyModificationsLoader github.com/gofrs/uuid.UUID "[]github.com/stashapp/stash-box/internal/models.BodyModification"; \
Expand Down
74 changes: 74 additions & 0 deletions e2e/support/fixtures/square-png.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// A square PNG for the crop e2e test, generated at load time and written to a
// stable path because Playwright's setInputFiles takes a path, not a buffer
//
// Square on purpose: every crop template is portrait, so cropping to one is a
// change the assertions can see. The 1x1 JPEG the other image tests use has no
// room to drag a frame in

import { deflateSync } from "node:zlib";
import { writeFileSync, existsSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const SIZE = 900;

const crcTable = Array.from({ length: 256 }, (_, n) => {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
return c >>> 0;
});

const crc32 = (buf: Buffer) => {
let c = 0xffffffff;
for (const byte of buf) c = crcTable[(c ^ byte) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
};

const chunk = (type: string, data: Buffer) => {
const length = Buffer.alloc(4);
length.writeUInt32BE(data.length);
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
const crc = Buffer.alloc(4);
crc.writeUInt32BE(crc32(body));
return Buffer.concat([length, body, crc]);
};

const png = () => {
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(SIZE, 0);
ihdr.writeUInt32BE(SIZE, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 0; // grayscale
// compression, filter, interlace all 0

// One filter byte per row, then the row itself. A gradient rather than a
// flat fill, so a crop of the wrong region is distinguishable from the right
// one if anyone ever looks at the bytes
const raw = Buffer.alloc(SIZE * (SIZE + 1));
for (let y = 0; y < SIZE; y++) {
const row = y * (SIZE + 1);
raw[row] = 0;
for (let x = 0; x < SIZE; x++) raw[row + 1 + x] = (x + y) & 0xff;
}

return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk("IHDR", ihdr),
chunk("IDAT", deflateSync(raw)),
chunk("IEND", Buffer.alloc(0)),
]);
};

let path: string | undefined;

// Absolute path to the fixture, written on first use
export const squarePngPath = () => {
if (!path) {
path = join(tmpdir(), `stash-box-e2e-${SIZE}x${SIZE}.png`);
if (!existsSync(path)) writeFileSync(path, png());
}
return path;
};

// The source's own dimensions, for asserting that a crop changed them
export const SQUARE_PNG_SIZE = SIZE;
16 changes: 11 additions & 5 deletions e2e/tests/auth/role-authorization.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@

import { test, expect } from "../../support/fixtures";
import { graphqlAs } from "../../support/helpers/graphql";
import { adminApi, submitStudioCreateEdit, uniq } from "../../support/helpers/seed";
import {
adminApi,
submitStudioCreateEdit,
uniq,
} from "../../support/helpers/seed";

type Role = "read" | "vote" | "edit" | "modify" | "moderate";

Expand Down Expand Up @@ -174,10 +178,7 @@ test("admin can perform all the mutations above", async () => {
`mutation($input: TagCategoryCreateInput!) { tagCategoryCreate(input: $input) { id } }`,
{ input: { name: uniq("cat"), group: "SCENE" } },
],
[
`query { queryUsers(input: { page: 1, per_page: 1 }) { count } }`,
{},
],
[`query { queryUsers(input: { page: 1, per_page: 1 }) { count } }`, {}],
];
for (const [q, v] of mutations) {
const res = await admin.post("/graphql", {
Expand All @@ -189,3 +190,8 @@ test("admin can perform all the mutations above", async () => {
}
await admin.dispose();
});

test("image type order screen is admin-only", async ({ readPage }) => {
await readPage.goto("/image-types");
await expect(readPage.getByText(/do not have permission/i)).toBeVisible();
});
97 changes: 97 additions & 0 deletions e2e/tests/entities/image-preferences.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { test, expect } from "../../support/fixtures";
import { graphqlAs } from "../../support/helpers/graphql";
import { gql } from "../../support/helpers/seed";

// The stored key behind a display name, which is all the screen shows
const keyNamed = async (name: string) => {
const api = await graphqlAs("e2e_edit");
const data = await gql<{
imageTypeGroups: { types: { key: string; name: string }[] }[];
}>(api, `query { imageTypeGroups { types { key name } } }`);
await api.dispose();

const match = data.imageTypeGroups
.flatMap((group) => group.types)
.find((type) => type.name === name);
if (!match) throw new Error(`no image type named ${name}`);
return match.key;
};

const groupPreferencesOf = async (username: string) => {
const api = await graphqlAs(username);
const data = await gql<{ me: { image_type_group_preferences: string[] } }>(
api,
`query { me { image_type_group_preferences } }`,
);
await api.dispose();
return data.me.image_type_group_preferences;
};

const preferencesOf = async (username: string) => {
const api = await graphqlAs(username);
const data = await gql<{ me: { image_type_preferences: string[] } }>(
api,
`query { me { image_type_preferences } }`,
);
await api.dispose();
return data.me.image_type_preferences;
};

test("image preference screen saves an order and clears it", async ({
editPage,
}) => {
try {
await editPage.goto("/users/e2e_edit/image-types");
await editPage.waitForLoadState("networkidle");

const firstGroup = editPage.locator(".card-body").first();
const rows = firstGroup.locator(".DragList-row");
const nameOf = (index: number) =>
rows.nth(index).locator(".DragList-content").innerText();

const wasSecond = await nameOf(1);

// Promote the second type of the first group
await rows.nth(1).locator(".DragList-handle").dragTo(rows.nth(0));

expect(await nameOf(0)).toBe(wasSecond);

await editPage.getByRole("button", { name: "Save", exact: true }).click();
await expect(editPage.getByText("Preferences saved.")).toBeVisible();

// We save the full ordering every time
const saved = await preferencesOf("e2e_edit");
expect(saved.length).toBeGreaterThan(1);
expect(saved[0]).toBe(await keyNamed(wasSecond));

// Groups reorder the same way, and are the stronger of the two
const groupRows = editPage.locator(".DragList.is-block > .DragList-row");
const groupNames = () => editPage.locator(".card-header b").allInnerTexts();
const groupsBefore = await groupNames();

await groupRows
.nth(0)
.locator("> .DragList-handle")
.dragTo(groupRows.nth(1), { targetPosition: { x: 20, y: 12 } });

expect((await groupNames())[0]).toBe(groupsBefore[1]);

await editPage.getByRole("button", { name: "Save", exact: true }).click();
await expect(editPage.getByText("Preferences saved.")).toBeVisible();
expect((await groupPreferencesOf("e2e_edit")).length).toBeGreaterThan(1);

await editPage.getByRole("button", { name: "Use site default" }).click();
await expect(editPage.getByText("Preferences saved.")).toBeVisible();

expect(await preferencesOf("e2e_edit")).toEqual([]);
expect(await groupPreferencesOf("e2e_edit")).toEqual([]);
} finally {
// Shared fixture user so we want them to go back to the default ordering
const api = await graphqlAs("e2e_edit");
await gql(
api,
`mutation { updateImageTypePreferences(input: { types: [], groups: [] }) }`,
);
await api.dispose();
}
});
Loading
Loading