{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}}
+ {{svg "octicon-triangle-down" 14 "dropdown icon"}}
{{ctx.Locale.Tr (printf "search.%s" .Selected)}}
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index bd2a3800a2..85ae7266d9 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -103,11 +103,11 @@
{{ctx.Locale.Tr "action.compare_commits" $push.Len}} »
{{end}}
{{else if .GetOpType.InActions "create_issue"}}
-
{{index .GetIssueInfos 1 | RenderEmoji $.Context | RenderCodeBlock}}
+
{{RenderIssueTitle ctx (index .GetIssueInfos 1) (.Repo.ComposeMetas ctx)}}
{{else if .GetOpType.InActions "create_pull_request"}}
-
{{index .GetIssueInfos 1 | RenderEmoji $.Context | RenderCodeBlock}}
+
{{RenderIssueTitle ctx (index .GetIssueInfos 1) (.Repo.ComposeMetas ctx)}}
{{else if .GetOpType.InActions "comment_issue" "approve_pull_request" "reject_pull_request" "comment_pull"}}
-
{{(.GetIssueTitle ctx) | RenderEmoji $.Context | RenderCodeBlock}}
+
{{RenderIssueTitle ctx (.GetIssueTitle ctx) (.Repo.ComposeMetas ctx)}}
{{$comment := index .GetIssueInfos 1}}
{{if $comment}}
{{RenderMarkdownToHtml ctx $comment}}
@@ -115,7 +115,7 @@
{{else if .GetOpType.InActions "merge_pull_request"}}
{{index .GetIssueInfos 1}}
{{else if .GetOpType.InActions "close_issue" "reopen_issue" "close_pull_request" "reopen_pull_request"}}
-
{{(.GetIssueTitle ctx) | RenderEmoji $.Context | RenderCodeBlock}}
+
{{RenderIssueTitle ctx (.GetIssueTitle ctx) (.Repo.ComposeMetas ctx)}}
{{else if .GetOpType.InActions "pull_review_dismissed"}}
{{ctx.Locale.Tr "action.review_dismissed_reason"}}
{{index .GetIssueInfos 2 | RenderEmoji $.Context}}
diff --git a/tests/e2e/README.md b/tests/e2e/README.md
index 8d8858bfd5..35fc5e7d1d 100644
--- a/tests/e2e/README.md
+++ b/tests/e2e/README.md
@@ -250,16 +250,18 @@ test('For anyone', async ({page}) => {
If you need a user account, you can use something like:
~~~js
-import {test, login_user, login} from './utils_e2e.ts';
+import {test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2'); // or another user
-});
+// reuse user2 token from scope `shared`
+test.use({user: 'user2', authScope: 'shared'})
-test('For signed users only', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('For signed users only', async ({page}) => {
+
+})
~~~
+users are created in [utils_e2e_test.go](utils_e2e_test.go)
+
### Run tests very selectively
Browser testing can take some time.
diff --git a/tests/e2e/actions.test.e2e.ts b/tests/e2e/actions.test.e2e.ts
index a66b608080..4e93b89ee0 100644
--- a/tests/e2e/actions.test.e2e.ts
+++ b/tests/e2e/actions.test.e2e.ts
@@ -10,76 +10,66 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
-
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+import {save_visual, test} from './utils_e2e.ts';
const workflow_trigger_notification_text = 'This workflow has a workflow_dispatch event trigger.';
+test.describe('Workflow Authenticated user2', () => {
+ test.use({user: 'user2'});
-test('workflow dispatch present', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+ test('workflow dispatch present', async ({page}) => {
+ await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
- await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
+ await expect(page.getByText(workflow_trigger_notification_text)).toBeVisible();
- await expect(page.getByText(workflow_trigger_notification_text)).toBeVisible();
+ const run_workflow_btn = page.locator('#workflow_dispatch_dropdown>button');
+ await expect(run_workflow_btn).toBeVisible();
- const run_workflow_btn = page.locator('#workflow_dispatch_dropdown>button');
- await expect(run_workflow_btn).toBeVisible();
-
- const menu = page.locator('#workflow_dispatch_dropdown>.menu');
- await expect(menu).toBeHidden();
- await run_workflow_btn.click();
- await expect(menu).toBeVisible();
- await save_visual(page);
-});
-
-test('workflow dispatch error: missing inputs', async ({browser}, workerInfo) => {
- test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
-
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
-
- await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
-
- await page.locator('#workflow_dispatch_dropdown>button').click();
-
- // Remove the required attribute so we can trigger the error message!
- await page.evaluate(() => {
- const elem = document.querySelector('input[name="inputs[string2]"]');
- elem?.removeAttribute('required');
+ const menu = page.locator('#workflow_dispatch_dropdown>.menu');
+ await expect(menu).toBeHidden();
+ await run_workflow_btn.click();
+ await expect(menu).toBeVisible();
+ await save_visual(page);
});
- await page.locator('#workflow-dispatch-submit').click();
+ test('dispatch error: missing inputs', async ({page}, testInfo) => {
+ test.skip(testInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
- await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible();
- await save_visual(page);
-});
+ await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
-test('workflow dispatch success', async ({browser}, workerInfo) => {
- test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
+ await page.locator('#workflow_dispatch_dropdown>button').click();
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+ // Remove the required attribute so we can trigger the error message!
+ await page.evaluate(() => {
+ const elem = document.querySelector('input[name="inputs[string2]"]');
+ elem?.removeAttribute('required');
+ });
- await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
+ await page.locator('#workflow-dispatch-submit').click();
- await page.locator('#workflow_dispatch_dropdown>button').click();
+ await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible();
+ await save_visual(page);
+ });
- await page.fill('input[name="inputs[string2]"]', 'abc');
- await save_visual(page);
- await page.locator('#workflow-dispatch-submit').click();
+ test('dispatch success', async ({page}, testInfo) => {
+ test.skip(testInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
+ await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
- await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible();
+ await page.locator('#workflow_dispatch_dropdown>button').click();
- await expect(page.locator('.run-list>:first-child .run-list-meta', {hasText: 'now'})).toBeVisible();
- await save_visual(page);
+ await page.fill('input[name="inputs[string2]"]', 'abc');
+ await save_visual(page);
+ await page.locator('#workflow-dispatch-submit').click();
+
+ await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible();
+
+ await expect(page.locator('.run-list>:first-child .run-list-meta', {hasText: 'now'})).toBeVisible();
+ await save_visual(page);
+ });
});
test('workflow dispatch box not available for unauthenticated users', async ({page}) => {
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
await expect(page.locator('body')).not.toContainText(workflow_trigger_notification_text);
+ await save_visual(page);
});
diff --git a/tests/e2e/clipboard-copy.test.e2e.ts b/tests/e2e/clipboard-copy.test.e2e.ts
index 70a3425868..2ae0e0dfff 100644
--- a/tests/e2e/clipboard-copy.test.e2e.ts
+++ b/tests/e2e/clipboard-copy.test.e2e.ts
@@ -8,7 +8,7 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
test('copy src file path to clipboard', async ({page}, workerInfo) => {
test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'Apple clipboard API addon - starting at just $499!');
@@ -19,6 +19,7 @@ test('copy src file path to clipboard', async ({page}, workerInfo) => {
await page.click('[data-clipboard-text]');
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
+ await save_visual(page);
});
test('copy diff file path to clipboard', async ({page}, workerInfo) => {
@@ -30,4 +31,6 @@ test('copy diff file path to clipboard', async ({page}, workerInfo) => {
await page.click('[data-clipboard-text]');
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
+ await expect(page.getByText('Copied')).toBeVisible();
+ await save_visual(page);
});
diff --git a/tests/e2e/dashboard-ci-status.test.e2e.ts b/tests/e2e/dashboard-ci-status.test.e2e.ts
index 1d23122b44..d35fe299ff 100644
--- a/tests/e2e/dashboard-ci-status.test.e2e.ts
+++ b/tests/e2e/dashboard-ci-status.test.e2e.ts
@@ -3,22 +3,26 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
+import {test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('Correct link and tooltip', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+test.describe.configure({retries: 2});
+
+test('Correct link and tooltip', async ({page}, testInfo) => {
+ if (testInfo.retry) {
+ await page.goto('/user2/test_workflows/actions');
+ }
+
+ const searchResponse = page.waitForResponse((resp) => resp.url().includes('/repo/search?') && resp.status() === 200);
const response = await page.goto('/?repo-search-query=test_workflows');
expect(response?.status()).toBe(200);
+ await searchResponse;
+
const repoStatus = page.locator('.dashboard-repos .repo-owner-name-list > li:nth-child(1) > a:nth-child(2)');
- // wait for network activity to cease (so status was loaded in frontend)
- await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
await expect(repoStatus).toHaveAttribute('href', '/user2/test_workflows/actions', {timeout: 10000});
await expect(repoStatus).toHaveAttribute('data-tooltip-content', /^(Error|Failure)$/);
- await save_visual(page);
+ // ToDo: Ensure stable screenshot of dashboard. Known to be flaky: https://code.forgejo.org/forgejo/visual-browser-testing/commit/206d4cfb7a4af6d8d7043026cdd4d63708798b2a
+ // await save_visual(page);
});
diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go
index b8c89625c0..245bd347b8 100644
--- a/tests/e2e/e2e_test.go
+++ b/tests/e2e/e2e_test.go
@@ -89,6 +89,7 @@ func TestE2e(t *testing.T) {
runArgs := []string{"npx", "playwright", "test"}
+ _, testVisual := os.LookupEnv("VISUAL_TEST")
// To update snapshot outputs
if _, set := os.LookupEnv("ACCEPT_VISUAL"); set {
runArgs = append(runArgs, "--update-snapshots")
@@ -112,6 +113,10 @@ func TestE2e(t *testing.T) {
onForgejoRun(t, func(*testing.T, *url.URL) {
defer DeclareGitRepos(t)()
thisTest := runArgs
+ // when all tests are run, use unique artifacts directories per test to preserve artifacts from other tests
+ if testVisual {
+ thisTest = append(thisTest, "--output=tests/e2e/test-artifacts/"+testname)
+ }
thisTest = append(thisTest, path)
cmd := exec.Command(runArgs[0], thisTest...)
cmd.Env = os.Environ()
@@ -121,7 +126,7 @@ func TestE2e(t *testing.T) {
cmd.Stderr = os.Stderr
err := cmd.Run()
- if err != nil {
+ if err != nil && !testVisual {
log.Fatal("Playwright Failed: %s", err)
}
})
diff --git a/tests/e2e/example.test.e2e.ts b/tests/e2e/example.test.e2e.ts
index b2a679a82d..97c5b8684b 100644
--- a/tests/e2e/example.test.e2e.ts
+++ b/tests/e2e/example.test.e2e.ts
@@ -5,7 +5,7 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
test('Load Homepage', async ({page}) => {
const response = await page.goto('/');
@@ -26,6 +26,7 @@ test('Register Form', async ({page}, workerInfo) => {
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible();
await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!');
+ await save_visual(page);
});
// eslint-disable-next-line playwright/no-skipped-test
diff --git a/tests/e2e/explore.test.e2e.ts b/tests/e2e/explore.test.e2e.ts
index 44c9b21f58..1bb5af3cc6 100644
--- a/tests/e2e/explore.test.e2e.ts
+++ b/tests/e2e/explore.test.e2e.ts
@@ -7,7 +7,7 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
test('Explore view taborder', async ({page}) => {
await page.goto('/explore/repos');
@@ -42,4 +42,5 @@ test('Explore view taborder', async ({page}) => {
}
}
expect(res).toBe(exp);
+ await save_visual(page);
});
diff --git a/tests/e2e/git-notes.test.e2e.ts b/tests/e2e/git-notes.test.e2e.ts
index 8b80a3aa77..1e2cbe76fc 100644
--- a/tests/e2e/git-notes.test.e2e.ts
+++ b/tests/e2e/git-notes.test.e2e.ts
@@ -1,17 +1,16 @@
// @ts-check
-import {test, expect} from '@playwright/test';
-import {login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
+import {expect} from '@playwright/test';
+import {save_visual, test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('Change git note', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+test('Change git note', async ({page}) => {
let response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
expect(response?.status()).toBe(200);
+ // An add button should not be present, because the commit already has a commit note
+ await expect(page.locator('#commit-notes-add-button')).toHaveCount(0);
+
await page.locator('#commit-notes-edit-button').click();
let textarea = page.locator('textarea[name="notes"]');
diff --git a/tests/e2e/issue-comment.test.e2e.ts b/tests/e2e/issue-comment.test.e2e.ts
index 4fce16764b..1c19f98c48 100644
--- a/tests/e2e/issue-comment.test.e2e.ts
+++ b/tests/e2e/issue-comment.test.e2e.ts
@@ -5,14 +5,11 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, login_user, login} from './utils_e2e.ts';
+import {test, save_visual} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('Menu accessibility', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('Menu accessibility', async ({page}) => {
await page.goto('/user2/repo1/issues/1');
await expect(page.getByLabel('user2 reacted eyes. Remove eyes')).toBeVisible();
await expect(page.getByLabel('reacted laugh. Remove laugh')).toBeVisible();
@@ -24,9 +21,8 @@ test('Menu accessibility', async ({browser}, workerInfo) => {
await expect(page.getByLabel('user1, user2 reacted laugh. Remove laugh')).toBeVisible();
});
-test('Hyperlink paste behaviour', async ({browser}, workerInfo) => {
+test('Hyperlink paste behaviour', async ({page}, workerInfo) => {
test.skip(['Mobile Safari', 'Mobile Chrome', 'webkit'].includes(workerInfo.project.name), 'Mobile clients seem to have very weird behaviour with this test, which I cannot confirm with real usage');
- const page = await login({browser}, workerInfo);
await page.goto('/user2/repo1/issues/new');
await page.locator('textarea').click();
// same URL
@@ -58,8 +54,7 @@ test('Hyperlink paste behaviour', async ({browser}, workerInfo) => {
await page.locator('textarea').fill('');
});
-test('Always focus edit tab first on edit', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('Always focus edit tab first on edit', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
@@ -82,9 +77,29 @@ test('Always focus edit tab first on edit', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('Quote reply', async ({browser}, workerInfo) => {
+test('Reset content of comment edit field on cancel', async ({page}) => {
+ const response = await page.goto('/user2/repo1/issues/1');
+ expect(response?.status()).toBe(200);
+
+ const editorTextarea = page.locator('[id="_combo_markdown_editor_1"]');
+
+ // Change the content of the edit field
+ await page.click('#issue-1 .comment-container .context-menu');
+ await page.click('#issue-1 .comment-container .menu>.edit-content');
+ await expect(editorTextarea).toHaveValue('content for the first issue');
+ await editorTextarea.fill('some random string');
+ await expect(editorTextarea).toHaveValue('some random string');
+ await page.click('#issue-1 .comment-container .edit .cancel');
+
+ // Edit again and assert that the edit field should be reset to the initial content
+ await page.click('#issue-1 .comment-container .context-menu');
+ await page.click('#issue-1 .comment-container .menu>.edit-content');
+ await expect(editorTextarea).toHaveValue('content for the first issue');
+ await save_visual(page);
+});
+
+test('Quote reply', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name !== 'firefox', 'Uses Firefox specific selection quirks');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
@@ -157,9 +172,8 @@ test('Quote reply', async ({browser}, workerInfo) => {
await editorTextarea.fill('');
});
-test('Pull quote reply', async ({browser}, workerInfo) => {
+test('Pull quote reply', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name !== 'firefox', 'Uses Firefox specific selection quirks');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/user2/commitsonpr/pulls/1/files');
expect(response?.status()).toBe(200);
diff --git a/tests/e2e/issue-sidebar.test.e2e.ts b/tests/e2e/issue-sidebar.test.e2e.ts
index f4d50a13ba..fe2a6cec87 100644
--- a/tests/e2e/issue-sidebar.test.e2e.ts
+++ b/tests/e2e/issue-sidebar.test.e2e.ts
@@ -7,14 +7,13 @@
/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["check_wip"] }] */
import {expect, type Page} from '@playwright/test';
-import {test, save_visual, login_user, login} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
test.describe('Pull: Toggle WIP', () => {
const prTitle = 'pull5';
+
async function toggle_wip_to({page}, should: boolean) {
await page.waitForLoadState('domcontentloaded');
if (should) {
@@ -39,8 +38,7 @@ test.describe('Pull: Toggle WIP', () => {
}
}
- test.beforeEach(async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+ test.beforeEach(async ({page}) => {
const response = await page.goto('/user2/repo1/pulls/5');
expect(response?.status()).toBe(200); // Status OK
// ensure original title
@@ -50,9 +48,8 @@ test.describe('Pull: Toggle WIP', () => {
await check_wip({page}, false);
});
- test('simple toggle', async ({browser}, workerInfo) => {
+ test('simple toggle', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
await page.goto('/user2/repo1/pulls/5');
// toggle to WIP
await toggle_wip_to({page}, true);
@@ -62,9 +59,8 @@ test.describe('Pull: Toggle WIP', () => {
await check_wip({page}, false);
});
- test('manual edit', async ({browser}, workerInfo) => {
+ test('manual edit', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
await page.goto('/user2/repo1/pulls/5');
// manually edit title to another prefix
await page.locator('#issue-title-edit-show').click();
@@ -76,9 +72,8 @@ test.describe('Pull: Toggle WIP', () => {
await check_wip({page}, false);
});
- test('maximum title length', async ({browser}, workerInfo) => {
+ test('maximum title length', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
await page.goto('/user2/repo1/pulls/5');
// check maximum title length is handled gracefully
const maxLenStr = prTitle + 'a'.repeat(240);
@@ -96,17 +91,16 @@ test.describe('Pull: Toggle WIP', () => {
});
});
-test('Issue: Labels', async ({browser}, workerInfo) => {
+test('Issue: Labels', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- async function submitLabels({page}: {page: Page}) {
+ async function submitLabels({page}: { page: Page }) {
const submitted = page.waitForResponse('/user2/repo1/issues/labels');
await page.locator('textarea').first().click(); // close via unrelated element
await submitted;
await page.waitForLoadState();
}
- const page = await login({browser}, workerInfo);
// select label list in sidebar only
const labelList = page.locator('.issue-content-right .labels-list a');
const response = await page.goto('/user2/repo1/issues/1');
@@ -144,9 +138,8 @@ test('Issue: Labels', async ({browser}, workerInfo) => {
await expect(labelList.filter({hasText: 'label1'})).toBeVisible();
});
-test('Issue: Assignees', async ({browser}, workerInfo) => {
+test('Issue: Assignees', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
// select label list in sidebar only
const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item a');
@@ -182,9 +175,8 @@ test('Issue: Assignees', async ({browser}, workerInfo) => {
await expect(page.locator('.ui.assignees.list .item.no-select')).toBeHidden();
});
-test('New Issue: Assignees', async ({browser}, workerInfo) => {
+test('New Issue: Assignees', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
// select label list in sidebar only
const assigneesList = page.locator('.issue-content-right .assignees.list .selected .item');
@@ -224,9 +216,8 @@ test('New Issue: Assignees', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('Issue: Milestone', async ({browser}, workerInfo) => {
+test('Issue: Milestone', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
@@ -248,9 +239,8 @@ test('Issue: Milestone', async ({browser}, workerInfo) => {
await expect(page.locator('.timeline-item.event').last()).toContainText('user2 removed this from the milestone1 milestone');
});
-test('New Issue: Milestone', async ({browser}, workerInfo) => {
+test('New Issue: Milestone', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts
index ca2d6e01b6..35e9de2ea6 100644
--- a/tests/e2e/markdown-editor.test.e2e.ts
+++ b/tests/e2e/markdown-editor.test.e2e.ts
@@ -5,21 +5,16 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, load_logged_in_context, login_user} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('Markdown image preview behaviour', async ({browser}, workerInfo) => {
+test('Markdown image preview behaviour', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari;');
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
-
// Editing the root README.md file for image preview
const editPath = '/user2/repo1/src/branch/master/README.md';
- const page = await context.newPage();
const response = await page.goto(editPath, {waitUntil: 'domcontentloaded'});
expect(response?.status()).toBe(200);
@@ -43,12 +38,9 @@ test('Markdown image preview behaviour', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('markdown indentation', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
-
+test('markdown indentation', async ({page}) => {
const initText = `* first\n* second\n* third\n* last`;
- const page = await context.newPage();
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
@@ -116,12 +108,9 @@ test('markdown indentation', async ({browser}, workerInfo) => {
await expect(textarea).toHaveValue(initText);
});
-test('markdown list continuation', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
+test('markdown list continuation', async ({page}) => {
+ const initText = `* first\n* second`;
- const initText = `* first\n* second\n* third\n* last`;
-
- const page = await context.newPage();
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
@@ -130,25 +119,20 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
const indent = page.locator('button[data-md-action="indent"]');
await textarea.fill(initText);
- // Test continuation of '* ' prefix
- await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond')));
+ // Test continuation of ' * ' prefix
+ await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('rst'), it.value.indexOf('rst')));
+ await indent.click();
await textarea.press('End');
await textarea.press('Enter');
- await textarea.pressSequentially('middle');
- await expect(textarea).toHaveValue(`* first\n* second\n* middle\n* third\n* last`);
-
- // Test continuation of ' * ' prefix
- await indent.click();
- await textarea.press('Enter');
await textarea.pressSequentially('muddle');
- await expect(textarea).toHaveValue(`* first\n* second\n${tab}* middle\n${tab}* muddle\n* third\n* last`);
+ await expect(textarea).toHaveValue(`${tab}* first\n${tab}* muddle\n* second`);
// Test breaking in the middle of a line
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.lastIndexOf('ddle'), it.value.lastIndexOf('ddle')));
await textarea.pressSequentially('tate');
await textarea.press('Enter');
await textarea.pressSequentially('me');
- await expect(textarea).toHaveValue(`* first\n* second\n${tab}* middle\n${tab}* mutate\n${tab}* meddle\n* third\n* last`);
+ await expect(textarea).toHaveValue(`${tab}* first\n${tab}* mutate\n${tab}* meddle\n* second`);
// Test not triggering when Shift held
await textarea.fill(initText);
@@ -156,35 +140,36 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
await textarea.press('Shift+Enter');
await textarea.press('Enter');
await textarea.pressSequentially('...but not least');
- await expect(textarea).toHaveValue(`* first\n* second\n* third\n* last\n\n...but not least`);
+ await expect(textarea).toHaveValue(`* first\n* second\n\n...but not least`);
// Test continuation of ordered list
- await textarea.fill(`1. one\n2. two`);
+ await textarea.fill(`1. one`);
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter');
+ await textarea.pressSequentially(' ');
+ await textarea.press('Enter');
await textarea.pressSequentially('three');
- await expect(textarea).toHaveValue(`1. one\n2. two\n3. three`);
+ await textarea.press('Enter');
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`1. one\n2. \n3. three\n\n`);
// Test continuation of alternative ordered list syntax
- await textarea.fill(`1) one\n2) two`);
+ await textarea.fill(`1) one`);
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter');
+ await textarea.pressSequentially(' ');
+ await textarea.press('Enter');
await textarea.pressSequentially('three');
- await expect(textarea).toHaveValue(`1) one\n2) two\n3) three`);
-
- // Test continuation of blockquote
- await textarea.fill(`> knowledge is power`);
- await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter');
- await textarea.pressSequentially('france is bacon');
- await expect(textarea).toHaveValue(`> knowledge is power\n> france is bacon`);
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`1) one\n2) \n3) three\n\n`);
// Test continuation of checklists
- await textarea.fill(`- [ ] have a problem\n- [x] create a solution`);
+ await textarea.fill(`- [ ]have a problem\n- [x]create a solution`);
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter');
await textarea.pressSequentially('write a test');
- await expect(textarea).toHaveValue(`- [ ] have a problem\n- [x] create a solution\n- [ ] write a test`);
+ await expect(textarea).toHaveValue(`- [ ]have a problem\n- [x]create a solution\n- [ ]write a test`);
// Test all conceivable syntax (except ordered lists)
const prefixes = [
@@ -200,7 +185,6 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
'> ',
'> > ',
'- [ ] ',
- '- [ ]', // This does seem to render, so allow.
'* [ ] ',
'+ [ ] ',
];
@@ -208,15 +192,16 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
await textarea.fill(`${prefix}one`);
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
await textarea.press('Enter');
+ await textarea.pressSequentially(' ');
+ await textarea.press('Enter');
await textarea.pressSequentially('two');
- await expect(textarea).toHaveValue(`${prefix}one\n${prefix}two`);
+ await textarea.press('Enter');
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`${prefix}one\n${prefix} \n${prefix}two\n\n`);
}
});
-test('markdown insert table', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
-
- const page = await context.newPage();
+test('markdown insert table', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/new');
expect(response?.status()).toBe(200);
@@ -238,3 +223,29 @@ test('markdown insert table', async ({browser}, workerInfo) => {
await expect(textarea).toHaveValue('| Header | Header |\n|---------|---------|\n| Content | Content |\n| Content | Content |\n| Content | Content |\n');
await save_visual(page);
});
+
+test('text expander has higher prio then prefix continuation', async ({page}) => {
+ const response = await page.goto('/user2/repo1/issues/new');
+ expect(response?.status()).toBe(200);
+
+ const textarea = page.locator('textarea[name=content]');
+ const initText = `* first`;
+ await textarea.fill(initText);
+ await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('rst'), it.value.indexOf('rst')));
+ await textarea.press('End');
+
+ // Test emoji completion
+ await textarea.press('Enter');
+ await textarea.pressSequentially(':smile_c');
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`* first\n* 😸`);
+
+ // Test username completion
+ await textarea.press('Enter');
+ await textarea.pressSequentially('@user');
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 `);
+
+ await textarea.press('Enter');
+ await expect(textarea).toHaveValue(`* first\n* 😸\n* @user2 \n* `);
+});
diff --git a/tests/e2e/markup.test.e2e.ts b/tests/e2e/markup.test.e2e.ts
index 2726942d57..398a0a6300 100644
--- a/tests/e2e/markup.test.e2e.ts
+++ b/tests/e2e/markup.test.e2e.ts
@@ -3,7 +3,7 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
test('markup with #xyz-mode-only', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
@@ -13,4 +13,5 @@ test('markup with #xyz-mode-only', async ({page}) => {
await expect(comment).toBeVisible();
await expect(comment.locator('[src$="#gh-light-mode-only"]')).toBeVisible();
await expect(comment.locator('[src$="#gh-dark-mode-only"]')).toBeHidden();
+ await save_visual(page);
});
diff --git a/tests/e2e/org-settings.test.e2e.ts b/tests/e2e/org-settings.test.e2e.ts
index 22a8bc0e2d..df554e0674 100644
--- a/tests/e2e/org-settings.test.e2e.ts
+++ b/tests/e2e/org-settings.test.e2e.ts
@@ -5,16 +5,13 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, login_user, login} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
import {validate_form} from './shared/forms.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('org team settings', async ({browser}, workerInfo) => {
+test('org team settings', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Cannot get it to work - as usual');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/org/org3/teams/team1/edit');
expect(response?.status()).toBe(200);
diff --git a/tests/e2e/profile_actions.test.e2e.ts b/tests/e2e/profile_actions.test.e2e.ts
index 65090e62b2..a66dc43aab 100644
--- a/tests/e2e/profile_actions.test.e2e.ts
+++ b/tests/e2e/profile_actions.test.e2e.ts
@@ -5,13 +5,11 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
-test('Follow actions', async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+test.use({user: 'user2'});
+test('Follow actions', async ({page}) => {
await page.goto('/user1');
// Check if following and then unfollowing works.
diff --git a/tests/e2e/reaction-selectors.test.e2e.ts b/tests/e2e/reaction-selectors.test.e2e.ts
index 3ce71b24d7..54b7d91869 100644
--- a/tests/e2e/reaction-selectors.test.e2e.ts
+++ b/tests/e2e/reaction-selectors.test.e2e.ts
@@ -4,11 +4,9 @@
// @watch end
import {expect, type Locator} from '@playwright/test';
-import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
const assertReactionCounts = (comment: Locator, counts: unknown) =>
expect(async () => {
@@ -26,6 +24,7 @@ const assertReactionCounts = (comment: Locator, counts: unknown) =>
]),
),
);
+ // eslint-disable-next-line playwright/no-standalone-expect
return expect(reactions).toStrictEqual(counts);
}).toPass();
@@ -35,10 +34,7 @@ async function toggleReaction(menu: Locator, reaction: string) {
await menu.locator(`[role=menuitem][data-reaction-content="${reaction}"]`).click();
}
-test('Reaction Selectors', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
-
+test('Reaction Selectors', async ({page}) => {
const response = await page.goto('/user2/repo1/issues/1');
expect(response?.status()).toBe(200);
diff --git a/tests/e2e/release.test.e2e.ts b/tests/e2e/release.test.e2e.ts
index fefa446c59..49c67793e6 100644
--- a/tests/e2e/release.test.e2e.ts
+++ b/tests/e2e/release.test.e2e.ts
@@ -9,24 +9,18 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
import {validate_form} from './shared/forms.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
test.describe.configure({
timeout: 30000,
});
-test('External Release Attachments', async ({browser, isMobile}, workerInfo) => {
+test('External Release Attachments', async ({page, isMobile}) => {
test.skip(isMobile);
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- /** @type {import('@playwright/test').Page} */
- const page = await context.newPage();
-
// Click "New Release"
await page.goto('/user2/repo2/releases');
await page.click('.button.small.primary');
diff --git a/tests/e2e/repo-code.test.e2e.ts b/tests/e2e/repo-code.test.e2e.ts
index 264dd3a8e0..11b710c956 100644
--- a/tests/e2e/repo-code.test.e2e.ts
+++ b/tests/e2e/repo-code.test.e2e.ts
@@ -5,13 +5,9 @@
// @watch end
import {expect, type Page} from '@playwright/test';
-import {test, save_visual, login_user, login} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
import {accessibilityCheck} from './shared/accessibility.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
-
async function assertSelectedLines(page: Page, nums: string[]) {
const pageAssertions = async () => {
expect(
@@ -53,6 +49,7 @@ test('Line Range Selection', async ({page}) => {
// out-of-bounds end line
await page.goto(`${filePath}#L1-L100`);
await assertSelectedLines(page, ['1', '2', '3']);
+ await save_visual(page);
});
test('Readable diff', async ({page}, workerInfo) => {
@@ -79,22 +76,26 @@ test('Readable diff', async ({page}, workerInfo) => {
await expect(page.getByText(thisDiff.added, {exact: true})).toHaveCSS('background-color', 'rgb(134, 239, 172)');
}
}
+ await save_visual(page);
});
-test('Username highlighted in commits', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
- await page.goto('/user2/mentions-highlighted/commits/branch/main');
- // check first commit
- await page.getByRole('link', {name: 'A commit message which'}).click();
- await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/);
- await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
- await accessibilityCheck({page}, ['.commit-header'], [], []);
- await save_visual(page);
- // check second commit
- await page.goto('/user2/mentions-highlighted/commits/branch/main');
- await page.locator('tbody').getByRole('link', {name: 'Another commit which mentions'}).click();
- await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/);
- await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
- await accessibilityCheck({page}, ['.commit-header'], [], []);
- await save_visual(page);
+test.describe('As authenticated user', () => {
+ test.use({user: 'user2'});
+
+ test('Username highlighted in commits', async ({page}) => {
+ await page.goto('/user2/mentions-highlighted/commits/branch/main');
+ // check first commit
+ await page.getByRole('link', {name: 'A commit message which'}).click();
+ await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/);
+ await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
+ await accessibilityCheck({page}, ['.commit-header'], [], []);
+ await save_visual(page);
+ // check second commit
+ await page.goto('/user2/mentions-highlighted/commits/branch/main');
+ await page.locator('tbody').getByRole('link', {name: 'Another commit which mentions'}).click();
+ await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/);
+ await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
+ await accessibilityCheck({page}, ['.commit-header'], [], []);
+ await save_visual(page);
+ });
});
diff --git a/tests/e2e/repo-commitgraph.test.e2e.ts b/tests/e2e/repo-commitgraph.test.e2e.ts
index 5f0cad117a..e8b85c5997 100644
--- a/tests/e2e/repo-commitgraph.test.e2e.ts
+++ b/tests/e2e/repo-commitgraph.test.e2e.ts
@@ -5,13 +5,30 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
test('Commit graph overflow', async ({page}) => {
- await page.goto('/user2/diff-test/graph');
+ const response = await page.goto('/user2/repo1/graph');
+ expect(response?.status()).toBe(200);
+
+ await page.click('#flow-select-refs-dropdown');
+ const input = page.locator('#flow-select-refs-dropdown');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+ await input.press('Enter');
+
+ await expect(page.locator('#flow-select-refs-dropdown')).toBeInViewport({ratio: 1});
await expect(page.getByRole('button', {name: 'Mono'})).toBeInViewport({ratio: 1});
await expect(page.getByRole('button', {name: 'Color'})).toBeInViewport({ratio: 1});
await expect(page.locator('.selection.search.dropdown')).toBeInViewport({ratio: 1});
+ await save_visual(page);
});
test('Switch branch', async ({page}) => {
@@ -28,4 +45,5 @@ test('Switch branch', async ({page}) => {
await expect(page.locator('#loading-indicator')).toBeHidden();
await expect(page.locator('#rel-container')).toBeVisible();
await expect(page.locator('#rev-container')).toBeVisible();
+ await save_visual(page);
});
diff --git a/tests/e2e/repo-migrate.test.e2e.ts b/tests/e2e/repo-migrate.test.e2e.ts
index a0f9ab6c80..5e67f89ed1 100644
--- a/tests/e2e/repo-migrate.test.e2e.ts
+++ b/tests/e2e/repo-migrate.test.e2e.ts
@@ -3,15 +3,13 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, login_user, load_logged_in_context} from './utils_e2e.ts';
+import {test, save_visual, test_context} from './utils_e2e.ts';
-test.beforeAll(({browser}, workerInfo) => login_user(browser, workerInfo, 'user2'));
+test.use({user: 'user2'});
-test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo) => {
+test('Migration Progress Page', async ({page, browser}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky actionability checks on Mobile Safari');
- const page = await (await load_logged_in_context(browser, workerInfo, 'user2')).newPage();
-
expect((await page.goto('/user2/invalidrepo'))?.status(), 'repo should not exist yet').toBe(404);
await page.goto('/repo/migrate?service_type=1');
@@ -23,10 +21,11 @@ test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo
await form.locator('button.primary').click({timeout: 5000});
await expect(page).toHaveURL('user2/invalidrepo');
await save_visual(page);
- // page screenshot of unauthedPage is checked automatically after the test
- expect((await unauthedPage.goto('/user2/invalidrepo'))?.status(), 'public migration page should be accessible').toBe(200);
- await expect(unauthedPage.locator('#repo_migrating_progress')).toBeVisible();
+ const ctx = await test_context(browser);
+ const unauthenticatedPage = await ctx.newPage();
+ expect((await unauthenticatedPage.goto('/user2/invalidrepo'))?.status(), 'public migration page should be accessible').toBe(200);
+ await expect(unauthenticatedPage.locator('#repo_migrating_progress')).toBeVisible();
await page.reload();
await expect(page.locator('#repo_migrating_failed')).toBeVisible();
@@ -37,4 +36,6 @@ test('Migration Progress Page', async ({page: unauthedPage, browser}, workerInfo
await save_visual(page);
await deleteModal.getByRole('button', {name: 'Delete repository'}).click();
await expect(page).toHaveURL('/');
+ // checked last to preserve the order of screenshots from first run
+ await save_visual(unauthenticatedPage);
});
diff --git a/tests/e2e/repo-new.test.e2e.ts b/tests/e2e/repo-new.test.e2e.ts
index c9cc29ad56..ad202825a0 100644
--- a/tests/e2e/repo-new.test.e2e.ts
+++ b/tests/e2e/repo-new.test.e2e.ts
@@ -4,15 +4,12 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, dynamic_id, save_visual, login_user, login} from './utils_e2e.ts';
+import {test, dynamic_id, save_visual} from './utils_e2e.ts';
import {validate_form} from './shared/forms.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('New repo: invalid', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('New repo: invalid', async ({page}) => {
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
// check that relevant form content is hidden or available
@@ -28,8 +25,7 @@ test('New repo: invalid', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('New repo: initialize', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('New repo: initialize', async ({page}, workerInfo) => {
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
// check that relevant form content is hidden or available
@@ -62,8 +58,7 @@ test('New repo: initialize', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('New repo: initialize later', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('New repo: initialize later', async ({page}) => {
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
@@ -97,9 +92,8 @@ test('New repo: initialize later', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('New repo: from template', async ({browser}, workerInfo) => {
+test('New repo: from template', async ({page}, workerInfo) => {
test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'WebKit browsers seem to have CORS issues with localhost here.');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/repo/create');
expect(response?.status()).toBe(200);
@@ -114,8 +108,7 @@ test('New repo: from template', async ({browser}, workerInfo) => {
await save_visual(page);
});
-test('New repo: label set', async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+test('New repo: label set', async ({page}) => {
await page.goto('/repo/create');
const reponame = dynamic_id();
diff --git a/tests/e2e/repo-settings.test.e2e.ts b/tests/e2e/repo-settings.test.e2e.ts
index 113b15181b..3d260866fb 100644
--- a/tests/e2e/repo-settings.test.e2e.ts
+++ b/tests/e2e/repo-settings.test.e2e.ts
@@ -7,16 +7,13 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, save_visual, login_user, login} from './utils_e2e.ts';
+import {test, save_visual} from './utils_e2e.ts';
import {validate_form} from './shared/forms.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
+test.use({user: 'user2'});
-test('repo webhook settings', async ({browser}, workerInfo) => {
+test('repo webhook settings', async ({page}, workerInfo) => {
test.skip(workerInfo.project.name === 'Mobile Safari', 'Cannot get it to work - as usual');
- const page = await login({browser}, workerInfo);
const response = await page.goto('/user2/repo1/settings/hooks/forgejo/new');
expect(response?.status()).toBe(200);
@@ -35,9 +32,8 @@ test('repo webhook settings', async ({browser}, workerInfo) => {
});
test.describe('repo branch protection settings', () => {
- test('form', async ({browser}, workerInfo) => {
- test.skip(workerInfo.project.name === 'Mobile Safari', 'Cannot get it to work - as usual');
- const page = await login({browser}, workerInfo);
+ test('form', async ({page}, {project}) => {
+ test.skip(project.name === 'Mobile Safari', 'Cannot get it to work - as usual');
const response = await page.goto('/user2/repo1/settings/branches/edit');
expect(response?.status()).toBe(200);
@@ -56,8 +52,7 @@ test.describe('repo branch protection settings', () => {
await save_visual(page);
});
- test.afterEach(async ({browser}, workerInfo) => {
- const page = await login({browser}, workerInfo);
+ test.afterEach(async ({page}) => {
// delete the rule for the next test
await page.goto('/user2/repo1/settings/branches/');
await page.waitForLoadState('domcontentloaded');
diff --git a/tests/e2e/repo-wiki.test.e2e.ts b/tests/e2e/repo-wiki.test.e2e.ts
index f32fe3fc91..4ce66da8bc 100644
--- a/tests/e2e/repo-wiki.test.e2e.ts
+++ b/tests/e2e/repo-wiki.test.e2e.ts
@@ -4,7 +4,7 @@
// @watch end
import {expect} from '@playwright/test';
-import {test} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
for (const searchTerm of ['space', 'consectetur']) {
for (const width of [null, 2560, 4000]) {
@@ -23,6 +23,7 @@ for (const searchTerm of ['space', 'consectetur']) {
await page.getByPlaceholder('Search wiki').dispatchEvent('keyup');
// timeout is necessary because HTMX search could be slow
await expect(page.locator('#wiki-search a[href]')).toBeInViewport({ratio: 1});
+ await save_visual(page);
});
}
}
@@ -36,4 +37,5 @@ test(`Search results show titles (and not file names)`, async ({page}, workerInf
// so we manually "type" the last letter
await page.getByPlaceholder('Search wiki').dispatchEvent('keyup');
await expect(page.locator('#wiki-search a[href] b')).toHaveText('Page With Spaced Name');
+ await save_visual(page);
});
diff --git a/tests/e2e/right-settings-button.test.e2e.ts b/tests/e2e/right-settings-button.test.e2e.ts
index bfb1800a27..3bea329ba0 100644
--- a/tests/e2e/right-settings-button.test.e2e.ts
+++ b/tests/e2e/right-settings-button.test.e2e.ts
@@ -5,19 +5,12 @@
// @watch end
import {expect} from '@playwright/test';
-import {test, login_user, load_logged_in_context} from './utils_e2e.ts';
+import {save_visual, test} from './utils_e2e.ts';
-test.beforeAll(async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user2');
-});
-
-test.describe('desktop viewport', () => {
- test.use({viewport: {width: 1920, height: 300}});
-
- test('Settings button on right of repo header', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+test.describe('desktop viewport as user 2', () => {
+ test.use({user: 'user2', viewport: {width: 1920, height: 300}});
+ test('Settings button on right of repo header', async ({page}) => {
await page.goto('/user2/repo1');
const settingsBtn = page.locator('.overflow-menu-items>#settings-btn');
@@ -27,11 +20,21 @@ test.describe('desktop viewport', () => {
await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
});
- test('Settings button on right of repo header also when add more button is shown', async ({browser}, workerInfo) => {
- await login_user(browser, workerInfo, 'user12');
- const context = await load_logged_in_context(browser, workerInfo, 'user12');
- const page = await context.newPage();
+ test('Settings button on right of org header', async ({page}) => {
+ await page.goto('/org3');
+ const settingsBtn = page.locator('.overflow-menu-items>#settings-btn');
+ await expect(settingsBtn).toBeVisible();
+ await expect(settingsBtn).toHaveClass(/right/);
+
+ await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
+ });
+});
+
+test.describe('desktop viewport as user12', () => {
+ test.use({user: 'user12', viewport: {width: 1920, height: 300}});
+
+ test('Settings button on right of repo header also when add more button is shown', async ({page}) => {
await page.goto('/user12/repo10');
const settingsBtn = page.locator('.overflow-menu-items>#settings-btn');
@@ -40,19 +43,10 @@ test.describe('desktop viewport', () => {
await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
});
+});
- test('Settings button on right of org header', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
-
- await page.goto('/org3');
-
- const settingsBtn = page.locator('.overflow-menu-items>#settings-btn');
- await expect(settingsBtn).toBeVisible();
- await expect(settingsBtn).toHaveClass(/right/);
-
- await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
- });
+test.describe('desktop viewport, unauthenticated', () => {
+ test.use({viewport: {width: 1920, height: 300}});
test('User overview overflow menu should not be influenced', async ({page}) => {
await page.goto('/user2');
@@ -60,16 +54,14 @@ test.describe('desktop viewport', () => {
await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0);
await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
+ await save_visual(page);
});
});
test.describe('small viewport', () => {
- test.use({viewport: {width: 800, height: 300}});
-
- test('Settings button in overflow menu of repo header', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
+ test.use({user: 'user2', viewport: {width: 800, height: 300}});
+ test('Settings button in overflow menu of repo header', async ({page}) => {
await page.goto('/user2/repo1');
await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0);
@@ -87,12 +79,10 @@ test.describe('small viewport', () => {
const items = shownItems.concat(overflowItems);
expect(Array.from(new Set(items))).toHaveLength(items.length);
+ await save_visual(page);
});
- test('Settings button in overflow menu of org header', async ({browser}, workerInfo) => {
- const context = await load_logged_in_context(browser, workerInfo, 'user2');
- const page = await context.newPage();
-
+ test('Settings button in overflow menu of org header', async ({page}) => {
await page.goto('/org3');
await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0);
@@ -111,6 +101,10 @@ test.describe('small viewport', () => {
const items = shownItems.concat(overflowItems);
expect(Array.from(new Set(items))).toHaveLength(items.length);
});
+});
+
+test.describe('small viewport, unauthenticated', () => {
+ test.use({viewport: {width: 800, height: 300}});
test('User overview overflow menu should not be influenced', async ({page}) => {
await page.goto('/user2');
@@ -129,5 +123,6 @@ test.describe('small viewport', () => {
const items = shownItems.concat(overflowItems);
expect(Array.from(new Set(items))).toHaveLength(items.length);
+ await save_visual(page);
});
});
diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts
index 7e25441ea3..ff921a2cf3 100644
--- a/tests/e2e/utils_e2e.ts
+++ b/tests/e2e/utils_e2e.ts
@@ -1,21 +1,34 @@
import {expect, test as baseTest, type Browser, type BrowserContextOptions, type APIRequestContext, type TestInfo, type Page} from '@playwright/test';
-export const test = baseTest.extend({
- context: async ({browser}, use) => {
- return use(await test_context(browser));
- },
- // see https://playwright.dev/docs/test-fixtures#adding-global-beforeeachaftereach-hooks
- forEachTest: [async ({page}, use) => {
- await use();
- // some tests create a new page which is not yet available here
- // only operate on tests that make the URL available
- if (page.url() !== 'about:blank') {
- await save_visual(page);
+import * as path from 'node:path';
+
+const AUTH_PATH = 'tests/e2e/.auth';
+
+type AuthScope = 'logout' | 'shared' | 'webauthn';
+
+export type TestOptions = {
+ forEachTest: void
+ user: string | null;
+ authScope: AuthScope;
+};
+
+export const test = baseTest.extend
({
+ context: async ({browser, user, authScope, contextOptions}, use, {project}) => {
+ if (user && authScope) {
+ const browserName = project.name.toLowerCase().replace(' ', '-');
+ contextOptions.storageState = path.join(AUTH_PATH, `state-${browserName}-${user}-${authScope}.json`);
+ } else {
+ // if no user is given, ensure to have clean state
+ contextOptions.storageState = {cookies: [], origins: []};
}
- }, {auto: true}],
+
+ return use(await test_context(browser, contextOptions));
+ },
+ user: null,
+ authScope: 'shared',
});
-async function test_context(browser: Browser, options?: BrowserContextOptions) {
+export async function test_context(browser: Browser, options?: BrowserContextOptions) {
const context = await browser.newContext(options);
context.on('page', (page) => {
@@ -106,6 +119,7 @@ export async function save_visual(page: Page) {
// update order of recently created repos is not fully deterministic
page.locator('.flex-item-main').filter({hasText: 'relative time in repo'}),
page.locator('#activity-feed'),
+ page.locator('#user-heatmap'),
// dynamic IDs in fixed-size inputs
page.locator('input[value*="dyn-id-"]'),
],
diff --git a/tests/e2e/utils_e2e_test.go b/tests/e2e/utils_e2e_test.go
index bf1a8a418c..96fd905363 100644
--- a/tests/e2e/utils_e2e_test.go
+++ b/tests/e2e/utils_e2e_test.go
@@ -5,17 +5,27 @@ package e2e
import (
"context"
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
"net"
"net/http"
"net/url"
"os"
+ "path/filepath"
"regexp"
+ "strings"
"testing"
"time"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/json"
+ modules_session "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/tests"
+ "code.forgejo.org/go-chi/session"
"github.com/stretchr/testify/require"
)
@@ -25,6 +35,8 @@ func onForgejoRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare .
if len(prepare) == 0 || prepare[0] {
defer tests.PrepareTestEnv(t, 1)()
}
+ createSessions(t)
+
s := http.Server{
Handler: testE2eWebRoutes,
}
@@ -64,3 +76,118 @@ func onForgejoRun(t *testing.T, callback func(*testing.T, *url.URL), prepare ...
callback(t.(*testing.T), u)
}, prepare...)
}
+
+func createSessions(t testing.TB) {
+ t.Helper()
+ // copied from playwright.config.ts
+ browsers := []string{
+ "chromium",
+ "firefox",
+ "webkit",
+ "Mobile Chrome",
+ "Mobile Safari",
+ }
+ scopes := []string{
+ "shared",
+ }
+ users := []string{
+ "user1",
+ "user2",
+ "user12",
+ "user40",
+ }
+
+ authState := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", ".auth")
+ err := os.RemoveAll(authState)
+ require.NoError(t, err)
+
+ err = os.MkdirAll(authState, os.ModePerm)
+ require.NoError(t, err)
+
+ createSessionCookie := stateHelper(t)
+
+ for _, user := range users {
+ u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: strings.ToLower(user)})
+ for _, browser := range browsers {
+ for _, scope := range scopes {
+ stateFile := strings.ReplaceAll(strings.ToLower(fmt.Sprintf("state-%s-%s-%s.json", browser, user, scope)), " ", "-")
+ createSessionCookie(filepath.Join(authState, stateFile), u)
+ }
+ }
+ }
+}
+
+func stateHelper(t testing.TB) func(stateFile string, user *user_model.User) {
+ type Cookie struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ Domain string `json:"domain"`
+ Path string `json:"path"`
+ Expires int `json:"expires"`
+ HTTPOnly bool `json:"httpOnly"`
+ Secure bool `json:"secure"`
+ SameSite string `json:"sameSite"`
+ }
+
+ type BrowserState struct {
+ Cookies []Cookie `json:"cookies"`
+ Origins []string `json:"origins"`
+ }
+
+ options := session.Options{
+ Provider: setting.SessionConfig.Provider,
+ ProviderConfig: setting.SessionConfig.ProviderConfig,
+ CookieName: setting.SessionConfig.CookieName,
+ CookiePath: setting.SessionConfig.CookiePath,
+ Gclifetime: setting.SessionConfig.Gclifetime,
+ Maxlifetime: setting.SessionConfig.Maxlifetime,
+ Secure: setting.SessionConfig.Secure,
+ SameSite: setting.SessionConfig.SameSite,
+ Domain: setting.SessionConfig.Domain,
+ }
+
+ opt := session.PrepareOptions([]session.Options{options})
+
+ vsp := modules_session.VirtualSessionProvider{}
+ err := vsp.Init(opt.Maxlifetime, opt.ProviderConfig)
+ require.NoError(t, err)
+
+ return func(stateFile string, user *user_model.User) {
+ buf := make([]byte, opt.IDLength/2)
+ _, err = rand.Read(buf)
+ require.NoError(t, err)
+
+ sessionID := hex.EncodeToString(buf)
+
+ s, err := vsp.Read(sessionID)
+ require.NoError(t, err)
+
+ err = s.Set("uid", user.ID)
+ require.NoError(t, err)
+
+ err = s.Release()
+ require.NoError(t, err)
+
+ state := BrowserState{
+ Cookies: []Cookie{
+ {
+ Name: opt.CookieName,
+ Value: sessionID,
+ Domain: setting.Domain,
+ Path: "/",
+ Expires: -1,
+ HTTPOnly: true,
+ Secure: false,
+ SameSite: "Lax",
+ },
+ },
+ Origins: []string{},
+ }
+
+ jsonData, err := json.Marshal(state)
+ require.NoError(t, err)
+
+ err = os.WriteFile(stateFile, jsonData, 0o644)
+ require.NoError(t, err)
+ }
+}
diff --git a/tests/integration/actions_variables_test.go b/tests/integration/actions_variables_test.go
new file mode 100644
index 0000000000..0179a543dc
--- /dev/null
+++ b/tests/integration/actions_variables_test.go
@@ -0,0 +1,150 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ forgejo_context "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestActionVariablesModification(t *testing.T) {
+ defer tests.AddFixtures("tests/integration/fixtures/TestActionVariablesModification")()
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ userVariable := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: 1001, OwnerID: user.ID})
+ userURL := "/user/settings/actions/variables"
+ org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
+ orgVariable := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: 1002, OwnerID: org.ID})
+ orgURL := "/org/" + org.Name + "/settings/actions/variables"
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user.ID})
+ repoVariable := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: 1003, RepoID: repo.ID})
+ repoURL := "/" + repo.FullName() + "/settings/actions/variables"
+ admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
+ globalVariable := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: 1004}, "owner_id = 0 AND repo_id = 0")
+ adminURL := "/admin/actions/variables"
+
+ adminSess := loginUser(t, admin.Name)
+ adminCSRF := GetCSRF(t, adminSess, "/")
+ sess := loginUser(t, user.Name)
+ csrf := GetCSRF(t, sess, "/")
+
+ type errorJSON struct {
+ Error string `json:"errorMessage"`
+ }
+
+ test := func(t *testing.T, fail bool, baseURL string, id int64) {
+ defer tests.PrintCurrentTest(t, 1)()
+ t.Helper()
+
+ sess := sess
+ csrf := csrf
+ if baseURL == adminURL {
+ sess = adminSess
+ csrf = adminCSRF
+ }
+
+ req := NewRequestWithValues(t, "POST", baseURL+fmt.Sprintf("/%d/edit", id), map[string]string{
+ "_csrf": csrf,
+ "name": "glados_quote",
+ "data": "I'm fine. Two plus two is...ten, in base four, I'm fine!",
+ })
+ if fail {
+ resp := sess.MakeRequest(t, req, http.StatusBadRequest)
+ var error errorJSON
+ DecodeJSON(t, resp, &error)
+ assert.EqualValues(t, "Failed to find the variable.", error.Error)
+ } else {
+ sess.MakeRequest(t, req, http.StatusOK)
+ flashCookie := sess.GetCookie(forgejo_context.CookieNameFlash)
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DThe%2Bvariable%2Bhas%2Bbeen%2Bedited.", flashCookie.Value)
+ }
+
+ req = NewRequestWithValues(t, "POST", baseURL+fmt.Sprintf("/%d/delete", id), map[string]string{
+ "_csrf": csrf,
+ })
+ if fail {
+ resp := sess.MakeRequest(t, req, http.StatusBadRequest)
+ var error errorJSON
+ DecodeJSON(t, resp, &error)
+ assert.EqualValues(t, "Failed to find the variable.", error.Error)
+ } else {
+ sess.MakeRequest(t, req, http.StatusOK)
+ flashCookie := sess.GetCookie(forgejo_context.CookieNameFlash)
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DThe%2Bvariable%2Bhas%2Bbeen%2Bremoved.", flashCookie.Value)
+ }
+ }
+
+ t.Run("User variable", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, userVariable.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, userVariable.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ test(t, true, adminURL, userVariable.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, false, userURL, userVariable.ID)
+ })
+ })
+
+ t.Run("Organisation variable", func(t *testing.T) {
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, orgVariable.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, orgVariable.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ test(t, true, adminURL, userVariable.ID)
+ })
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, false, orgURL, orgVariable.ID)
+ })
+ })
+
+ t.Run("Repository variable", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, repoVariable.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, repoVariable.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ test(t, true, adminURL, userVariable.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, false, repoURL, repoVariable.ID)
+ })
+ })
+
+ t.Run("Global variable", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, globalVariable.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, globalVariable.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, globalVariable.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ test(t, false, adminURL, globalVariable.ID)
+ })
+ })
+}
diff --git a/tests/integration/api_helper_for_declarative_test.go b/tests/integration/api_helper_for_declarative_test.go
index dae71ca8ef..5bd8dc5567 100644
--- a/tests/integration/api_helper_for_declarative_test.go
+++ b/tests/integration/api_helper_for_declarative_test.go
@@ -22,6 +22,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/forms"
+ "github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -461,3 +462,27 @@ func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, r
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
}
}
+
+// generate and activate an ssh key for the user attached to the APITestContext
+// TODO: pick a better name; golang doesn't do method overriding.
+func withCtxKeyFile(t *testing.T, ctx APITestContext, callback func()) {
+ // we need to have write:public_key to do this step
+ // the easiest way is to create a throwaway ctx that is identical but only has that permission
+ ctxKeyWriter := ctx
+ ctxKeyWriter.Token = getTokenForLoggedInUser(t, ctx.Session, auth.AccessTokenScopeWriteUser)
+
+ keyName := "One of " + ctx.Username + "'s keys: #" + uuid.New().String()
+ withKeyFile(t, keyName, func(keyFile string) {
+ var key api.PublicKey
+
+ doAPICreateUserKey(ctxKeyWriter, keyName, keyFile,
+ func(t *testing.T, _key api.PublicKey) {
+ // save the key ID so we can delete it at the end
+ key = _key
+ })(t)
+
+ defer doAPIDeleteUserKey(ctxKeyWriter, key.ID)(t)
+
+ callback()
+ })
+}
diff --git a/tests/integration/api_token_test.go b/tests/integration/api_token_test.go
index 01d18ef6f1..f94a0986f2 100644
--- a/tests/integration/api_token_test.go
+++ b/tests/integration/api_token_test.go
@@ -30,6 +30,23 @@ func TestAPICreateAndDeleteToken(t *testing.T) {
deleteAPIAccessToken(t, newAccessToken, user)
}
+func TestAPIGetTokens(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+ // with basic auth...
+ req := NewRequest(t, "GET", "/api/v1/users/user2/tokens").
+ AddBasicAuth(user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ // ... or with a token.
+ newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, []auth_model.AccessTokenScope{auth_model.AccessTokenScopeAll})
+ req = NewRequest(t, "GET", "/api/v1/users/user2/tokens").
+ AddTokenAuth(newAccessToken.Token)
+ MakeRequest(t, req, http.StatusOK)
+ deleteAPIAccessToken(t, newAccessToken, user)
+}
+
// TestAPIDeleteMissingToken ensures that error is thrown when token not found
func TestAPIDeleteMissingToken(t *testing.T) {
defer tests.PrepareTestEnv(t)()
diff --git a/tests/integration/explore_org_test.go b/tests/integration/explore_org_test.go
new file mode 100644
index 0000000000..e0c48ccf0d
--- /dev/null
+++ b/tests/integration/explore_org_test.go
@@ -0,0 +1,49 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestExploreOrg(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // Set the default sort order
+ defer test.MockVariableValue(&setting.UI.ExploreDefaultSort, "alphabetically")()
+
+ cases := []struct{ sortOrder, expected string }{
+ {"", "?sort=" + setting.UI.ExploreDefaultSort + "&q="},
+ {"newest", "?sort=newest&q="},
+ {"oldest", "?sort=oldest&q="},
+ {"alphabetically", "?sort=alphabetically&q="},
+ {"reversealphabetically", "?sort=reversealphabetically&q="},
+ }
+ for _, c := range cases {
+ req := NewRequest(t, "GET", "/explore/organizations?sort="+c.sortOrder)
+ resp := MakeRequest(t, req, http.StatusOK)
+ h := NewHTMLParser(t, resp.Body)
+ href, _ := h.Find(`.ui.dropdown .menu a.active.item[href^="?sort="]`).Attr("href")
+ assert.Equal(t, c.expected, href)
+ }
+
+ // these sort orders shouldn't be supported, to avoid leaking user activity
+ cases404 := []string{
+ "/explore/organizations?sort=mostMembers",
+ "/explore/organizations?sort=leastGroups",
+ "/explore/organizations?sort=leastupdate",
+ "/explore/organizations?sort=reverseleastupdate",
+ }
+ for _, c := range cases404 {
+ req := NewRequest(t, "GET", c).SetHeader("Accept", "text/html")
+ MakeRequest(t, req, http.StatusNotFound)
+ }
+}
diff --git a/tests/integration/explore_user_test.go b/tests/integration/explore_user_test.go
index 441d89cea5..d1e3fd85af 100644
--- a/tests/integration/explore_user_test.go
+++ b/tests/integration/explore_user_test.go
@@ -7,6 +7,8 @@ import (
"net/http"
"testing"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -15,8 +17,11 @@ import (
func TestExploreUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
+ // Set the default sort order
+ defer test.MockVariableValue(&setting.UI.ExploreDefaultSort, "reversealphabetically")()
+
cases := []struct{ sortOrder, expected string }{
- {"", "?sort=newest&q="},
+ {"", "?sort=" + setting.UI.ExploreDefaultSort + "&q="},
{"newest", "?sort=newest&q="},
{"oldest", "?sort=oldest&q="},
{"alphabetically", "?sort=alphabetically&q="},
diff --git a/tests/integration/fixtures/TestActionVariablesModification/action_variable.yml b/tests/integration/fixtures/TestActionVariablesModification/action_variable.yml
new file mode 100644
index 0000000000..925838d0f0
--- /dev/null
+++ b/tests/integration/fixtures/TestActionVariablesModification/action_variable.yml
@@ -0,0 +1,31 @@
+-
+ id: 1001
+ name: GLADOS_QUOTE
+ owner_id: 2
+ repo_id: 0
+ data: ""
+ created_unix: 1737000000
+
+-
+ id: 1002
+ name: GLADOS_QUOTE
+ owner_id: 3
+ repo_id: 0
+ data: ""
+ created_unix: 1737000001
+
+-
+ id: 1003
+ name: GLADOS_QUOTE
+ owner_id: 0
+ repo_id: 1
+ data: ""
+ created_unix: 1737000002
+
+-
+ id: 1004
+ name: GLADOS_QUOTE
+ owner_id: 0
+ repo_id: 0
+ data: ""
+ created_unix: 1737000003
diff --git a/tests/integration/fixtures/TestRunnerModification/action_runner.yml b/tests/integration/fixtures/TestRunnerModification/action_runner.yml
new file mode 100644
index 0000000000..95599b19bd
--- /dev/null
+++ b/tests/integration/fixtures/TestRunnerModification/action_runner.yml
@@ -0,0 +1,31 @@
+-
+ id: 1001
+ uuid: "43b5d4d3-401b-42f9-94df-a9d45b447b82"
+ name: "User runner"
+ owner_id: 2
+ repo_id: 0
+ deleted: 0
+
+-
+ id: 1002
+ uuid: "bdc77f4f-2b2b-442d-bd44-e808f4306347"
+ name: "Organisation runner"
+ owner_id: 3
+ repo_id: 0
+ deleted: 0
+
+-
+ id: 1003
+ uuid: "9268bc8c-efbf-4dbe-aeb5-945733cdd098"
+ name: "Repository runner"
+ owner_id: 0
+ repo_id: 1
+ deleted: 0
+
+-
+ id: 1004
+ uuid: "fb857e63-c0ce-4571-a6c9-fde26c128073"
+ name: "Global runner"
+ owner_id: 0
+ repo_id: 0
+ deleted: 0
diff --git a/tests/integration/git_annex_test.go b/tests/integration/git_annex_test.go
new file mode 100644
index 0000000000..efcb571aa2
--- /dev/null
+++ b/tests/integration/git_annex_test.go
@@ -0,0 +1,2925 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integration
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/perm"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/annex"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/require"
+)
+
+// Some guidelines:
+//
+// * a APITestContext is an awkward union of session credential + username + target repo
+// which is assumed to be owned by that username; if you want to target a different
+// repo, you need to edit its .Reponame or just ignore it and write "username/reponame.git"
+
+func doCreateRemoteAnnexRepository(t *testing.T, u *url.URL, ctx APITestContext, private bool, objectFormat git.ObjectFormat) (err error) {
+ // creating a repo counts as editing the user's profile (is done by POSTing
+ // to /api/v1/user/repos/) -- which means it needs a User-scoped token and
+ // both that and editing need a Repo-scoped token because they edit repositories.
+ rescopedCtx := ctx
+ rescopedCtx.Token = getTokenForLoggedInUser(t, ctx.Session, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
+ doAPICreateRepository(rescopedCtx, false, objectFormat)(t)
+ t.Cleanup(func() { util.MakeWritable(setting.RepoRootPath) })
+ doAPIEditRepository(rescopedCtx, &api.EditRepoOption{Private: &private})(t)
+
+ repoURL := createSSHUrl(ctx.GitPath(), u)
+
+ // Fill in fixture data
+ withAnnexCtxKeyFile(t, ctx, func() {
+ err = doInitRemoteAnnexRepository(t, repoURL)
+ })
+ if err != nil {
+ return fmt.Errorf("Unable to initialize remote repo with git-annex fixture: %w", err)
+ }
+ return nil
+}
+
+func TestGitAnnexWebUpload(t *testing.T) {
+ if !setting.Annex.Enabled {
+ t.Skip("Skipping since annex support is disabled.")
+ }
+
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) {
+ ctx := NewAPITestContext(t, "user2", "annex-web-upload-test"+objectFormat.Name(), auth_model.AccessTokenScopeWriteRepository)
+ require.NoError(t, doCreateRemoteAnnexRepository(t, u, ctx, false, objectFormat))
+
+ uploadFile := func(t *testing.T, path string) string {
+ t.Helper()
+
+ body := &bytes.Buffer{}
+ mpForm := multipart.NewWriter(body)
+ err := mpForm.WriteField("_csrf", GetCSRF(t, ctx.Session, ctx.Username+"/"+ctx.Reponame+"/_upload/"+setting.Repository.DefaultBranch))
+ require.NoError(t, err)
+
+ file, err := mpForm.CreateFormFile("file", filepath.Base(path))
+ require.NoError(t, err)
+
+ srcFile, err := os.Open(path)
+ require.NoError(t, err)
+
+ io.Copy(file, srcFile)
+ require.NoError(t, mpForm.Close())
+
+ req := NewRequestWithBody(t, "POST", "/"+ctx.Username+"/"+ctx.Reponame+"/upload-file", body)
+ req.Header.Add("Content-Type", mpForm.FormDataContentType())
+ resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+
+ respMap := map[string]string{}
+ DecodeJSON(t, resp, &respMap)
+ return respMap["uuid"]
+ }
+
+ // Generate random file
+ tmpFile := path.Join(t.TempDir(), "web-upload-test-file.bin")
+ require.NoError(t, generateRandomFile(1024*1024/4, tmpFile))
+ expectedContent, err := os.ReadFile(tmpFile)
+ require.NoError(t, err)
+
+ // Upload generated file
+ fileUUID := uploadFile(t, tmpFile)
+ req := NewRequestWithValues(t, "POST", ctx.Username+"/"+ctx.Reponame+"/_upload/"+setting.Repository.DefaultBranch, map[string]string{
+ "commit_choice": "direct",
+ "files": fileUUID,
+ "_csrf": GetCSRF(t, ctx.Session, ctx.Username+"/"+ctx.Reponame+"/_upload/"+setting.Repository.DefaultBranch),
+ "commit_mail_id": "-1",
+ })
+ ctx.Session.MakeRequest(t, req, http.StatusSeeOther)
+
+ // Get some handles on the target repository and file
+ remoteRepoPath := path.Join(setting.RepoRootPath, ctx.GitPath())
+ repo, err := git.OpenRepository(git.DefaultContext, remoteRepoPath)
+ require.NoError(t, err)
+ defer repo.Close()
+ tree, err := repo.GetTree(setting.Repository.DefaultBranch)
+ require.NoError(t, err)
+ treeEntry, err := tree.GetTreeEntryByPath(filepath.Base(tmpFile))
+ require.NoError(t, err)
+ blob := treeEntry.Blob()
+
+ // Check that the uploaded file is annexed
+ isAnnexed, err := annex.IsAnnexed(blob)
+ require.NoError(t, err)
+ require.True(t, isAnnexed)
+
+ // Check that the uploaded file has the correct content
+ annexedFile, err := annex.Content(blob)
+ require.NoError(t, err)
+ actualContent, err := io.ReadAll(annexedFile)
+ require.NoError(t, err)
+ require.Equal(t, expectedContent, actualContent)
+ })
+ })
+}
+
+func TestGitAnnexMedia(t *testing.T) {
+ if !setting.Annex.Enabled {
+ t.Skip("Skipping since annex support is disabled.")
+ }
+
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) {
+ ctx := NewAPITestContext(t, "user2", "annex-media-test"+objectFormat.Name(), auth_model.AccessTokenScopeWriteRepository)
+
+ // create a public repo
+ require.NoError(t, doCreateRemoteAnnexRepository(t, u, ctx, false, objectFormat))
+
+ // the filenames here correspond to specific cases defined in doInitAnnexRepository()
+ t.Run("AnnexSymlink", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doAnnexMediaTest(t, ctx, "annexed.tiff")
+ })
+ t.Run("AnnexPointer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doAnnexMediaTest(t, ctx, "annexed.bin")
+ })
+ })
+ })
+}
+
+func doAnnexMediaTest(t *testing.T, ctx APITestContext, file string) {
+ // Make sure that downloading via /media on the website recognizes it should give the annexed content
+
+ // TODO:
+ // - [ ] roll this into TestGitAnnexPermissions to ensure that permission enforcement works correctly even on /media?
+
+ session := loginUser(t, ctx.Username) // logs in to the http:// site/API, storing a cookie;
+ // this is a different auth method than the git+ssh:// or git+http:// protocols TestGitAnnexPermissions uses!
+
+ // compute server-side path of the annexed file
+ remoteRepoPath := path.Join(setting.RepoRootPath, ctx.GitPath())
+ remoteObjectPath, err := contentLocation(remoteRepoPath, file)
+ require.NoError(t, err)
+
+ // download annexed file
+ localObjectPath := path.Join(t.TempDir(), file)
+ fd, err := os.OpenFile(localObjectPath, os.O_CREATE|os.O_WRONLY, 0o777)
+ defer fd.Close()
+ require.NoError(t, err)
+
+ mediaLink := path.Join("/", ctx.Username, ctx.Reponame, "/media/branch/master", file)
+ req := NewRequest(t, "GET", mediaLink)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ _, err = io.Copy(fd, resp.Body)
+ require.NoError(t, err)
+ fd.Close()
+
+ // verify the download
+ match, err := tests.FileCmp(localObjectPath, remoteObjectPath, 0)
+ require.NoError(t, err)
+ require.True(t, match, "Annexed files should be the same")
+}
+
+func TestGitAnnexViews(t *testing.T) {
+ if !setting.Annex.Enabled {
+ t.Skip("Skipping since annex support is disabled.")
+ }
+
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) {
+ ctx := NewAPITestContext(t, "user2", "annex-template-render-test"+objectFormat.Name(), auth_model.AccessTokenScopeWriteRepository)
+
+ // create a public repo
+ require.NoError(t, doCreateRemoteAnnexRepository(t, u, ctx, false, objectFormat))
+
+ session := loginUser(t, ctx.Username)
+
+ t.Run("Index", func(t *testing.T) {
+ // test that annexed files render with the binary file icon on the main list
+ defer tests.PrintCurrentTest(t)()
+
+ repoLink := path.Join("/", ctx.Username, ctx.Reponame)
+ req := NewRequest(t, "GET", repoLink)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ isFileBinaryIconLocked := htmlDoc.Find("tr[data-entryname='annexed.tiff'] > td.name svg").HasClass("octicon-file-binary")
+ require.True(t, isFileBinaryIconLocked, "locked annexed files should render a binary file icon")
+ isFileBinaryIconUnlocked := htmlDoc.Find("tr[data-entryname='annexed.bin'] > td.name svg").HasClass("octicon-file-binary")
+ require.True(t, isFileBinaryIconUnlocked, "unlocked annexed files should render a binary file icon")
+ })
+
+ t.Run("View", func(t *testing.T) {
+ // test how routers/web/repo/view.go + templates/repo/view_file.tmpl handle annexed files
+ defer tests.PrintCurrentTest(t)()
+
+ doViewTest := func(file string) (htmlDoc *HTMLDoc, viewLink, mediaLink string) {
+ viewLink = path.Join("/", ctx.Username, ctx.Reponame, "/src/branch/master", file)
+ // rawLink := strings.Replace(viewLink, "/src/", "/raw/", 1) // TODO: do something with this?
+ mediaLink = strings.Replace(viewLink, "/src/", "/media/", 1)
+
+ req := NewRequest(t, "GET", viewLink)
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ // the first button on the toolbar on the view template is the "Raw" button
+ // this CSS selector is the most precise I can think to use
+ buttonLink, exists := htmlDoc.Find(".file-header").Find("a[download]").Attr("href")
+ require.True(t, exists, "Download button should exist on the file header")
+ require.EqualValues(t, mediaLink, buttonLink, "Download link should use /media URL for annex files")
+
+ return htmlDoc, viewLink, mediaLink
+ }
+
+ t.Run("Binary", func(t *testing.T) {
+ // test that annexing a file renders the /media link in /src and NOT the /raw link
+ defer tests.PrintCurrentTest(t)()
+
+ doBinaryViewTest := func(file string) {
+ htmlDoc, _, mediaLink := doViewTest(file)
+
+ rawLink, exists := htmlDoc.Find("div.file-view > div.view-raw > a").Attr("href")
+ require.True(t, exists, "Download link should render instead of content because this is a binary file")
+ require.EqualValues(t, mediaLink, rawLink)
+ }
+
+ t.Run("AnnexSymlink", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doBinaryViewTest("annexed.tiff")
+ })
+ t.Run("AnnexPointer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doBinaryViewTest("annexed.bin")
+ })
+ })
+
+ t.Run("Text", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ doTextViewTest := func(file string) {
+ htmlDoc, _, _ := doViewTest(file)
+ require.True(t, htmlDoc.Find("div.file-view").Is(".code-view"), "should render as code")
+ }
+
+ t.Run("AnnexSymlink", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doTextViewTest("annexed.txt")
+
+ t.Run("Markdown", func(t *testing.T) {
+ // special case: check that markdown can be pulled out of the annex and rendered, too
+ defer tests.PrintCurrentTest(t)()
+ htmlDoc, _, _ := doViewTest("annexed.md")
+ require.True(t, htmlDoc.Find("div.file-view").Is(".markdown"), "should render as markdown")
+ })
+ })
+ t.Run("AnnexPointer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ doTextViewTest("annexed.rst")
+
+ t.Run("Markdown", func(t *testing.T) {
+ // special case: check that markdown can be pulled out of the annex and rendered, too
+ defer tests.PrintCurrentTest(t)()
+ htmlDoc, _, _ := doViewTest("annexed.markdown")
+ require.True(t, htmlDoc.Find("div.file-view").Is(".markdown"), "should render as markdown")
+ })
+ })
+ })
+ })
+ })
+ })
+}
+
+/*
+Test that permissions are enforced on git-annex-shell commands.
+
+ Along the way, this also tests that uploading, downloading, and deleting all work,
+ so we haven't written separate tests for those.
+*/
+func TestGitAnnexPermissions(t *testing.T) {
+ if !setting.Annex.Enabled {
+ t.Skip("Skipping since annex support is disabled.")
+ }
+
+ // Each case below is split so that 'clone' is done as
+ // the repo owner, but 'copy' as the user under test.
+ //
+ // Otherwise, in cases where permissions block the
+ // initial 'clone', the test would simply end there
+ // and never verify if permissions apply properly to
+ // 'annex copy' -- potentially leaving a security gap.
+
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ // Tell git-annex to allow http://127.0.0.1, http://localhost and http://::1. Without
+ // this, all `git annex` commands will silently fail when run against http:// remotes
+ // without explaining what's wrong.
+ //
+ // Note: onGiteaRun() sets up an alternate HOME so this actually edits
+ // tests/integration/gitea-integration-*/data/home/.gitconfig and
+ // if you're debugging you need to remember to match that.
+ _, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "config").AddOptionValues("--global").AddArguments("annex.security.allowed-ip-addresses", "all").RunStdString(&git.RunOpts{})
+ require.NoError(t, err)
+
+ forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) {
+ t.Run("Public", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ ownerCtx := NewAPITestContext(t, "user2", "annex-public"+objectFormat.Name(), auth_model.AccessTokenScopeWriteRepository)
+
+ // create a public repo
+ require.NoError(t, doCreateRemoteAnnexRepository(t, u, ownerCtx, false, objectFormat))
+
+ // double-check it's public
+ repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ownerCtx.Username, ownerCtx.Reponame)
+ require.NoError(t, err)
+ require.False(t, repo.IsPrivate)
+
+ remoteRepoPath := path.Join(setting.RepoRootPath, ownerCtx.GitPath()) // path on disk -- which can be examined directly because we're testing from localhost
+
+ // Different sessions, so we can test different permissions.
+ // We leave Reponame blank because we don't actually then later add it according to each case if needed
+ //
+ // NB: these usernames need to match appropriate entries in models/fixtures/user.yml
+ writerCtx := NewAPITestContext(t, "user5", "", auth_model.AccessTokenScopeWriteRepository)
+ readerCtx := NewAPITestContext(t, "user4", "", auth_model.AccessTokenScopeReadRepository)
+ outsiderCtx := NewAPITestContext(t, "user8", "", auth_model.AccessTokenScopeReadRepository) // a user with no specific access
+
+ // set up collaborators
+ doAPIAddCollaborator(ownerCtx, readerCtx.Username, perm.AccessModeRead)(t)
+ doAPIAddCollaborator(ownerCtx, writerCtx.Username, perm.AccessModeWrite)(t)
+
+ // tests
+ t.Run("Owner", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Writer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Reader", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath), "Uploading should fail due to permissions")
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Outsider", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath), "Uploading should fail due to permissions")
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err = git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Anonymous", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Only HTTP and P2PHTTP have an anonymous mode
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ // unlike the other tests, at this step we *do not* define credentials:
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ // unlike the other tests, at this step we *do not* define credentials:
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Delete the repo, make sure it's fully gone
+ doAPIDeleteRepository(ownerCtx)(t)
+ _, statErr := os.Stat(remoteRepoPath)
+ require.True(t, os.IsNotExist(statErr), "Remote annex repo should be removed from disk")
+ })
+ })
+
+ t.Run("Private", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ ownerCtx := NewAPITestContext(t, "user2", "annex-private"+objectFormat.Name(), auth_model.AccessTokenScopeWriteRepository)
+
+ // create a private repo
+ require.NoError(t, doCreateRemoteAnnexRepository(t, u, ownerCtx, true, objectFormat))
+
+ // double-check it's private
+ repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, ownerCtx.Username, ownerCtx.Reponame)
+ require.NoError(t, err)
+ require.True(t, repo.IsPrivate)
+
+ remoteRepoPath := path.Join(setting.RepoRootPath, ownerCtx.GitPath()) // path on disk -- which can be examined directly because we're testing from localhost
+
+ // Different sessions, so we can test different permissions.
+ // We leave Reponame blank because we don't actually then later add it according to each case if needed
+ //
+ // NB: these usernames need to match appropriate entries in models/fixtures/user.yml
+ writerCtx := NewAPITestContext(t, "user5", "", auth_model.AccessTokenScopeWriteRepository)
+ readerCtx := NewAPITestContext(t, "user4", "", auth_model.AccessTokenScopeReadRepository)
+ outsiderCtx := NewAPITestContext(t, "user8", "", auth_model.AccessTokenScopeReadRepository) // a user with no specific access
+ // Note: there's also full anonymous access, which is only available for public HTTP repos;
+ // it should behave the same as 'outsider' but we (will) test it separately below anyway
+
+ // set up collaborators
+ doAPIAddCollaborator(ownerCtx, readerCtx.Username, perm.AccessModeRead)(t)
+ doAPIAddCollaborator(ownerCtx, writerCtx.Username, perm.AccessModeWrite)(t)
+
+ // tests
+ t.Run("Owner", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Writer", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, writerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+
+ t.Run("Reader", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath), "Uploading should fail due to permissions")
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Unset annexurl so that git-annex uses the dumb http support
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.NoError(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.NoError(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, readerCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("Outsider", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ t.Run("SSH", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createSSHUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxKeyFile(t, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexInitTest(remoteRepoPath, repoPath), "annex init should fail due to permissions")
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath), "annex copy --from should fail due to permissions")
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath), "annex copy --to should fail due to permissions")
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxKeyFile(t, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ // Try unsetting annexurl
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.Error(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexLocalDropTest(repoPath))
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ withAnnexCtxHTTPPassword(t, u, outsiderCtx, func() {
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+ })
+ })
+
+ t.Run("Anonymous", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Only HTTP and P2PHTTP have an anonymous mode
+ t.Run("HTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ // unlike the other tests, at this step we *do not* define credentials:
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+
+ // Try unsetting annexurl
+ _, _, err := git.NewCommand(git.DefaultContext, "config", "--unset", "remote.origin.annexurl").RunStdString(&git.RunOpts{Dir: repoPath})
+ require.Error(t, err)
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexLocalDropTest(repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+
+ t.Run("P2PHTTP", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ repoURL := createHTTPUrl(ownerCtx.GitPath(), u)
+
+ repoPath := path.Join(t.TempDir(), ownerCtx.Reponame)
+ defer util.RemoveAll(repoPath) // cleans out git-annex lockdown permissions
+
+ withAnnexCtxHTTPPassword(t, u, ownerCtx, func() {
+ doGitClone(repoPath, repoURL)(t)
+ })
+
+ // unlike the other tests, at this step we *do not* define credentials:
+
+ t.Run("Init", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexInitTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("LocalDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexLocalDropTest(repoPath))
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexDownloadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("RemoteDrop", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexRemoteDropTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexUploadTest(remoteRepoPath, repoPath))
+ })
+
+ t.Run("TestremoteReadOnly", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadOnlyTest(repoPath))
+ })
+
+ t.Run("TestremoteReadWrite", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ require.Error(t, doAnnexTestremoteReadWriteTest(repoPath))
+ })
+ })
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ // Delete the repo, make sure it's fully gone
+ doAPIDeleteRepository(ownerCtx)(t)
+ _, statErr := os.Stat(remoteRepoPath)
+ require.True(t, os.IsNotExist(statErr), "Remote annex repo should be removed from disk")
+ })
+ })
+ })
+ })
+}
+
+/*
+Test that 'git annex init' works.
+
+ precondition: repoPath contains a pre-cloned repo set up by doInitAnnexRepository().
+*/
+func doAnnexInitTest(remoteRepoPath, repoPath string) (err error) {
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "init", "cloned-repo").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return fmt.Errorf("Couldn't `git annex init`: %w", err)
+ }
+
+ // - method 0: 'git config remote.origin.annex-uuid'.
+ // Demonstrates that 'git annex init' successfully contacted
+ // the remote git-annex and was able to learn its ID number.
+ readAnnexUUID, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "config", "remote.origin.annex-uuid").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return fmt.Errorf("Couldn't read remote `git config remote.origin.annex-uuid`: %w", err)
+ }
+ readAnnexUUID = strings.TrimSpace(readAnnexUUID)
+
+ match := regexp.MustCompile("^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$").MatchString(readAnnexUUID)
+ if !match {
+ return fmt.Errorf("'git config remote.origin.annex-uuid' should have been able to download the remote's uuid; but instead read '%s'", readAnnexUUID)
+ }
+
+ remoteAnnexUUID, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "config", "annex.uuid").RunStdString(&git.RunOpts{Dir: remoteRepoPath})
+ if err != nil {
+ return fmt.Errorf("Couldn't read local `git config annex.uuid`: %w", err)
+ }
+
+ remoteAnnexUUID = strings.TrimSpace(remoteAnnexUUID)
+ match = regexp.MustCompile("^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$").MatchString(remoteAnnexUUID)
+ if !match {
+ return fmt.Errorf("'git annex init' should have been able to download the remote's uuid; but instead read '%s'", remoteAnnexUUID)
+ }
+
+ if readAnnexUUID != remoteAnnexUUID {
+ return fmt.Errorf("'git annex init' should have read the expected annex UUID '%s', but instead got '%s'", remoteAnnexUUID, readAnnexUUID)
+ }
+
+ // - method 1: 'git annex whereis'.
+ // Demonstrates that git-annex understands annexed files can be found in the remote annex.
+ annexWhereis, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "whereis", "annexed.bin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return fmt.Errorf("Couldn't `git annex whereis`: %w", err)
+ }
+ // Note: this regex is unanchored because 'whereis' outputs multiple lines containing
+ // headers and 1+ remotes and we just want to find one of them.
+ match = regexp.MustCompile(regexp.QuoteMeta(remoteAnnexUUID) + " -- .* \\[origin\\]\n").MatchString(annexWhereis)
+ if !match {
+ return errors.New("'git annex whereis' should report files are known to be in [origin]")
+ }
+
+ return nil
+}
+
+func doAnnexTestremoteReadWriteTest(repoPath string) (err error) {
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "testremote", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func doAnnexTestremoteReadOnlyTest(repoPath string) (err error) {
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "testremote", "origin", "--test-readonly", "annexed.tiff").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func doAnnexDownloadTest(remoteRepoPath, repoPath string) (err error) {
+ // NB: this test does something slightly different if run separately from "doAnnexInitTest()":
+ // "git annex copy" will notice and run "git annex init", silently.
+ // This shouldn't change any results, but be aware in case it does.
+
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "copy", "--from", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+
+ // verify the files downloaded
+
+ cmp := func(filename string) error {
+ localObjectPath, err := contentLocation(repoPath, filename)
+ if err != nil {
+ return err
+ }
+ // localObjectPath := path.Join(repoPath, filename) // or, just compare against the checked-out file
+
+ remoteObjectPath, err := contentLocation(remoteRepoPath, filename)
+ if err != nil {
+ return err
+ }
+
+ match, err := tests.FileCmp(localObjectPath, remoteObjectPath, 0)
+ if err != nil {
+ return err
+ }
+ if !match {
+ return errors.New("Annexed files should be the same")
+ }
+
+ return nil
+ }
+
+ // this is the annex-symlink file
+ stat, err := os.Lstat(path.Join(repoPath, "annexed.tiff"))
+ if err != nil {
+ return fmt.Errorf("Lstat: %w", err)
+ }
+ if !((stat.Mode() & os.ModeSymlink) != 0) {
+ // this line is really just double-checking that the text fixture is set up correctly
+ return errors.New("*.tiff should be a symlink")
+ }
+ if err = cmp("annexed.tiff"); err != nil {
+ return err
+ }
+
+ // this is the annex-pointer file
+ stat, err = os.Lstat(path.Join(repoPath, "annexed.bin"))
+ if err != nil {
+ return fmt.Errorf("Lstat: %w", err)
+ }
+ if !((stat.Mode() & os.ModeSymlink) == 0) {
+ // this line is really just double-checking that the text fixture is set up correctly
+ return errors.New("*.bin should not be a symlink")
+ }
+ err = cmp("annexed.bin")
+
+ return err
+}
+
+func doAnnexLocalDropTest(repoPath string) (err error) {
+ // This test assumes that files are present in repoPath, i.e. it is run after doAnnexDownloadTest.
+ // This test drops all files from the repository clone.
+ binPath, err := contentLocation(repoPath, "annexed.bin")
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(binPath)
+ if err != nil {
+ return err
+ }
+ tiffPath, err := contentLocation(repoPath, "annexed.tiff")
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(tiffPath)
+ if err != nil {
+ return err
+ }
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "drop").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(binPath)
+ if !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("annexed.bin wasn't dropped properly: %w", err)
+ }
+ _, err = os.Stat(tiffPath)
+ if !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("annexed.tiff wasn't dropped properly: %w", err)
+ }
+ return nil
+}
+
+func doAnnexUploadTest(remoteRepoPath, repoPath string) (err error) {
+ // NB: this test does something slightly different if run separately from "Init":
+ // it first runs "git annex init" silently in the background.
+ // This shouldn't change any results, but be aware in case it does.
+
+ err = generateRandomFile(1024*1024/4, path.Join(repoPath, "contribution.bin"))
+ if err != nil {
+ return err
+ }
+
+ err = git.AddChanges(repoPath, false, ".")
+ if err != nil {
+ return err
+ }
+
+ err = git.CommitChanges(repoPath, git.CommitChangesOptions{Message: "Annex another file"})
+ if err != nil {
+ return err
+ }
+
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "copy", "--to", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+
+ // verify the file was uploaded
+ blob, err := blobForFile(repoPath, "contribution.bin")
+ if err != nil {
+ return err
+ }
+ key, err := annex.LookupKey(blob)
+ if err != nil {
+ return err
+ }
+ localObjectPath, err := annex.ContentLocationFromKey(repoPath, key)
+ if err != nil {
+ return err
+ }
+
+ remoteObjectPath, err := annex.ContentLocationFromKey(remoteRepoPath, key)
+ if err != nil {
+ return err
+ }
+
+ match, err := tests.FileCmp(localObjectPath, remoteObjectPath, 0)
+ if err != nil {
+ return err
+ }
+ if !match {
+ return errors.New("Annexed files should be the same")
+ }
+
+ return nil
+}
+
+func doAnnexRemoteDropTest(remoteRepoPath, repoPath string) (err error) {
+ // This test assumes that files are present in repoPath, i.e. it is run after doAnnexDownloadTest.
+ // This test drops all files from the remote repository.
+ binPath, err := contentLocation(remoteRepoPath, "annexed.bin")
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(binPath)
+ if err != nil {
+ return err
+ }
+ tiffPath, err := contentLocation(remoteRepoPath, "annexed.tiff")
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(tiffPath)
+ if err != nil {
+ return err
+ }
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "drop", "--from", "origin").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+ _, err = os.Stat(binPath)
+ if !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("annexed.bin wasn't dropped properly: %w", err)
+ }
+ _, err = os.Stat(tiffPath)
+ if !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("annexed.tiff wasn't dropped properly: %w", err)
+ }
+ return nil
+}
+
+// ---- Helpers ----
+
+func generateRandomFile(size int, path string) (err error) {
+ // Generate random file
+
+ // XXX TODO: maybe this should not be random, but instead a predictable pattern, so that the test is deterministic
+ bufSize := 4 * 1024
+ if bufSize > size {
+ bufSize = size
+ }
+
+ buffer := make([]byte, bufSize)
+
+ f, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ written := 0
+ for written < size {
+ n := size - written
+ if n > bufSize {
+ n = bufSize
+ }
+ _, err := rand.Read(buffer[:n])
+ if err != nil {
+ return err
+ }
+ n, err = f.Write(buffer[:n])
+ if err != nil {
+ return err
+ }
+ written += n
+ }
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// ---- Annex-specific helpers ----
+
+/*
+Initialize a repo with some baseline annexed and non-annexed files.
+
+ TODO: perhaps this generator could be replaced with a fixture (see
+ integrations/gitea-repositories-meta/ and models/fixtures/repository.yml).
+ However we reuse this template for -different- repos, so maybe not.
+*/
+func doInitAnnexRepository(repoPath string) error {
+ // set up what files should be annexed
+ // in this case, all *.bin files will be annexed
+ // without this, git-annex's default config annexes every file larger than some number of megabytes
+ f, err := os.Create(path.Join(repoPath, ".gitattributes"))
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ // set up git-annex to store certain filetypes via *annex* pointers
+ // (https://git-annex.branchable.com/internals/pointer_file/).
+ // but only when run via 'git add' (see git-annex-smudge(1))
+ _, err = f.WriteString("* annex.largefiles=anything\n")
+ if err != nil {
+ return err
+ }
+ _, err = f.WriteString("*.bin filter=annex\n")
+ if err != nil {
+ return err
+ }
+ _, err = f.WriteString("*.rst filter=annex\n")
+ if err != nil {
+ return err
+ }
+ _, err = f.WriteString("*.markdown filter=annex\n")
+ if err != nil {
+ return err
+ }
+ f.Close()
+
+ err = git.AddChanges(repoPath, false, ".")
+ if err != nil {
+ return err
+ }
+ err = git.CommitChanges(repoPath, git.CommitChangesOptions{Message: "Configure git-annex settings"})
+ if err != nil {
+ return err
+ }
+
+ // 'git annex init'
+ err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "init", "test-repo").Run(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+
+ // add files to the annex, stored via annex symlinks
+ // // a binary file
+ err = generateRandomFile(1024*1024/4, path.Join(repoPath, "annexed.tiff"))
+ if err != nil {
+ return err
+ }
+
+ // // a text file
+ err = os.WriteFile(path.Join(repoPath, "annexed.md"), []byte("Overview\n=====\n\n1. Profit\n2. ???\n3. Review Life Activations\n"), 0o777)
+ if err != nil {
+ return err
+ }
+
+ // // a markdown file
+ err = os.WriteFile(path.Join(repoPath, "annexed.txt"), []byte("We're going to see the wizard\nThe wonderful\nMonkey of\nBoz\n"), 0o777)
+ if err != nil {
+ return err
+ }
+
+ err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "add", ".").Run(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+
+ // add files to the annex, stored via git-annex-smudge
+ // // a binary file
+ err = generateRandomFile(1024*1024/4, path.Join(repoPath, "annexed.bin"))
+ if err != nil {
+ return err
+ }
+
+ // // a text file
+ err = os.WriteFile(path.Join(repoPath, "annexed.rst"), []byte("Title\n=====\n\n- this is to test annexing a text file\n- lists are fun\n"), 0o777)
+ if err != nil {
+ return err
+ }
+
+ // // a markdown file
+ err = os.WriteFile(path.Join(repoPath, "annexed.markdown"), []byte("Overview\n=====\n\n1. Profit\n2. ???\n3. Review Life Activations\n"), 0o777)
+ if err != nil {
+ return err
+ }
+
+ err = git.AddChanges(repoPath, false, ".")
+ if err != nil {
+ return err
+ }
+
+ // save everything
+ err = git.CommitChanges(repoPath, git.CommitChangesOptions{Message: "Annex files"})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+/*
+Initialize a remote repo with some baseline annexed and non-annexed files.
+*/
+func doInitRemoteAnnexRepository(t *testing.T, repoURL *url.URL) error {
+ repoPath := path.Join(t.TempDir(), path.Base(repoURL.Path))
+ // This clone is immediately thrown away, which
+ // helps force the tests to be end-to-end.
+ defer util.RemoveAll(repoPath)
+
+ doGitClone(repoPath, repoURL)(t) // TODO: this call is the only reason for the testing.T; can it be removed?
+
+ err := doInitAnnexRepository(repoPath)
+ if err != nil {
+ return err
+ }
+
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "sync", "--content").RunStdString(&git.RunOpts{Dir: repoPath})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func blobForFile(repoPath, file string) (*git.Blob, error) {
+ repo, err := git.OpenRepository(git.DefaultContext, repoPath)
+ if err != nil {
+ return nil, err
+ }
+ defer repo.Close()
+
+ commitID, err := repo.GetRefCommitID("HEAD") // NB: to examine a *branch*, prefix with "refs/branch/", or call repo.GetBranchCommitID(); ditto for tags
+ if err != nil {
+ return nil, err
+ }
+
+ commit, err := repo.GetCommit(commitID)
+ if err != nil {
+ return nil, err
+ }
+
+ treeEntry, err := commit.GetTreeEntryByPath(file)
+ if err != nil {
+ return nil, err
+ }
+
+ return treeEntry.Blob(), nil
+}
+
+/*
+Find the path in .git/annex/objects/ of the contents for a given annexed file.
+
+ repoPath: the git repository to examine
+ file: the path (in the repo's current HEAD) of the annex pointer
+
+ TODO: pass a parameter to allow examining non-HEAD branches
+*/
+func contentLocation(repoPath, file string) (path string, err error) {
+ blob, err := blobForFile(repoPath, file)
+ if err != nil {
+ return "", err
+ }
+ return annex.ContentLocation(blob)
+}
+
+/* like withKeyFile(), but automatically sets it the account given in ctx for use by git-annex */
+func withAnnexCtxKeyFile(t *testing.T, ctx APITestContext, callback func()) {
+ _gitAnnexUseGitSSH, gitAnnexUseGitSSHExists := os.LookupEnv("GIT_ANNEX_USE_GIT_SSH")
+ defer func() {
+ // reset
+ if gitAnnexUseGitSSHExists {
+ t.Setenv("GIT_ANNEX_USE_GIT_SSH", _gitAnnexUseGitSSH)
+ }
+ }()
+
+ t.Setenv("GIT_ANNEX_USE_GIT_SSH", "1") // withKeyFile works by setting GIT_SSH_COMMAND, but git-annex only respects that if this is set
+
+ withCtxKeyFile(t, ctx, callback)
+}
+
+/*
+Like withKeyFile(), but sets HTTP credentials instead of SSH credentials.
+
+ It does this by temporarily arranging through `git config --global`
+ to use git-credential-store(1) with the password written to a tempfile.
+
+ This is the only reliable way to pass HTTP credentials non-interactively
+ to git-annex. See https://git-annex.branchable.com/bugs/http_remotes_ignore_annex.web-options_--netrc/#comment-b5a299e9826b322f2d85c96d4929a430
+ for joeyh's proclamation on the subject.
+
+ This **is only effective** when used around git.NewCommandContextNoGlobals() calls.
+ git.NewCommand() disables credential.helper as a precaution (see modules/git/git.go).
+
+ In contrast, the tests in git_test.go put the password in the remote's URL like
+ `git config remote.origin.url http://user2:password@localhost:3003/user2/repo-name.git`,
+ writing the password in repoPath+"/.git/config". That would be equally good, except
+ that git-annex ignores it!
+*/
+func withAnnexCtxHTTPPassword(t *testing.T, u *url.URL, ctx APITestContext, callback func()) {
+ credentialedURL := *u
+ credentialedURL.User = url.UserPassword(ctx.Username, userPassword) // NB: all test users use the same password
+
+ credentialedAnnexURL := *u
+ credentialedAnnexURL.Host = strings.ReplaceAll(credentialedAnnexURL.Host, "127.0.0.1", "localhost")
+ credentialedAnnexURL.Scheme = "annex+" + credentialedAnnexURL.Scheme
+ credentialedAnnexURL.Path += "git-annex-p2phttp"
+ credentialedAnnexURL.User = url.UserPassword(ctx.Username, userPassword) // NB: all test users use the same password
+
+ creds := path.Join(t.TempDir(), "creds")
+ require.NoError(t, os.WriteFile(creds, []byte(credentialedURL.String()+"\n"+credentialedAnnexURL.String()+"\n"), 0o600))
+
+ originalCredentialHelper, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "config").AddOptionValues("--global", "credential.helper").RunStdString(&git.RunOpts{})
+ if err != nil && !git.IsErrorExitCode(err, 1) {
+ // ignore the 'error' thrown when credential.helper is unset (when git config returns 1)
+ // but catch all others
+ require.NoError(t, err)
+ }
+ hasOriginalCredentialHelper := (err == nil)
+
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "config").AddOptionValues("--global", "credential.helper", fmt.Sprintf("store --file=%s", creds)).RunStdString(&git.RunOpts{})
+ require.NoError(t, err)
+
+ defer (func() {
+ // reset
+ if hasOriginalCredentialHelper {
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "config").AddOptionValues("--global").AddArguments("credential.helper").AddDynamicArguments(originalCredentialHelper).RunStdString(&git.RunOpts{})
+ } else {
+ _, _, err = git.NewCommandContextNoGlobals(git.DefaultContext, "config").AddOptionValues("--global").AddOptionValues("--unset").AddArguments("credential.helper").RunStdString(&git.RunOpts{})
+ }
+ require.NoError(t, err)
+ })()
+
+ callback()
+}
diff --git a/tests/integration/git_helper_for_declarative_test.go b/tests/integration/git_helper_for_declarative_test.go
index 83d8177460..575f01dcdb 100644
--- a/tests/integration/git_helper_for_declarative_test.go
+++ b/tests/integration/git_helper_for_declarative_test.go
@@ -42,6 +42,28 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) {
"ssh -o \"UserKnownHostsFile=/dev/null\" -o \"StrictHostKeyChecking=no\" -o \"IdentitiesOnly=yes\" -i \""+keyFile+"\" \"$@\""), 0o700)
require.NoError(t, err)
+ // reset ssh wrapper afterwards
+ _gitSSH, gitSSHExists := os.LookupEnv("GIT_SSH")
+ defer func() {
+ if gitSSHExists {
+ os.Setenv("GIT_SSH", _gitSSH)
+ }
+ }()
+
+ _gitSSHCommand, gitSSHCommandExists := os.LookupEnv("GIT_SSH_COMMAND")
+ defer func() {
+ if gitSSHCommandExists {
+ os.Setenv("GIT_SSH_COMMAND", _gitSSHCommand)
+ }
+ }()
+
+ _gitSSHVariant, gitSSHVariantExists := os.LookupEnv("GIT_SSH_VARIANT")
+ defer func() {
+ if gitSSHVariantExists {
+ os.Setenv("GIT_SSH_VARIANT", _gitSSHVariant)
+ }
+ }()
+
// Setup ssh wrapper
t.Setenv("GIT_SSH", path.Join(tmpDir, "ssh"))
t.Setenv("GIT_SSH_COMMAND",
@@ -51,6 +73,13 @@ func withKeyFile(t *testing.T, keyname string, callback func(string)) {
callback(keyFile)
}
+func createHTTPUrl(gitPath string, u *url.URL) *url.URL {
+ // this assumes u contains the HTTP base URL that Gitea is running on
+ u2 := *u
+ u2.Path = gitPath
+ return &u2
+}
+
func createSSHUrl(gitPath string, u *url.URL) *url.URL {
u2 := *u
u2.Scheme = "ssh"
diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go
index 4c0a64ad70..3cdb0b8a28 100644
--- a/tests/integration/issue_test.go
+++ b/tests/integration/issue_test.go
@@ -1336,3 +1336,46 @@ func TestIssueCount(t *testing.T) {
allCount := htmlDoc.doc.Find("a[data-test-name='all-issue-count']").Text()
assert.Contains(t, allCount, "2\u00a0All")
}
+
+func TestIssuePostersSearch(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ type userSearchInfo struct {
+ UserID int64 `json:"user_id"`
+ UserName string `json:"username"`
+ }
+
+ type userSearchResponse struct {
+ Results []*userSearchInfo `json:"results"`
+ }
+
+ t.Run("Name search", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ defer test.MockVariableValue(&setting.UI.DefaultShowFullName, false)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/issues/posters?q=USer2")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var data userSearchResponse
+ DecodeJSON(t, resp, &data)
+
+ assert.Len(t, data.Results, 1)
+ assert.EqualValues(t, "user2", data.Results[0].UserName)
+ assert.EqualValues(t, 2, data.Results[0].UserID)
+ })
+
+ t.Run("Full name search", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+ defer test.MockVariableValue(&setting.UI.DefaultShowFullName, true)()
+
+ req := NewRequest(t, "GET", "/user2/repo1/issues/posters?q=OnE")
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var data userSearchResponse
+ DecodeJSON(t, resp, &data)
+
+ assert.Len(t, data.Results, 1)
+ assert.EqualValues(t, "user1", data.Results[0].UserName)
+ assert.EqualValues(t, 1, data.Results[0].UserID)
+ })
+}
diff --git a/tests/integration/private_project_test.go b/tests/integration/private_project_test.go
new file mode 100644
index 0000000000..1a4adb4366
--- /dev/null
+++ b/tests/integration/private_project_test.go
@@ -0,0 +1,84 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package integration
+
+import (
+ "net/http"
+ "strings"
+ "testing"
+
+ org_model "code.gitea.io/gitea/models/organization"
+ project_model "code.gitea.io/gitea/models/project"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPrivateIssueProject(t *testing.T) {
+ defer tests.AddFixtures("models/fixtures/PrivateIssueProjects/")()
+ defer tests.PrepareTestEnv(t)()
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ sess := loginUser(t, user2.Name)
+
+ test := func(t *testing.T, sess *TestSession, username string, projectID int64, hasAccess bool) {
+ t.Helper()
+ defer tests.PrintCurrentTest(t, 1)()
+
+ // Test that the projects overview page shows the correct open and close issues.
+ req := NewRequestf(t, "GET", "%s/-/projects", username)
+ resp := sess.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ openCloseStats := htmlDoc.Find(".milestone-toolbar .group").First().Text()
+ if hasAccess {
+ assert.Contains(t, openCloseStats, "1\u00a0Open")
+ } else {
+ assert.Contains(t, openCloseStats, "0\u00a0Open")
+ }
+ assert.Contains(t, openCloseStats, "0\u00a0Closed")
+
+ // Check that on the project itself the issue is not shown.
+ req = NewRequestf(t, "GET", "%s/-/projects/%d", username, projectID)
+ resp = sess.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc = NewHTMLParser(t, resp.Body)
+ htmlDoc.AssertElement(t, ".project-column .issue-card", hasAccess)
+
+ // And that the issue count is correct.
+ issueCount := strings.TrimSpace(htmlDoc.Find(".project-column-issue-count").Text())
+ if hasAccess {
+ assert.EqualValues(t, "1", issueCount)
+ } else {
+ assert.EqualValues(t, "0", issueCount)
+ }
+ }
+
+ t.Run("Organization project", func(t *testing.T) {
+ org := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: 3})
+ orgProject := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1001, OwnerID: org.ID})
+
+ t.Run("Authenticated user", func(t *testing.T) {
+ test(t, sess, org.Name, orgProject.ID, true)
+ })
+
+ t.Run("Anonymous user", func(t *testing.T) {
+ test(t, emptyTestSession(t), org.Name, orgProject.ID, false)
+ })
+ })
+
+ t.Run("User project", func(t *testing.T) {
+ userProject := unittest.AssertExistsAndLoadBean(t, &project_model.Project{ID: 1002, OwnerID: user2.ID})
+
+ t.Run("Authenticated user", func(t *testing.T) {
+ test(t, sess, user2.Name, userProject.ID, true)
+ })
+
+ t.Run("Anonymous user", func(t *testing.T) {
+ test(t, emptyTestSession(t), user2.Name, userProject.ID, false)
+ })
+ })
+}
diff --git a/tests/integration/pull_icon_test.go b/tests/integration/pull_icon_test.go
index 8fde547ce9..b678550c30 100644
--- a/tests/integration/pull_icon_test.go
+++ b/tests/integration/pull_icon_test.go
@@ -133,7 +133,7 @@ func testPullRequestListIcon(t *testing.T, doc *HTMLDoc, name, expectedColor, ex
}
func createOpenPullRequest(ctx context.Context, t *testing.T, user *user_model.User, repo *repo_model.Repository) *issues_model.PullRequest {
- pull := createPullRequest(t, user, repo, "open")
+ pull := createPullRequest(t, user, repo, "branch-open", "open")
assert.False(t, pull.Issue.IsClosed)
assert.False(t, pull.HasMerged)
@@ -143,7 +143,7 @@ func createOpenPullRequest(ctx context.Context, t *testing.T, user *user_model.U
}
func createOpenWipPullRequest(ctx context.Context, t *testing.T, user *user_model.User, repo *repo_model.Repository) *issues_model.PullRequest {
- pull := createPullRequest(t, user, repo, "open-wip")
+ pull := createPullRequest(t, user, repo, "branch-open-wip", "open-wip")
err := issue_service.ChangeTitle(ctx, pull.Issue, user, "WIP: "+pull.Issue.Title)
require.NoError(t, err)
@@ -156,7 +156,7 @@ func createOpenWipPullRequest(ctx context.Context, t *testing.T, user *user_mode
}
func createClosedPullRequest(ctx context.Context, t *testing.T, user *user_model.User, repo *repo_model.Repository) *issues_model.PullRequest {
- pull := createPullRequest(t, user, repo, "closed")
+ pull := createPullRequest(t, user, repo, "branch-closed", "closed")
err := issue_service.ChangeStatus(ctx, pull.Issue, user, "", true)
require.NoError(t, err)
@@ -169,7 +169,7 @@ func createClosedPullRequest(ctx context.Context, t *testing.T, user *user_model
}
func createClosedWipPullRequest(ctx context.Context, t *testing.T, user *user_model.User, repo *repo_model.Repository) *issues_model.PullRequest {
- pull := createPullRequest(t, user, repo, "closed-wip")
+ pull := createPullRequest(t, user, repo, "branch-closed-wip", "closed-wip")
err := issue_service.ChangeTitle(ctx, pull.Issue, user, "WIP: "+pull.Issue.Title)
require.NoError(t, err)
@@ -185,7 +185,7 @@ func createClosedWipPullRequest(ctx context.Context, t *testing.T, user *user_mo
}
func createMergedPullRequest(ctx context.Context, t *testing.T, user *user_model.User, repo *repo_model.Repository) *issues_model.PullRequest {
- pull := createPullRequest(t, user, repo, "merged")
+ pull := createPullRequest(t, user, repo, "branch-merged", "merged")
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
defer gitRepo.Close()
@@ -202,10 +202,7 @@ func createMergedPullRequest(ctx context.Context, t *testing.T, user *user_model
return pull
}
-func createPullRequest(t *testing.T, user *user_model.User, repo *repo_model.Repository, name string) *issues_model.PullRequest {
- branch := "branch-" + name
- title := "Testing " + name
-
+func createPullRequest(t *testing.T, user *user_model.User, repo *repo_model.Repository, branch, title string) *issues_model.PullRequest {
_, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
diff --git a/tests/integration/pull_review_test.go b/tests/integration/pull_review_test.go
index 1319db29bf..e1db171f16 100644
--- a/tests/integration/pull_review_test.go
+++ b/tests/integration/pull_review_test.go
@@ -518,7 +518,7 @@ func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) {
resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "a-test-branch", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3])
- testIssueClose(t, user1Session, elem[1], elem[2], elem[4])
+ testIssueClose(t, user1Session, elem[1], elem[2], elem[4], true)
// Get the commit SHA
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
@@ -579,8 +579,12 @@ func testSubmitReview(t *testing.T, session *TestSession, csrf, owner, repo, pul
return session.MakeRequest(t, req, expectedSubmitStatus)
}
-func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string) *httptest.ResponseRecorder {
- req := NewRequest(t, "GET", path.Join(owner, repo, "pulls", issueNumber))
+func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string, isPull bool) *httptest.ResponseRecorder {
+ issueType := "issues"
+ if isPull {
+ issueType = "pulls"
+ }
+ req := NewRequest(t, "GET", path.Join(owner, repo, issueType, issueNumber))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
index 90fc19c193..01d905895a 100644
--- a/tests/integration/repo_test.go
+++ b/tests/integration/repo_test.go
@@ -1462,3 +1462,15 @@ func TestRepoSubmoduleView(t *testing.T) {
htmlDoc.AssertElement(t, fmt.Sprintf(`tr[data-entryname="repo1"] a[href="%s"]`, u.JoinPath("/user2/repo1").String()), true)
})
}
+
+func TestBlameDirectory(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ // Ensure directory exists.
+ req := NewRequest(t, "GET", "/user2/repo59/src/branch/master/deep")
+ MakeRequest(t, req, http.StatusOK)
+
+ // Blame is not allowed
+ req = NewRequest(t, "GET", "/user2/repo59/blame/branch/master/deep")
+ MakeRequest(t, req, http.StatusNotFound)
+}
diff --git a/tests/integration/runner_test.go b/tests/integration/runner_test.go
new file mode 100644
index 0000000000..bab2a67230
--- /dev/null
+++ b/tests/integration/runner_test.go
@@ -0,0 +1,130 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package integration
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ forgejo_context "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRunnerModification(t *testing.T) {
+ defer tests.AddFixtures("tests/integration/fixtures/TestRunnerModification")()
+ defer tests.PrepareTestEnv(t)()
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ userRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 1001, OwnerID: user.ID})
+ userURL := "/user/settings/actions/runners"
+ org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
+ orgRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 1002, OwnerID: org.ID})
+ orgURL := "/org/" + org.Name + "/settings/actions/runners"
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user.ID})
+ repoRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 1003, RepoID: repo.ID})
+ repoURL := "/" + repo.FullName() + "/settings/actions/runners"
+ admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{IsAdmin: true})
+ globalRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: 1004}, "owner_id = 0 AND repo_id = 0")
+ adminURL := "/admin/actions/runners"
+
+ adminSess := loginUser(t, admin.Name)
+ adminCSRF := GetCSRF(t, adminSess, "/")
+ sess := loginUser(t, user.Name)
+ csrf := GetCSRF(t, sess, "/")
+
+ test := func(t *testing.T, fail bool, baseURL string, id int64) {
+ defer tests.PrintCurrentTest(t, 1)()
+ t.Helper()
+
+ sess := sess
+ csrf := csrf
+ if baseURL == adminURL {
+ sess = adminSess
+ csrf = adminCSRF
+ }
+
+ req := NewRequestWithValues(t, "POST", baseURL+fmt.Sprintf("/%d", id), map[string]string{
+ "_csrf": csrf,
+ "description": "New Description",
+ })
+ if fail {
+ sess.MakeRequest(t, req, http.StatusNotFound)
+ } else {
+ sess.MakeRequest(t, req, http.StatusSeeOther)
+ flashCookie := sess.GetCookie(forgejo_context.CookieNameFlash)
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DRunner%2Bupdated%2Bsuccessfully", flashCookie.Value)
+ }
+
+ req = NewRequestWithValues(t, "POST", baseURL+fmt.Sprintf("/%d/delete", id), map[string]string{
+ "_csrf": csrf,
+ })
+ if fail {
+ sess.MakeRequest(t, req, http.StatusNotFound)
+ } else {
+ sess.MakeRequest(t, req, http.StatusOK)
+ flashCookie := sess.GetCookie(forgejo_context.CookieNameFlash)
+ assert.NotNil(t, flashCookie)
+ assert.EqualValues(t, "success%3DRunner%2Bdeleted%2Bsuccessfully", flashCookie.Value)
+ }
+ }
+
+ t.Run("User runner", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, userRunner.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, userRunner.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, false, userURL, userRunner.ID)
+ })
+ })
+
+ t.Run("Organisation runner", func(t *testing.T) {
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, orgRunner.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, orgRunner.ID)
+ })
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, false, orgURL, orgRunner.ID)
+ })
+ })
+
+ t.Run("Repository runner", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, repoRunner.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, repoRunner.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, false, repoURL, repoRunner.ID)
+ })
+ })
+
+ t.Run("Global runner", func(t *testing.T) {
+ t.Run("Organisation", func(t *testing.T) {
+ test(t, true, orgURL, globalRunner.ID)
+ })
+ t.Run("User", func(t *testing.T) {
+ test(t, true, userURL, globalRunner.ID)
+ })
+ t.Run("Repository", func(t *testing.T) {
+ test(t, true, repoURL, globalRunner.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ test(t, false, adminURL, globalRunner.ID)
+ })
+ })
+}
diff --git a/tests/integration/user_dashboard_test.go b/tests/integration/user_dashboard_test.go
index abc3e065d9..0ed5193c48 100644
--- a/tests/integration/user_dashboard_test.go
+++ b/tests/integration/user_dashboard_test.go
@@ -5,12 +5,21 @@ package integration
import (
"net/http"
+ "net/url"
+ "strconv"
"strings"
"testing"
+ "code.gitea.io/gitea/models/db"
+ unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/translation"
+ issue_service "code.gitea.io/gitea/services/issue"
+ files_service "code.gitea.io/gitea/services/repository/files"
+ "code.gitea.io/gitea/tests"
+ "github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -28,3 +37,45 @@ func TestUserDashboardActionLinks(t *testing.T) {
assert.EqualValues(t, locale.TrString("new_migrate.link"), strings.TrimSpace(links.Find("a[href='/repo/migrate']").Text()))
assert.EqualValues(t, locale.TrString("new_org.link"), strings.TrimSpace(links.Find("a[href='/org/create']").Text()))
}
+
+func TestDashboardTitleRendering(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ sess := loginUser(t, user4.Name)
+
+ repo, _, f := tests.CreateDeclarativeRepo(t, user4, "",
+ []unit_model.Type{unit_model.TypePullRequests, unit_model.TypeIssues}, nil,
+ []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: "test.txt",
+ ContentReader: strings.NewReader("Just some text here"),
+ },
+ },
+ )
+ defer f()
+
+ issue := createIssue(t, user4, repo, "`:exclamation:` not rendered", "Hi there!")
+ pr := createPullRequest(t, user4, repo, "testing", "`:exclamation:` not rendered")
+
+ _, err := issue_service.CreateIssueComment(db.DefaultContext, user4, repo, issue, "hi", nil)
+ require.NoError(t, err)
+
+ _, err = issue_service.CreateIssueComment(db.DefaultContext, user4, repo, pr.Issue, "hi", nil)
+ require.NoError(t, err)
+
+ testIssueClose(t, sess, repo.OwnerName, repo.Name, strconv.Itoa(int(issue.Index)), false)
+ testIssueClose(t, sess, repo.OwnerName, repo.Name, strconv.Itoa(int(pr.Issue.Index)), true)
+
+ response := sess.MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK)
+ htmlDoc := NewHTMLParser(t, response.Body)
+
+ count := 0
+ htmlDoc.doc.Find("#activity-feed .flex-item-main .title").Each(func(i int, s *goquery.Selection) {
+ count++
+ assert.EqualValues(t, ":exclamation: not rendered", s.Text())
+ })
+
+ assert.EqualValues(t, 6, count)
+ })
+}
diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl
index e15e79952b..b8320265ab 100644
--- a/tests/mysql.ini.tmpl
+++ b/tests/mysql.ini.tmpl
@@ -97,6 +97,9 @@ DISABLE_QUERY_AUTH_TOKEN = true
[lfs]
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/lfs
+[annex]
+ENABLED = true
+
[packages]
ENABLED = true
diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl
index 340531fb38..781508a648 100644
--- a/tests/pgsql.ini.tmpl
+++ b/tests/pgsql.ini.tmpl
@@ -122,6 +122,9 @@ MINIO_LOCATION = us-east-1
MINIO_USE_SSL = false
MINIO_CHECKSUM_ALGORITHM = md5
+[annex]
+ENABLED = true
+
[packages]
ENABLED = true
diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl
index 277916a539..231c0d19c2 100644
--- a/tests/sqlite.ini.tmpl
+++ b/tests/sqlite.ini.tmpl
@@ -102,6 +102,9 @@ JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko
[lfs]
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/lfs
+[annex]
+ENABLED = true
+
[packages]
ENABLED = true
diff --git a/tests/test_utils.go b/tests/test_utils.go
index b3c03a30a1..6e9374db25 100644
--- a/tests/test_utils.go
+++ b/tests/test_utils.go
@@ -5,9 +5,11 @@
package tests
import (
+ "bytes"
"context"
"database/sql"
"fmt"
+ "io"
"os"
"path"
"path/filepath"
@@ -488,3 +490,80 @@ func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, en
return CreateDeclarativeRepoWithOptions(t, owner, opts)
}
+
+// Decide if two files have the same contents or not.
+// chunkSize is the size of the blocks to scan by; pass 0 to get a sensible default.
+// *Follows* symlinks.
+//
+// May return an error if something else goes wrong; in this case, you should ignore the value of 'same'.
+//
+// derived from https://stackoverflow.com/a/30038571
+// under CC-BY-SA-4.0 by several contributors
+func FileCmp(file1, file2 string, chunkSize int) (same bool, err error) {
+ if chunkSize == 0 {
+ chunkSize = 4 * 1024
+ }
+
+ // shortcuts: check file metadata
+ stat1, err := os.Stat(file1)
+ if err != nil {
+ return false, err
+ }
+
+ stat2, err := os.Stat(file2)
+ if err != nil {
+ return false, err
+ }
+
+ // are inputs are literally the same file?
+ if os.SameFile(stat1, stat2) {
+ return true, nil
+ }
+
+ // do inputs at least have the same size?
+ if stat1.Size() != stat2.Size() {
+ return false, nil
+ }
+
+ // long way: compare contents
+ f1, err := os.Open(file1)
+ if err != nil {
+ return false, err
+ }
+ defer f1.Close()
+
+ f2, err := os.Open(file2)
+ if err != nil {
+ return false, err
+ }
+ defer f2.Close()
+
+ b1 := make([]byte, chunkSize)
+ b2 := make([]byte, chunkSize)
+ for {
+ n1, err1 := io.ReadFull(f1, b1)
+ n2, err2 := io.ReadFull(f2, b2)
+
+ // https://pkg.go.dev/io#Reader
+ // > Callers should always process the n > 0 bytes returned
+ // > before considering the error err. Doing so correctly
+ // > handles I/O errors that happen after reading some bytes
+ // > and also both of the allowed EOF behaviors.
+
+ if !bytes.Equal(b1[:n1], b2[:n2]) {
+ return false, nil
+ }
+
+ if (err1 == io.EOF && err2 == io.EOF) || (err1 == io.ErrUnexpectedEOF && err2 == io.ErrUnexpectedEOF) {
+ return true, nil
+ }
+
+ // some other error, like a dropped network connection or a bad transfer
+ if err1 != nil {
+ return false, err1
+ }
+ if err2 != nil {
+ return false, err2
+ }
+ }
+}
diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css
index 4da871da61..726ac7e9e2 100644
--- a/web_src/css/features/gitgraph.css
+++ b/web_src/css/features/gitgraph.css
@@ -23,6 +23,18 @@
#git-graph-heading {
align-items: center;
}
+
+ #git-graph-heading-left {
+ margin-right: 1rem;
+ }
+
+ #git-graph-heading h2 {
+ flex-shrink: 0;
+ }
+
+ #git-graph-container #flow-select-refs-dropdown {
+ min-width: 250px;
+ }
}
@media (max-width: 767.98px) {
@@ -34,15 +46,10 @@
#git-graph-heading-left {
margin-bottom: 1rem;
}
-
- h2,
- #flow-select-refs-dropdown {
- max-width: 100%;
- }
}
#git-graph-container #flow-select-refs-dropdown {
- min-width: 250px;
+ flex-wrap: wrap;
}
#git-graph-container #flow-select-refs-dropdown .ui.label {
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index e9cfc1ddde..c3f3292dd4 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -244,6 +244,7 @@ td .commit-summary {
}
.repository.file.list #repo-files-table tbody .svg.octicon-file,
+.repository.file.list #repo-files-table tbody .svg.octicon-file-binary,
.repository.file.list #repo-files-table tbody .svg.octicon-file-symlink-file,
.repository.file.list #repo-files-table tbody .svg.octicon-file-directory-symlink {
color: var(--color-secondary-dark-7);
@@ -2425,6 +2426,11 @@ details.repo-search-result summary::marker {
padding-right: 22px !important; /* normal buttons have !important paddings, so we need to override it for dropdown (Add File) icons */
}
+.repo-button-row .button strong {
+ /* Workaround where 'overflow: hidden' is clipping the y-axis, force a small amount of extra padding in the y-axis. */
+ padding: .1em 0;
+}
+
.repo-button-row input {
height: 30px;
}
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 007891a39f..0d8396b6f3 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -1,5 +1,5 @@