feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
-92
View File
@@ -1,92 +0,0 @@
# End to end tests
E2e tests largely follow the same syntax as [integration tests](../integration).
Whereas integration tests are intended to mock and stress the back-end, server-side code, e2e tests the interface between front-end and back-end, as well as visual regressions with both assertions and visual comparisons.
They can be run with make commands for the appropriate backends, namely:
```shell
make test-sqlite
make test-pgsql
make test-mysql
make test-mssql
```
Make sure to perform a clean front-end build before running tests:
```
make clean frontend
```
## Install playwright system dependencies
```
npx playwright install-deps
```
## Run all tests via local act_runner
```
act_runner exec -W ./.github/workflows/pull-e2e-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest
```
## Run sqlite e2e tests
Start tests
```
make test-e2e-sqlite
```
## Run MySQL e2e tests
Setup a MySQL database inside docker
```
docker run -e "MYSQL_DATABASE=test" -e "MYSQL_ALLOW_EMPTY_PASSWORD=yes" -p 3306:3306 --rm --name mysql mysql:latest #(just ctrl-c to stop db and clean the container)
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" --rm --name elasticsearch elasticsearch:7.6.0 #(in a second terminal, just ctrl-c to stop db and clean the container)
```
Start tests based on the database container
```
TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=root TEST_MYSQL_PASSWORD='' make test-e2e-mysql
```
## Run pgsql e2e tests
Setup a pgsql database inside docker
```
docker run -e "POSTGRES_DB=test" -p 5432:5432 --rm --name pgsql postgres:latest #(just ctrl-c to stop db and clean the container)
```
Start tests based on the database container
```
TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-e2e-pgsql
```
## Run mssql e2e tests
Setup a mssql database inside docker
```
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container)
```
Start tests based on the database container
```
TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql
```
## Running individual tests
Example command to run `example.test.e2e.ts` test file:
_Note: unlike integration tests, this filtering is at the file level, not function_
For SQLite:
```
make test-e2e-sqlite#example
```
For other databases(replace `mssql` to `mysql` or `pgsql`):
```
TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql#example
```
## Visual testing
Although the main goal of e2e is assertion testing, we have added a framework for visual regress testing. If you are working on front-end features, please use the following:
- Check out `main`, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1` to generate outputs. This will initially fail, as no screenshots exist. You can run the e2e tests again to assert it passes.
- Check out your branch, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1`. You should be able to assert you front-end changes don't break any other tests unintentionally.
VISUAL_TEST=1 will create screenshots in tests/e2e/test-snapshots. The test will fail the first time this is enabled (until we get visual test image persistence figured out), because it will be testing against an empty screenshot folder.
ACCEPT_VISUAL=1 will overwrite the snapshot images with new images.
@@ -0,0 +1,20 @@
import {env} from 'node:process';
import {expect, test} from '@playwright/test';
import {login, apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
test('codeeditor textarea updates correctly', async ({page, request}) => {
const repoName = `e2e-codeeditor-${randomString(8)}`;
await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]);
try {
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/_new/main`);
await page.getByPlaceholder('Name your file…').fill('test.js');
await expect(page.locator('.editor-loading')).toBeHidden();
const editor = page.locator('.cm-content[role="textbox"]');
await expect(editor).toBeVisible();
await editor.click();
await page.keyboard.type('const hello = "world";');
await expect(page.locator('textarea[name="content"]')).toHaveValue('const hello = "world";');
} finally {
await apiDeleteRepo(request, env.GITEA_TEST_E2E_USER, repoName);
}
});
-115
View File
@@ -1,115 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// This is primarily coped from /tests/integration/integration_test.go
// TODO: Move common functions to shared file
//nolint:forbidigo // use of print functions is allowed in tests
package e2e
import (
"bytes"
"context"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/testlogger"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/tests"
)
var testE2eWebRoutes *web.Router
func TestMain(m *testing.M) {
defer log.GetManager().Close()
managerCtx, cancel := context.WithCancel(context.Background())
graceful.InitManager(managerCtx)
defer cancel()
tests.InitTest(false)
testE2eWebRoutes = routers.NormalRoutes()
err := unittest.InitFixtures(
unittest.FixturesOptions{
Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),
},
)
if err != nil {
fmt.Printf("Error initializing test database: %v\n", err)
os.Exit(1)
}
exitVal := m.Run()
testlogger.WriterCloser.Reset()
if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
fmt.Printf("util.RemoveAll: %v\n", err)
os.Exit(1)
}
if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
fmt.Printf("Unable to remove repo indexer: %v\n", err)
os.Exit(1)
}
os.Exit(exitVal)
}
// TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.ts" files in this directory and build a test for each.
func TestE2e(t *testing.T) {
// Find the paths of all e2e test files in test directory.
searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.ts")
paths, err := filepath.Glob(searchGlob)
if err != nil {
t.Fatal(err)
} else if len(paths) == 0 {
t.Fatal(fmt.Errorf("No e2e tests found in %s", searchGlob))
}
runArgs := []string{"npx", "playwright", "test"}
// To update snapshot outputs
if _, set := os.LookupEnv("ACCEPT_VISUAL"); set {
runArgs = append(runArgs, "--update-snapshots")
}
// Create new test for each input file
for _, path := range paths {
_, filename := filepath.Split(path)
testname := filename[:len(filename)-len(filepath.Ext(path))]
t.Run(testname, func(t *testing.T) {
// Default 2 minute timeout
onGiteaRun(t, func(*testing.T, *url.URL) {
cmd := exec.Command(runArgs[0], runArgs...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "GITEA_URL="+setting.AppURL)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
if err != nil {
// Currently colored output is conflicting. Using Printf until that is resolved.
fmt.Printf("%v", stdout.String())
fmt.Printf("%v", stderr.String())
log.Fatal("Playwright Failed: %s", err)
}
fmt.Printf("%v", stdout.String())
})
})
}
}
+9
View File
@@ -0,0 +1,9 @@
declare namespace NodeJS {
interface ProcessEnv {
GITEA_TEST_E2E_DOMAIN: string;
GITEA_TEST_E2E_USER: string;
GITEA_TEST_E2E_EMAIL: string;
GITEA_TEST_E2E_PASSWORD: string;
GITEA_TEST_E2E_URL: string;
}
}
@@ -0,0 +1,89 @@
import {test, expect} from '@playwright/test';
import {loginUser, baseUrl, apiUserHeaders, apiCreateUser, apiDeleteUser, apiCreateRepo, apiCreateIssue, apiStartStopwatch, timeoutFactor, randomString} from './utils.ts';
// These tests rely on a short EVENT_SOURCE_UPDATE_TIME in the e2e server config.
test.describe('events', () => {
test('notification count', async ({page, request}) => {
const owner = `ev-notif-owner-${randomString(8)}`;
const commenter = `ev-notif-commenter-${randomString(8)}`;
const repoName = `ev-notif-${randomString(8)}`;
await Promise.all([apiCreateUser(request, owner), apiCreateUser(request, commenter)]);
// Create repo and login in parallel — repo is needed for the issue, login for the event stream
await Promise.all([
apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(owner)}),
loginUser(page, owner),
]);
const badge = page.locator('a.not-mobile .notification_count');
await expect(badge).toBeHidden();
// Create issue as another user — this generates a notification delivered via server push
await apiCreateIssue(request, owner, repoName, {title: 'events notification test', headers: apiUserHeaders(commenter)});
// Wait for the notification badge to appear via server event
await expect(badge).toBeVisible({timeout: 15000 * timeoutFactor});
// Cleanup
await Promise.all([apiDeleteUser(request, commenter), apiDeleteUser(request, owner)]);
});
test('stopwatch', async ({page, request}) => {
const name = `ev-sw-${randomString(8)}`;
const headers = apiUserHeaders(name);
await apiCreateUser(request, name);
// Create repo, issue, and start stopwatch before login
await apiCreateRepo(request, {name, headers});
await apiCreateIssue(request, name, name, {title: 'events stopwatch test', headers});
await apiStartStopwatch(request, name, name, 1, {headers});
// Login — page renders with the active stopwatch element
await loginUser(page, name);
// Verify stopwatch is visible and links to the correct issue
const stopwatch = page.locator('.active-stopwatch.not-mobile');
await expect(stopwatch).toBeVisible();
// Cleanup
await apiDeleteUser(request, name);
});
test('logout propagation', async ({browser, request}) => {
const name = `ev-logout-${randomString(8)}`;
await apiCreateUser(request, name);
// Use a single context so both pages share the same session and SharedWorker
const context = await browser.newContext({baseURL: baseUrl()});
const page1 = await context.newPage();
const page2 = await context.newPage();
await loginUser(page1, name);
// Navigate page2 so it connects to the shared event stream
await page2.goto('/');
// Verify page2 is logged in
await expect(page2.getByRole('link', {name: 'Sign In'})).toBeHidden();
// Give page2's SharedWorker time to register its SSE connection on the
// server — otherwise the logout event can race the connection and be
// silently dropped. See https://github.com/go-gitea/gitea/pull/37403
// In the future, we can set an attribute to HTML page when the connection is established,
// then here we can just wait for that attribute (it should also work for the planned WebSocket SharedWorker)
await page2.waitForTimeout(500); // eslint-disable-line playwright/no-wait-for-timeout
// Logout from page1 — this sends a logout event to all tabs
await page1.goto('/user/logout');
// page2 should be redirected via the logout event
await expect(page2.getByRole('link', {name: 'Sign In'})).toBeVisible();
await context.close();
// Cleanup
await apiDeleteUser(request, name);
});
});
@@ -1,56 +0,0 @@
import {test, expect} from '@playwright/test';
import {login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
test.beforeAll(async ({browser}, workerInfo) => {
await login_user(browser, workerInfo, 'user2');
});
test('homepage', async ({page}) => {
const response = await page.goto('/');
expect(response?.status()).toBe(200); // Status OK
await expect(page).toHaveTitle(/^Gitea: Git with a cup of tea\s*$/);
await expect(page.locator('.logo')).toHaveAttribute('src', '/assets/img/logo.svg');
});
test('register', async ({page}, workerInfo) => {
const response = await page.goto('/user/sign_up');
expect(response?.status()).toBe(200); // Status OK
await page.locator('input[name=user_name]').fill(`e2e-test-${workerInfo.workerIndex}`);
await page.locator('input[name=email]').fill(`e2e-test-${workerInfo.workerIndex}@test.com`);
await page.locator('input[name=password]').fill('test123test123');
await page.locator('input[name=retype]').fill('test123test123');
await page.click('form button.ui.primary.button:visible');
// Make sure we routed to the home page. Else login failed.
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!');
save_visual(page);
});
test('login', async ({page}, workerInfo) => {
const response = await page.goto('/user/login');
expect(response?.status()).toBe(200); // Status OK
await page.locator('input[name=user_name]').fill(`user2`);
await page.locator('input[name=password]').fill(`password`);
await page.click('form button.ui.primary.button:visible');
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
save_visual(page);
});
test('logged in user', async ({browser}, workerInfo) => {
const context = await load_logged_in_context(browser, workerInfo, 'user2');
const page = await context.newPage();
await page.goto('/');
// Make sure we routed to the home page. Else login failed.
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
save_visual(page);
});
@@ -0,0 +1,17 @@
import {test, expect} from '@playwright/test';
test('explore repositories', async ({page}) => {
await page.goto('/explore/repos');
await expect(page.getByPlaceholder('Search repos…')).toBeVisible();
await expect(page.getByRole('link', {name: 'Repositories'})).toBeVisible();
});
test('explore users', async ({page}) => {
await page.goto('/explore/users');
await expect(page.getByPlaceholder('Search users…')).toBeVisible();
});
test('explore organizations', async ({page}) => {
await page.goto('/explore/organizations');
await expect(page.getByPlaceholder('Search orgs…')).toBeVisible();
});
@@ -0,0 +1,61 @@
import {env} from 'node:process';
import {expect, test} from '@playwright/test';
import {login, apiCreateRepo, apiCreateFile, apiDeleteRepo, assertFlushWithParent, assertNoJsError, randomString} from './utils.ts';
test('external file', async ({page, request}) => {
const repoName = `e2e-external-render-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await Promise.all([
apiCreateRepo(request, {name: repoName}),
login(page),
]);
try {
await apiCreateFile(request, owner, repoName, 'test.external', '<p>rendered content</p>');
await page.goto(`/${owner}/${repoName}/src/branch/main/test.external`);
const iframe = page.locator('iframe.external-render-iframe');
await expect(iframe).toBeVisible();
await expect(iframe).toHaveAttribute('data-src', new RegExp(`/${owner}/${repoName}/render/branch/main/test\\.external`));
const frame = page.frameLocator('iframe.external-render-iframe');
await expect(frame.locator('p')).toContainText('rendered content');
await assertFlushWithParent(iframe, page.locator('.file-view'));
await assertNoJsError(page);
} finally {
await apiDeleteRepo(request, owner, repoName);
}
});
test('openapi file', async ({page, request}) => {
const repoName = `e2e-openapi-render-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await Promise.all([
apiCreateRepo(request, {name: repoName}),
login(page),
]);
try {
const title = 'Test <API> & "quoted"';
const spec = JSON.stringify({
openapi: '3.0.0',
info: {title, version: '1.0'},
paths: {'/pets': {get: {responses: {'200': {description: 'OK', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}}}}}},
components: {schemas: {Pet: {type: 'object', properties: {children: {type: 'array', items: {$ref: '#/components/schemas/Pet'}}}}}},
});
await apiCreateFile(request, owner, repoName, 'openapi.json', spec);
await page.goto(`/${owner}/${repoName}/src/branch/main/openapi.json`);
const iframe = page.locator('iframe.external-render-iframe');
await expect(iframe).toBeVisible();
const viewer = page.frameLocator('iframe.external-render-iframe').locator('#frontend-render-viewer');
await expect(viewer.locator('.swagger-ui')).toBeVisible();
await expect(viewer.locator('.info .title')).toContainText(title);
// expanding the operation triggers swagger-ui's $ref resolver, which fetches window.location
// (about:srcdoc since the iframe is loaded via srcdoc); failure surfaces as "Could not resolve reference"
await viewer.locator('.opblock-tag').first().click();
await viewer.locator('.opblock').first().click();
await expect(viewer.getByText('Could not resolve reference')).toHaveCount(0);
// poll: postMessage resize may not have settled yet when the visibility checks pass
await expect.poll(async () => (await iframe.boundingBox())!.height).toBeGreaterThan(300);
await assertFlushWithParent(iframe, page.locator('.file-view'));
await assertNoJsError(page);
} finally {
await apiDeleteRepo(request, owner, repoName);
}
});
@@ -0,0 +1,69 @@
import {env} from 'node:process';
import {expect, test} from '@playwright/test';
import {apiCreateBranch, apiCreateRepo, apiCreateFile, apiDeleteRepo, assertFlushWithParent, assertNoJsError, login, randomString} from './utils.ts';
test('3d model file', async ({page, request}) => {
const repoName = `e2e-3d-render-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await apiCreateRepo(request, {name: repoName});
try {
const stl = 'solid test\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid test\n';
await apiCreateFile(request, owner, repoName, 'test.stl', stl);
await page.goto(`/${owner}/${repoName}/src/branch/main/test.stl?display=rendered`);
const iframe = page.locator('iframe.external-render-iframe');
await expect(iframe).toBeVisible();
const frame = page.frameLocator('iframe.external-render-iframe');
const viewer = frame.locator('#frontend-render-viewer');
await expect(viewer.locator('canvas')).toBeVisible();
expect((await viewer.boundingBox())!.height).toBeGreaterThan(300);
await assertFlushWithParent(iframe, page.locator('.file-view'));
// bgcolor passed via gitea-iframe-bgcolor; 3D viewer reads it from body bgcolor — must match parent
const [parentBg, iframeBg] = await Promise.all([
page.evaluate(() => getComputedStyle(document.body).backgroundColor),
frame.locator('body').evaluate((el) => getComputedStyle(el).backgroundColor),
]);
expect(iframeBg).toBe(parentBg);
await assertNoJsError(page);
} finally {
await apiDeleteRepo(request, owner, repoName);
}
});
test('pdf file', async ({page, request}) => {
// headless playwright cannot render PDFs (PDFObject.embed returns false), so this is a limited test
const repoName = `e2e-pdf-render-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await apiCreateRepo(request, {name: repoName});
try {
await apiCreateFile(request, owner, repoName, 'test.pdf', '%PDF-1.0\n%%EOF\n');
await page.goto(`/${owner}/${repoName}/src/branch/main/test.pdf`);
const container = page.locator('.file-view-render-container');
await expect(container).toHaveAttribute('data-render-name', 'pdf-viewer');
expect((await container.boundingBox())!.height).toBeGreaterThan(300);
await assertFlushWithParent(container, page.locator('.file-view'));
} finally {
await apiDeleteRepo(request, owner, repoName);
}
});
test('asciicast file', async ({page, request}) => {
// regression for repo_file.go's RefTypeNameSubURL double-escape: readme.cast on a non-ASCII branch
// is rendered via view_readme.go (no metas override), exposing the bug as a broken player URL
const repoName = `e2e-asciicast-render-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
const branch = '日本語-branch';
const branchEnc = encodeURIComponent(branch);
await Promise.all([apiCreateRepo(request, {name: repoName, autoInit: false}), login(page)]);
try {
const cast = '{"version": 2, "width": 80, "height": 24}\n[0.0, "o", "hi"]\n';
await apiCreateFile(request, owner, repoName, 'readme.cast', cast);
await apiCreateBranch(request, owner, repoName, branch);
await page.goto(`/${owner}/${repoName}/src/branch/${branchEnc}`);
const container = page.locator('.asciinema-player-container');
await expect(container).toHaveAttribute('data-asciinema-player-src', `/${owner}/${repoName}/raw/branch/${branchEnc}/readme.cast`);
await expect(container.locator('.ap-wrapper')).toBeVisible();
expect((await container.boundingBox())!.height).toBeGreaterThan(300);
} finally {
await apiDeleteRepo(request, owner, repoName);
}
});
@@ -0,0 +1,9 @@
import {test, expect} from '@playwright/test';
test('licenses.txt', async ({page}) => {
const resp = await page.goto('/assets/licenses.txt');
expect(resp?.status()).toBe(200);
const content = await resp!.text();
expect(content).toContain('@vue/');
expect(content).toContain('code.gitea.io/');
});
@@ -0,0 +1,12 @@
import {test, expect} from '@playwright/test';
import {login, logout} from './utils.ts';
test('homepage', async ({page}) => {
await page.goto('/');
await expect(page.getByRole('img', {name: 'Logo'})).toHaveAttribute('src', '/assets/img/logo.svg');
});
test('login and logout', async ({page}) => {
await login(page);
await logout(page);
});
@@ -0,0 +1,22 @@
import {env} from 'node:process';
import {expect, test} from '@playwright/test';
import {apiCreateRepo, apiHeaders, assertNoJsError, baseUrl, randomString} from './utils.ts';
test('mermaid diagram in issue', async ({page, request}) => {
const repoName = `e2e-mermaid-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await apiCreateRepo(request, {name: repoName});
const body = '```mermaid\nflowchart LR\n Alpha --> Beta\n Beta --> Gamma\n```\n';
const response = await request.post(`${baseUrl()}/api/v1/repos/${owner}/${repoName}/issues`, {
headers: apiHeaders(),
data: {title: 'mermaid test', body},
});
expect(response.ok(), `create issue failed: ${response.status()}`).toBe(true);
const {number} = await response.json();
await page.goto(`/${owner}/${repoName}/issues/${number}`);
const svg = page.frameLocator('iframe.markup-content-iframe').locator('svg');
await expect(svg).toContainText(/Alpha[\s\S]*Beta[\s\S]*Gamma/);
await assertNoJsError(page);
});
@@ -0,0 +1,13 @@
import {env} from 'node:process';
import {test, expect} from '@playwright/test';
import {login, apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
test('create a milestone', async ({page}) => {
const repoName = `e2e-milestone-${randomString(8)}`;
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/milestones/new`);
await page.getByPlaceholder('Title').fill('Test Milestone');
await page.getByRole('button', {name: 'Create Milestone'}).click();
await expect(page.locator('.milestone-list')).toContainText('Test Milestone');
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
});
@@ -0,0 +1,13 @@
import {test, expect} from '@playwright/test';
import {login, apiDeleteOrg, randomString} from './utils.ts';
test('create an organization', async ({page}) => {
const orgName = `e2e-org-${randomString(8)}`;
await login(page);
await page.goto('/org/create');
await page.getByLabel('Organization Name').fill(orgName);
await page.getByRole('button', {name: 'Create Organization'}).click();
await expect(page).toHaveURL(new RegExp(`/org/${orgName}`));
// delete via API because of issues related to form-fetch-action
await apiDeleteOrg(page.request, orgName);
});
@@ -0,0 +1,30 @@
import {env} from 'node:process';
import {expect, test} from '@playwright/test';
import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, randomString} from './utils.ts';
test('toggle issue reactions', async ({page, request}) => {
const repoName = `e2e-reactions-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
await apiCreateRepo(request, {name: repoName});
await Promise.all([
apiCreateIssue(request, owner, repoName, {title: 'Reaction test'}),
login(page),
]);
try {
await page.goto(`/${owner}/${repoName}/issues/1`);
const issueComment = page.locator('.timeline-item.comment.first');
const reactionPicker = issueComment.locator('.select-reaction');
await reactionPicker.click();
await reactionPicker.getByLabel('+1').click();
const reactions = issueComment.getByRole('group', {name: 'Reactions'});
await expect(reactions.getByRole('button', {name: /^\+1:/})).toContainText('1');
await reactions.getByRole('button', {name: /^\+1:/}).click();
await expect(reactions.getByRole('button', {name: /^\+1:/})).toHaveCount(0);
} finally {
await apiDeleteRepo(request, owner, repoName);
}
});
@@ -0,0 +1,11 @@
import {env} from 'node:process';
import {test, expect} from '@playwright/test';
import {apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
test('repo readme', async ({page}) => {
const repoName = `e2e-readme-${randomString(8)}`;
await apiCreateRepo(page.request, {name: repoName});
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}`);
await expect(page.locator('#readme')).toContainText(repoName);
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
});
@@ -0,0 +1,70 @@
import {env} from 'node:process';
import {test, expect} from '@playwright/test';
import {login, logout, apiDeleteUser, randomString} from './utils.ts';
test.beforeEach(async ({page}) => {
await page.goto('/user/sign_up');
});
test('register page has form', async ({page}) => {
await expect(page.getByLabel('Username')).toBeVisible();
await expect(page.getByLabel('Email Address')).toBeVisible();
await expect(page.getByLabel('Password', {exact: true})).toBeVisible();
await expect(page.getByLabel('Confirm Password')).toBeVisible();
await expect(page.getByRole('button', {name: 'Register Account'})).toBeVisible();
});
test('register with empty fields shows error', async ({page}) => {
// HTML5 required attribute prevents submission, so verify the fields are required
await expect(page.locator('input[name="user_name"][required]')).toBeVisible();
await expect(page.locator('input[name="email"][required]')).toBeVisible();
await expect(page.locator('input[name="password"][required]')).toBeVisible();
await expect(page.locator('input[name="retype"][required]')).toBeVisible();
});
test('register with mismatched passwords shows error', async ({page}) => {
await page.getByLabel('Username').fill('e2e-register-mismatch');
await page.getByLabel('Email Address').fill(`e2e-register-mismatch@${env.GITEA_TEST_E2E_DOMAIN}`);
await page.getByLabel('Password', {exact: true}).fill('password123!');
await page.getByLabel('Confirm Password').fill('different123!');
await page.getByRole('button', {name: 'Register Account'}).click();
await expect(page.locator('.ui.negative.message')).toBeVisible();
});
test('register then login', async ({page}) => {
const username = `e2e-register-${randomString(8)}`;
const email = `${username}@${env.GITEA_TEST_E2E_DOMAIN}`;
const password = 'password123!';
await page.getByLabel('Username').fill(username);
await page.getByLabel('Email Address').fill(email);
await page.getByLabel('Password', {exact: true}).fill(password);
await page.getByLabel('Confirm Password').fill(password);
await page.getByRole('button', {name: 'Register Account'}).click();
// After successful registration, should be redirected away from sign_up
await expect(page).not.toHaveURL(/sign_up/);
// Logout then login with the newly created account
await logout(page);
await login(page, username, password);
// delete via API because of issues related to form-fetch-action
await apiDeleteUser(page.request, username);
});
test('register with existing username shows error', async ({page}) => {
await page.getByLabel('Username').fill(env.GITEA_TEST_E2E_USER);
await page.getByLabel('Email Address').fill(`e2e-duplicate@${env.GITEA_TEST_E2E_DOMAIN}`);
await page.getByLabel('Password', {exact: true}).fill('password123!');
await page.getByLabel('Confirm Password').fill('password123!');
await page.getByRole('button', {name: 'Register Account'}).click();
await expect(page.locator('.ui.negative.message')).toBeVisible();
});
test('sign in link exists', async ({page}) => {
const signInLink = page.getByText('Sign in now!');
await expect(signInLink).toBeVisible();
await signInLink.click();
await expect(page).toHaveURL(/\/user\/login$/);
});
@@ -0,0 +1,10 @@
import {test, expect} from '@playwright/test';
import {assertNoJsError} from './utils.ts';
test('relative-time renders without errors', async ({page}) => {
await page.goto('/devtest/relative-time');
const relativeTime = page.getByTestId('relative-time-now');
await expect(relativeTime).toHaveAttribute('data-tooltip-content', /.+/);
await expect(relativeTime).toHaveText('now');
await assertNoJsError(page);
});
@@ -0,0 +1,13 @@
import {env} from 'node:process';
import {test} from '@playwright/test';
import {login, apiDeleteRepo, randomString} from './utils.ts';
test('create a repository', async ({page}) => {
const repoName = `e2e-repo-${randomString(8)}`;
await login(page);
await page.goto('/repo/create');
await page.locator('input[name="repo_name"]').fill(repoName);
await page.getByRole('button', {name: 'Create Repository'}).click();
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`));
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
});
@@ -0,0 +1,20 @@
import {test, expect} from '@playwright/test';
import {loginUser, apiCreateUser, apiDeleteUser, randomString} from './utils.ts';
test('update profile biography', async ({page, request}) => {
const username = `e2e-settings-${randomString(8)}`;
const bio = `e2e-bio-${randomString(8)}`;
await apiCreateUser(request, username);
try {
await loginUser(page, username);
await page.goto('/user/settings');
await page.getByLabel('Biography').fill(bio);
await page.getByRole('button', {name: 'Update Profile'}).click();
await expect(page.getByLabel('Biography')).toHaveValue(bio);
await page.getByLabel('Biography').fill('');
await page.getByRole('button', {name: 'Update Profile'}).click();
await expect(page.getByLabel('Biography')).toHaveValue('');
} finally {
await apiDeleteUser(request, username);
}
});
+138
View File
@@ -0,0 +1,138 @@
import {env} from 'node:process';
import {expect} from '@playwright/test';
import type {APIRequestContext, Locator, Page} from '@playwright/test';
/** Generate a random alphanumeric string. */
export function randomString(length: number): string {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let index = 0; index < length; index++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
export const timeoutFactor = Number(env.GITEA_TEST_E2E_TIMEOUT_FACTOR) || 1;
export function baseUrl() {
return env.GITEA_TEST_E2E_URL?.replace(/\/$/g, '');
}
function apiAuthHeader(username: string, password: string) {
return {Authorization: `Basic ${globalThis.btoa(`${username}:${password}`)}`};
}
export function apiHeaders() {
return apiAuthHeader(env.GITEA_TEST_E2E_USER, env.GITEA_TEST_E2E_PASSWORD);
}
async function apiRetry(fn: () => Promise<{ok: () => boolean; status: () => number; text: () => Promise<string>}>, label: string) {
const maxAttempts = 5;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await fn();
if (response.ok()) return;
if ([500, 502, 503].includes(response.status()) && attempt < maxAttempts - 1) {
const jitter = Math.random() * 500;
await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1) + jitter));
continue;
}
throw new Error(`${label} failed: ${response.status()} ${await response.text()}`);
}
}
export async function apiCreateRepo(requestContext: APIRequestContext, {name, autoInit = true, headers}: {name: string; autoInit?: boolean; headers?: Record<string, string>}) {
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/user/repos`, {
headers: headers || apiHeaders(),
data: {name, auto_init: autoInit},
}), 'apiCreateRepo');
}
export async function apiCreateIssue(requestContext: APIRequestContext, owner: string, repo: string, {title, headers}: {title: string; headers?: Record<string, string>}) {
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues`, {
headers: headers || apiHeaders(),
data: {title},
}), 'apiCreateIssue');
}
export async function apiStartStopwatch(requestContext: APIRequestContext, owner: string, repo: string, issueIndex: number, {headers}: {headers?: Record<string, string>} = {}) {
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues/${issueIndex}/stopwatch/start`, {
headers: headers || apiHeaders(),
}), 'apiStartStopwatch');
}
export async function apiCreateFile(requestContext: APIRequestContext, owner: string, repo: string, filepath: string, content: string) {
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/contents/${filepath}`, {
headers: apiHeaders(),
data: {content: globalThis.btoa(content)},
}), 'apiCreateFile');
}
export async function apiCreateBranch(requestContext: APIRequestContext, owner: string, repo: string, newBranch: string) {
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/branches`, {
headers: apiHeaders(),
data: {new_branch_name: newBranch},
}), 'apiCreateBranch');
}
export async function apiDeleteRepo(requestContext: APIRequestContext, owner: string, name: string) {
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/repos/${owner}/${name}`, {
headers: apiHeaders(),
}), 'apiDeleteRepo');
}
export async function apiDeleteOrg(requestContext: APIRequestContext, name: string) {
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/orgs/${name}`, {
headers: apiHeaders(),
}), 'apiDeleteOrg');
}
/** Password shared by all test users — used for both API user creation and browser login. */
const testUserPassword = 'e2e-password!aA1';
export function apiUserHeaders(username: string) {
return apiAuthHeader(username, testUserPassword);
}
export async function apiCreateUser(requestContext: APIRequestContext, username: string) {
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/admin/users`, {
headers: apiHeaders(),
data: {username, password: testUserPassword, email: `${username}@${env.GITEA_TEST_E2E_DOMAIN}`, must_change_password: false},
}), 'apiCreateUser');
}
export async function apiDeleteUser(requestContext: APIRequestContext, username: string) {
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/admin/users/${username}?purge=true`, {
headers: apiHeaders(),
}), 'apiDeleteUser');
}
export async function loginUser(page: Page, username: string) {
return login(page, username, testUserPassword);
}
export async function login(page: Page, username = env.GITEA_TEST_E2E_USER, password = env.GITEA_TEST_E2E_PASSWORD) {
await page.goto('/user/login');
await page.getByLabel('Username or Email Address').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', {name: 'Sign In'}).click();
await expect(page.getByRole('link', {name: 'Sign In'})).toBeHidden();
}
export async function assertNoJsError(page: Page) {
await expect(page.locator('.js-global-error')).toHaveCount(0);
}
/* asserts the child has no horizontal inset from its parent — catches padding/border anywhere
* in between regardless of which element declares it */
export async function assertFlushWithParent(child: Locator, parent: Locator) {
const [childBox, parentBox] = await Promise.all([child.boundingBox(), parent.boundingBox()]);
if (!childBox || !parentBox) throw new Error('boundingBox returned null');
expect(childBox.x).toBe(parentBox.x);
expect(childBox.width).toBe(parentBox.width);
}
export async function logout(page: Page) {
await page.context().clearCookies(); // workaround issues related to fomantic dropdown
await page.goto('/');
await expect(page.getByRole('link', {name: 'Sign In'})).toBeVisible();
}
@@ -1,62 +0,0 @@
import {expect} from '@playwright/test';
import {env} from 'node:process';
import type {Browser, Page, WorkerInfo} from '@playwright/test';
const ARTIFACTS_PATH = `tests/e2e/test-artifacts`;
const LOGIN_PASSWORD = 'password';
// log in user and store session info. This should generally be
// run in test.beforeAll(), then the session can be loaded in tests.
export async function login_user(browser: Browser, workerInfo: WorkerInfo, user: string) {
// Set up a new context
const context = await browser.newContext();
const page = await context.newPage();
// Route to login page
// Note: this could probably be done more quickly with a POST
const response = await page.goto('/user/login');
expect(response?.status()).toBe(200); // Status OK
// Fill out form
await page.locator('input[name=user_name]').fill(user);
await page.locator('input[name=password]').fill(LOGIN_PASSWORD);
await page.click('form button.ui.primary.button:visible');
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
expect(page.url(), {message: `Failed to login user ${user}`}).toBe(`${workerInfo.project.use.baseURL}/`);
// Save state
await context.storageState({path: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
return context;
}
export async function load_logged_in_context(browser: Browser, workerInfo: WorkerInfo, user: string) {
let context;
try {
context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`Could not find state for '${user}'. Did you call login_user(browser, workerInfo, '${user}') in test.beforeAll()?`);
}
}
return context;
}
export async function save_visual(page: Page) {
// Optionally include visual testing
if (env.VISUAL_TEST) {
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
// Mock page/version string
await page.locator('footer div.ui.left').evaluate((node) => node.innerHTML = 'MOCK');
await expect(page).toHaveScreenshot({
fullPage: true,
timeout: 20000,
mask: [
page.locator('.secondary-nav span>img.ui.avatar'),
page.locator('.ui.dropdown.jump.item span>img.ui.avatar'),
],
});
}
}
@@ -1,56 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package e2e
import (
"context"
"net"
"net/http"
"net/url"
"testing"
"time"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func onGiteaRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare ...bool) {
if len(prepare) == 0 || prepare[0] {
defer tests.PrepareTestEnv(t, 1)()
}
s := http.Server{
Handler: testE2eWebRoutes,
}
u, err := url.Parse(setting.AppURL)
assert.NoError(t, err)
listener, err := net.Listen("tcp", u.Host)
i := 0
for err != nil && i <= 10 {
time.Sleep(100 * time.Millisecond)
listener, err = net.Listen("tcp", u.Host)
i++
}
assert.NoError(t, err)
u.Host = listener.Addr().String()
defer func() {
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
s.Shutdown(ctx)
cancel()
}()
go s.Serve(listener)
// Started by config go ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
callback(t, u)
}
func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL), prepare ...bool) {
onGiteaRunTB(t, func(t testing.TB, u *url.URL) {
callback(t.(*testing.T), u)
}, prepare...)
}
@@ -0,0 +1,142 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"testing"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert"
)
func TestApproveAllRunsOnPullRequestPage(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
// user2 is the owner of the base repo
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user2Session := loginUser(t, user2.Name)
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// user4 is the owner of the fork repo
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
user4Session := loginUser(t, user4.Name)
user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
apiBaseRepo := createActionsTestRepo(t, user2Token, "approve-all-runs", false)
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
defer doAPIDeleteRepository(user2APICtx)(t)
runner := newMockRunner()
runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
// init workflows
wf1TreePath := ".gitea/workflows/pull_1.yml"
wf1FileContent := `name: Pull 1
on: pull_request
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- run: echo unit-test
`
opts1 := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent)
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wf1TreePath, opts1)
wf2TreePath := ".gitea/workflows/pull_2.yml"
wf2FileContent := `name: Pull 2
on: pull_request
jobs:
integration-test:
runs-on: ubuntu-latest
steps:
- run: echo integration-test
`
opts2 := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create %s"+wf2TreePath, wf2FileContent)
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wf2TreePath, opts2)
// user4 forks the repo
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
&api.CreateForkOption{
Name: new("approve-all-runs-fork"),
}).AddTokenAuth(user4Token)
resp := MakeRequest(t, req, http.StatusAccepted)
var apiForkRepo api.Repository
DecodeJSON(t, resp, &apiForkRepo)
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID})
user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository)
defer doAPIDeleteRepository(user4APICtx)(t)
// user4 creates a pull request from branch "bugfix/user4"
doAPICreateFile(user4APICtx, "user4-fix.txt", &api.CreateFileOptions{
FileOptions: api.FileOptions{
NewBranchName: "bugfix/user4",
Message: "create user4-fix.txt",
Author: api.Identity{
Name: user4.Name,
Email: user4.Email,
},
Committer: api.Identity{
Name: user4.Name,
Email: user4.Email,
},
Dates: api.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
},
ContentBase64: base64.StdEncoding.EncodeToString([]byte("user4-fix")),
})(t)
apiPull, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":bugfix/user4")(t)
assert.NoError(t, err)
// check runs
run1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, WorkflowID: "pull_1.yml"})
assert.True(t, run1.NeedApproval)
assert.Equal(t, actions_model.StatusBlocked, run1.Status)
run2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, WorkflowID: "pull_2.yml"})
assert.True(t, run2.NeedApproval)
assert.Equal(t, actions_model.StatusBlocked, run2.Status)
// user4 cannot see the approve button
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", baseRepo.OwnerName, baseRepo.Name, apiPull.Index))
resp = user4Session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assert.Zero(t, htmlDoc.doc.Find("#approve-status-checks button.link-action").Length())
// user2 can see the approve button
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", baseRepo.OwnerName, baseRepo.Name, apiPull.Index))
resp = user2Session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
dataURL, exist := htmlDoc.doc.Find("#approve-status-checks button.link-action").Attr("data-url")
assert.True(t, exist)
assert.Equal(t,
fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s",
baseRepo.Link(), apiPull.Head.Sha),
dataURL,
)
// user2 approves all runs
req = NewRequest(t, "POST", dataURL)
user2Session.MakeRequest(t, req, http.StatusOK)
// check runs
run1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run1.ID})
assert.False(t, run1.NeedApproval)
assert.Equal(t, user2.ID, run1.ApprovedBy)
assert.Equal(t, actions_model.StatusWaiting, run1.Status)
run2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2.ID})
assert.False(t, run2.NeedApproval)
assert.Equal(t, user2.ID, run2.ApprovedBy)
assert.Equal(t, actions_model.StatusWaiting, run2.Status)
})
}
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"net/url"
"strconv"
"testing"
"time"
@@ -121,59 +122,55 @@ jobs:
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+testCase.treePath, testCase.fileContent)
createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts)
runIndex := ""
var runID int64
for i := 0; i < len(testCase.outcomes); i++ {
task := runner.fetchTask(t)
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
outcome := testCase.outcomes[jobName]
assert.NotNil(t, outcome)
runner.execTask(t, task, outcome)
runIndex = task.Context.GetFields()["run_number"].GetStringValue()
assert.Equal(t, "1", runIndex)
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
parsedRunIndex, err := strconv.ParseInt(runIndex, 10, 64)
assert.NoError(t, err)
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: apiRepo.ID, Index: parsedRunIndex})
runID = run.ID
}
jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID)
assert.NoError(t, err)
for i := 0; i < len(testCase.outcomes); i++ {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
jobID := jobs[i].ID
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, apiRepo.Name, runID, jobID))
resp := session.MakeRequest(t, req, http.StatusOK)
var listResp actions.ViewResponse
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
assert.NoError(t, err)
assert.Len(t, listResp.State.Run.Jobs, 3)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)).
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, apiRepo.Name, runID, jobID)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
}
req := NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, apiRepo.Name, runID))
session.MakeRequest(t, req, http.StatusOK)
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID))
session.MakeRequest(t, req, http.StatusOK)
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID))
session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, apiRepo.Name, runID))
session.MakeRequest(t, req, http.StatusNotFound)
for i := 0; i < len(testCase.outcomes); i++ {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
jobID := jobs[i].ID
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, apiRepo.Name, runID, jobID))
session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)).
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, apiRepo.Name, runID, jobID)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
@@ -62,9 +62,8 @@ jobs:
// run the workflow with os=windows
urlStr := fmt.Sprintf("/%s/%s/actions/run?workflow=%s", user2.Name, repo.Name, "test-inputs-context.yml")
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"ref": "refs/heads/main",
"os": "windows",
"ref": "refs/heads/main",
"os": "windows",
})
session.MakeRequest(t, req, http.StatusSeeOther)
@@ -27,6 +27,7 @@ import (
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJobWithNeeds(t *testing.T) {
@@ -349,6 +350,122 @@ jobs:
})
}
func TestRunnerDisableEnable(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
t.Run("BasicDisableEnable", func(t *testing.T) {
testData := prepareRunnerDisableEnableTest(t, user2, token, "actions-runner-disable-enable", "mock-runner", "runner-disable-enable")
task1 := testData.runner.fetchTask(t)
require.NotNil(t, task1)
triggerRunnerDisableEnableRun(t, user2, token, testData.repo, "second-push.txt")
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
testData.runner.execTask(t, task1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
testData.runner.fetchNoTask(t, 2*time.Second)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
task2 := testData.runner.fetchTask(t, 5*time.Second)
require.NotNil(t, task2)
testData.runner.execTask(t, task2, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
})
t.Run("TasksVersionPath", func(t *testing.T) {
testData := prepareRunnerDisableEnableTest(t, user2, token, "actions-runner-version-path", "mock-runner-version-path", "runner-version-path")
var firstVersion int64
var task1 *runnerv1.Task
ddl := time.Now().Add(5 * time.Second)
for time.Now().Before(ddl) {
task1, firstVersion = testData.runner.fetchTaskOnce(t, 0)
if task1 != nil {
break
}
time.Sleep(200 * time.Millisecond)
}
require.NotNil(t, task1, "expected to receive first task")
require.NotZero(t, firstVersion, "response TasksVersion should be set")
// Trigger a second run so there is a pending job after we re-enable the runner
triggerRunnerDisableEnableRun(t, user2, token, testData.repo, "second-push.txt")
time.Sleep(500 * time.Millisecond) // allow workflow run to be created
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
testData.runner.execTask(t, task1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
// Fetch with the version we had before disable. Server has bumped version on disable,
// so we enter PickTask with a re-loaded runner (disabled) and get no task.
taskAfterDisable, _ := testData.runner.fetchTaskOnce(t, firstVersion)
assert.Nil(t, taskAfterDisable, "disabled runner must not receive a task when sending previous TasksVersion")
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
task2 := testData.runner.fetchTask(t, 5*time.Second)
require.NotNil(t, task2, "after re-enable runner should receive tasks again")
testData.runner.execTask(t, task2, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
})
})
}
type runnerDisableEnableTestData struct {
repo *api.Repository
runner *mockRunner
runnerID int64
}
func prepareRunnerDisableEnableTest(t *testing.T, user *user_model.User, authToken, repoName, runnerName, workflowName string) *runnerDisableEnableTestData {
t.Helper()
apiRepo := createActionsTestRepo(t, authToken, repoName, false)
runner := newMockRunner()
runner.registerAsRepoRunner(t, user.Name, apiRepo.Name, runnerName, []string{"ubuntu-latest"}, false)
wfTreePath := fmt.Sprintf(".gitea/workflows/%s.yml", workflowName)
wfContent := fmt.Sprintf(`name: %s
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo %s
`, workflowName, workflowName)
opts := getWorkflowCreateFileOptions(user, apiRepo.DefaultBranch, "create workflow", wfContent)
createWorkflowFile(t, authToken, user.Name, apiRepo.Name, wfTreePath, opts)
return &runnerDisableEnableTestData{
repo: apiRepo,
runner: runner,
runnerID: getRepoRunnerID(t, authToken, user.Name, apiRepo.Name),
}
}
func triggerRunnerDisableEnableRun(t *testing.T, user *user_model.User, authToken string, repo *api.Repository, treePath string) {
t.Helper()
opts := getWorkflowCreateFileOptions(user, repo.DefaultBranch, "second push", "second run")
createWorkflowFile(t, authToken, user.Name, repo.Name, treePath, opts)
}
func getRepoRunnerID(t *testing.T, authToken, ownerName, repoName string) int64 {
t.Helper()
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners", ownerName, repoName)).AddTokenAuth(authToken)
resp := MakeRequest(t, req, http.StatusOK)
runnerList := api.ActionRunnersResponse{}
DecodeJSON(t, resp, &runnerList)
require.Len(t, runnerList.Entries, 1)
return runnerList.Entries[0].ID
}
func TestActionsGiteaContext(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -0,0 +1,476 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
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/lfs"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsJobTokenPermissiveAccess(t *testing.T) {
cases := []struct {
name string
isFork bool
ownerPermMode repo_model.ActionsTokenPermissionMode
ownerMaxPerms map[unit_model.Type]perm.AccessMode
repoPermMode repo_model.ActionsTokenPermissionMode
repoMaxPerms map[unit_model.Type]perm.AccessMode
expectGitAccess perm.AccessMode
}{
{
name: "OwnerConfig-Permissive",
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
expectGitAccess: perm.AccessModeWrite,
},
{
name: "OwnerConfig-Permissive-CodeNone",
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
expectGitAccess: perm.AccessModeNone,
},
{
name: "OwnerConfig-Restricted",
ownerPermMode: repo_model.ActionsTokenPermissionModeRestricted,
expectGitAccess: perm.AccessModeRead,
},
// repo uses its own settings, so owner settings should not affect it
{
name: "SameRepo-Permissive",
ownerPermMode: repo_model.ActionsTokenPermissionModeRestricted,
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
expectGitAccess: perm.AccessModeWrite,
},
{
name: "SameRepo-Permissive-CodeNone",
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeRead},
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
repoMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
expectGitAccess: perm.AccessModeNone,
},
{
name: "SameRepo-Restricted",
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
expectGitAccess: perm.AccessModeRead,
},
// forks should be always restricted to max read access for code
{
name: "Fork-Permissive",
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
isFork: true,
expectGitAccess: perm.AccessModeRead,
},
{
name: "Fork-Restricted",
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
isFork: true,
expectGitAccess: perm.AccessModeRead,
},
{
name: "Fork-Restricted-CodeNone",
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
repoMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
isFork: true,
expectGitAccess: perm.AccessModeNone,
},
}
onGiteaRun(t, func(t *testing.T, u *url.URL) {
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: task.RepoID})
repoActionsUnit := repo.MustGetUnit(t.Context(), unit_model.TypeActions)
repoActionsCfg := repoActionsUnit.ActionsConfig()
ownerActionsCfg, err := actions_model.GetOwnerActionsConfig(t.Context(), repo.OwnerID)
require.NoError(t, err)
_, err = db.GetEngine(t.Context()).ID(task.RepoID).Cols("is_private").Update(&repo_model.Repository{IsPrivate: true})
require.NoError(t, err)
assertRespCodeForSuccess := func(t *testing.T, resp *httptest.ResponseRecorder, succeed bool) {
if succeed {
assert.True(t, 200 <= resp.Code && resp.Code < 300, "Expected success status code, got %d", resp.Code)
} else {
assert.True(t, 400 <= resp.Code && resp.Code < 500, "Expected client error status code, got %d", resp.Code)
}
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
// prepare owner's token permissions settings
ownerActionsCfg.TokenPermissionMode = tt.ownerPermMode
ownerActionsCfg.MaxTokenPermissions = util.Iif(tt.ownerMaxPerms == nil, nil, &repo_model.ActionsTokenPermissions{UnitAccessModes: tt.ownerMaxPerms})
require.NoError(t, actions_model.SetOwnerActionsConfig(t.Context(), repo.OwnerID, ownerActionsCfg))
// prepare repo's token permissions settings
repoActionsCfg.OverrideOwnerConfig = tt.repoPermMode != "" || tt.repoMaxPerms != nil
repoActionsCfg.TokenPermissionMode = tt.repoPermMode
repoActionsCfg.MaxTokenPermissions = util.Iif(tt.repoMaxPerms == nil, nil, &repo_model.ActionsTokenPermissions{UnitAccessModes: tt.repoMaxPerms})
require.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoActionsUnit))
// prepare task and its token
require.NoError(t, task.GenerateToken())
task.Status = actions_model.StatusRunning
task.IsForkPullRequest = tt.isFork
err := actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status", "is_fork_pull_request")
require.NoError(t, err)
require.NoError(t, task.LoadJob(t.Context()))
require.NoError(t, task.Job.LoadRun(t.Context()))
task.Job.Run.IsForkPullRequest = tt.isFork
require.NoError(t, actions_model.UpdateRun(t.Context(), task.Job.Run, "is_fork_pull_request"))
testURL := *u
testURL.User = url.UserPassword("gitea-actions", task.Token)
t.Run("ReadGitContent", func(t *testing.T) {
testURL.Path = "/user5/repo4.git/HEAD"
resp := MakeRequest(t, NewRequest(t, "GET", testURL.String()), NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess != perm.AccessModeNone)
testURL.Path = "/user5/repo4.git/info/lfs/locks"
req := NewRequest(t, "GET", testURL.String()).SetHeader("Accept", lfs.MediaType)
resp = MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess != perm.AccessModeNone)
})
t.Run("WriteGitContent", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/contents/test-filename", repo.FullName()), &structs.CreateFileOptions{
FileOptions: structs.FileOptions{NewBranchName: "new-branch" + t.Name()},
ContentBase64: base64.StdEncoding.EncodeToString([]byte(`dummy content`)),
}).AddTokenAuth(task.Token)
resp := MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess == perm.AccessModeWrite)
testURL.Path = "/user5/repo4.git/info/lfs/objects/batch"
req = NewRequestWithJSON(t, "POST", testURL.String(), lfs.BatchRequest{Operation: "upload"}).SetHeader("Accept", lfs.MediaType)
resp = MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess == perm.AccessModeWrite)
})
t.Run("NoOtherPermissions", func(t *testing.T) {
req := NewRequest(t, "DELETE", "/api/v1/repos/"+repo.FullName()).AddTokenAuth(task.Token)
resp := MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, false)
})
})
}
})
}
func TestActionsCrossRepoAccess(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
// 1. Create Organization
orgName := "org-cross-test"
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &structs.CreateOrgOption{
UserName: orgName,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
owner, err := org_model.GetOrgByName(t.Context(), orgName)
require.NoError(t, err)
// 2. Create Two Repositories in owner
createRepoInOrg := func(name string) int64 {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/repos", orgName), &structs.CreateRepoOption{
Name: name,
AutoInit: true,
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
var repo structs.Repository
DecodeJSON(t, resp, &repo)
return repo.ID
}
repoAID := createRepoInOrg("repo-A")
repoBID := createRepoInOrg("repo-B")
// 3. Enable Actions in Repo A (Source) and Repo B (Target)
enableActions := func(repoID int64) {
err := db.Insert(t.Context(), &repo_model.RepoUnit{
RepoID: repoID,
Type: unit_model.TypeActions,
Config: &repo_model.ActionsConfig{
TokenPermissionMode: repo_model.ActionsTokenPermissionModePermissive,
},
})
require.NoError(t, err)
}
enableActions(repoAID)
enableActions(repoBID)
// 4. Create Task in Repo A, and use A's token to access B
taskA := createActionTask(t, repoAID, false)
testCtxA := APITestContext{
Session: emptyTestSession(t),
Token: taskA.Token,
Username: orgName,
Reponame: "repo-B",
}
testCtxA.ExpectedCode = http.StatusOK
t.Run("PublicCrossRepoAccess", doAPIGetRepository(testCtxA, func(t *testing.T, r structs.Repository) {
assert.Equal(t, "repo-B", r.Name)
}))
// make repo-B be private
req = NewRequestWithJSON(t, "PATCH", "/api/v1/repos/org-cross-test/repo-B", &structs.EditRepoOption{Private: new(true)}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
testCtxA.ExpectedCode = http.StatusNotFound
t.Run("NoPrivateCrossRepoAccess", doAPIGetRepository(testCtxA, nil))
ownerActionsCfg := actions_model.OwnerActionsConfig{AllowedCrossRepoIDs: []int64{repoBID}}
require.NoError(t, actions_model.SetOwnerActionsConfig(t.Context(), owner.ID, ownerActionsCfg))
testCtxA.ExpectedCode = http.StatusOK
t.Run("AccessToSelectedPrivateRepo", doAPIGetRepository(testCtxA, func(t *testing.T, r structs.Repository) {
assert.Equal(t, "repo-B", r.Name)
}))
t.Run("RepoTransfer", func(t *testing.T) {
ownerActionsCfg, err := actions_model.GetOwnerActionsConfig(t.Context(), owner.ID)
require.NoError(t, err)
assert.Contains(t, ownerActionsCfg.AllowedCrossRepoIDs, repoBID)
// Transfer Repository to user4
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/repo-B/transfer", orgName), &structs.TransferRepoOption{
NewOwner: "user4",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// Accept transfer as user4
session4 := loginUser(t, "user4")
token4 := getTokenForLoggedInUser(t, session4, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/repo-B/transfer/accept", orgName)).AddTokenAuth(token4)
MakeRequest(t, req, http.StatusAccepted)
// Verify it is removed from the org's config
ownerActionsCfg, err = actions_model.GetOwnerActionsConfig(t.Context(), owner.ID)
require.NoError(t, err)
assert.NotContains(t, ownerActionsCfg.AllowedCrossRepoIDs, repoBID)
})
})
}
func TestActionsJobTokenPermissions(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("WriteIssue", TestActionsJobTokenPermissionsWriteIssue)
}
func TestActionsJobTokenPermissionsWriteIssue(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
require.Equal(t, repo.ID, task.RepoID)
require.NoError(t, db.Insert(t.Context(), &repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypeActions,
Config: &repo_model.ActionsConfig{},
}))
repoActionsUnit := repo.MustGetUnit(t.Context(), unit_model.TypeActions)
repoActionsCfg := repoActionsUnit.ActionsConfig()
repoActionsCfg.OverrideOwnerConfig = true
repoActionsCfg.TokenPermissionMode = repo_model.ActionsTokenPermissionModePermissive
repoActionsCfg.MaxTokenPermissions = nil
require.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoActionsUnit))
require.NoError(t, task.GenerateToken())
task.Status = actions_model.StatusRunning
require.NoError(t, actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status"))
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository)
labelURL := fmt.Sprintf("/api/v1/repos/%s/%s/labels", user.Name, repo.Name)
req := NewRequestWithJSON(t, "POST", labelURL, &structs.CreateLabelOption{
Name: "task-label",
Color: "0e8a16",
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
label := DecodeJSON(t, resp, &structs.Label{})
issueURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues", user.Name, repo.Name)
req = NewRequestWithJSON(t, "POST", issueURL, &structs.CreateIssueOption{
Title: "issue for actions token label deletion",
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
issue := DecodeJSON(t, resp, &structs.Issue{})
taskToken := task.Token
require.NotEmpty(t, taskToken)
issueLabelsURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", user.Name, repo.Name, issue.Index)
req = NewRequestWithJSON(t, "POST", issueLabelsURL, &structs.IssueLabelsOption{
Labels: []any{label.ID},
}).AddTokenAuth(taskToken)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%d", issueLabelsURL, label.ID)).AddTokenAuth(taskToken)
MakeRequest(t, req, http.StatusNoContent)
}
func createActionTask(t *testing.T, repoID int64, isFork bool) *actions_model.ActionTask {
job := &actions_model.ActionRunJob{
RepoID: repoID,
Status: actions_model.StatusRunning,
IsForkPullRequest: isFork,
JobID: "test_job",
Name: "test_job",
}
require.NoError(t, db.Insert(t.Context(), job))
task := &actions_model.ActionTask{
JobID: job.ID,
RepoID: repoID,
Status: actions_model.StatusRunning,
IsForkPullRequest: isFork,
}
require.NoError(t, task.GenerateToken())
require.NoError(t, db.Insert(t.Context(), task))
return task
}
func TestActionsTokenPermissionsPersistenceWithWorkflow(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// create repos
repo1 := createActionsTestRepo(t, token, "actions-permission-repo1", false)
repo2 := createActionsTestRepo(t, token, "actions-permission-repo2", true)
// add repo2 to owner-level cross-repo access list
req := NewRequestWithValues(t, "POST", "/user/settings/actions/general", map[string]string{
"cross_repo_add_target": "true",
"cross_repo_add_target_name": repo2.Name,
})
session.MakeRequest(t, req, http.StatusOK)
// create the runner for repo1
runner1 := newMockRunner()
runner1.registerAsRepoRunner(t, user2.Name, repo1.Name, "mock-runner", []string{"ubuntu-latest"}, false)
// set repo1 actions token permission mode to "permissive"
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo1.Name), map[string]string{
"token_permission_mode": "permissive",
"override_owner_config": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// set repo2 actions token permission mode to "restricted", and set max permissions
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeReleases)): "read",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// create a workflow file with "permission" keyword for repo1
wfTreePath := ".gitea/workflows/test_permissions.yml"
wfFileContent := `name: Test Permissions
on:
push:
paths:
- '.gitea/workflows/test_permissions.yml'
jobs:
job-override:
runs-on: ubuntu-latest
permissions:
code: write
steps:
- run: echo "test perms"
`
opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent)
createWorkflowFile(t, token, user2.Name, repo1.Name, wfTreePath, opts)
task1 := runner1.fetchTask(t)
task1Token := task1.Secrets["GITEA_TOKEN"]
require.NotEmpty(t, task1Token)
// should fail: target repo does not allow code access
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo2.Name)).AddTokenAuth(task1Token)
MakeRequest(t, req, http.StatusNotFound)
// set repo2 max permission to "read" so that the actions token can access code
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "read",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeReleases)): "read",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// should succeed: target repo now allows code read access for this token
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo2.Name)).AddTokenAuth(task1Token)
MakeRequest(t, req, http.StatusOK)
// but it should not have write access
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
MakeRequest(t, req, http.StatusUnauthorized)
// set repo1&repo2 max permission to "write" so that the actions token can access code
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo1.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "write",
})
session.MakeRequest(t, req, http.StatusSeeOther)
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "write",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// now task1 has write access to repo1, but still only read access to repo2 (different repo)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo1.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
MakeRequest(t, req, http.StatusOK)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
MakeRequest(t, req, http.StatusUnauthorized)
})
}
@@ -0,0 +1,77 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/url"
"os"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/dbfs"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
actions_module "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/storage"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Regression for https://gitea.com/gitea/runner/issues/950: a runner that
// finalizes a task with no log output sends UpdateLog{Rows:[], NoMore:true}.
// The previous short-circuit on len(Rows)==0 skipped TransferLogs, leaving
// an orphan dbfs_data row. Verify the row is now archived and removed.
func TestActionsLogFinalizeWithoutRows(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
apiRepo := createActionsTestRepo(t, token, "actions-finalize-no-rows", false)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
runner := newMockRunner()
runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
const wfTreePath = ".gitea/workflows/finalize-no-rows.yml"
wfFileContent := fmt.Sprintf(`name: finalize-no-rows
on:
push:
paths:
- '%s'
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: noop
`, wfTreePath)
createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "trigger", wfFileContent))
task := runner.fetchTask(t)
resp, err := runner.client.runnerServiceClient.UpdateLog(t.Context(), connect.NewRequest(&runnerv1.UpdateLogRequest{
TaskId: task.Id,
Index: 0,
Rows: nil,
NoMore: true,
}))
require.NoError(t, err)
assert.EqualValues(t, 0, resp.Msg.AckIndex)
freshTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id})
require.True(t, freshTask.LogInStorage, "log_in_storage must flip after empty NoMore=true")
_, err = storage.Actions.Stat(freshTask.LogFilename)
assert.NoError(t, err, "archived log must exist in storage")
_, err = dbfs.Open(t.Context(), actions_module.DBFSPrefix+freshTask.LogFilename)
assert.ErrorIs(t, err, os.ErrNotExist, "DBFS row must be cleaned up after TransferLogs")
})
}
@@ -7,12 +7,10 @@ import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"testing"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
@@ -171,7 +169,7 @@ jobs:
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
// fetch and execute tasks
for jobIndex, outcome := range tc.outcome {
for _, outcome := range tc.outcome {
task := runner.fetchTask(t)
runner.execTask(t, task, outcome)
@@ -183,9 +181,10 @@ jobs:
_, err := storage.Actions.Stat(logFileName)
assert.NoError(t, err)
_, job, run := getTaskAndJobAndRunByTaskID(t, task.Id)
// download task logs and check content
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, repo.Name, runIndex, jobIndex)).
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job.ID)).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
@@ -198,15 +197,8 @@ jobs:
)
}
runID, _ := strconv.ParseInt(task.Context.GetFields()["run_id"].GetStringValue(), 10, 64)
jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID)
assert.NoError(t, err)
assert.Len(t, jobs, len(tc.outcome))
jobID := jobs[jobIndex].ID
// download task logs from API and check content
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, jobID)).
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, job.ID)).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
@@ -33,7 +33,7 @@ func TestActionsRerun(t *testing.T) {
wfTreePath := ".gitea/workflows/actions-rerun-workflow-1.yml"
wfFileContent := `name: actions-rerun-workflow-1
on:
on:
push:
paths:
- '.gitea/workflows/actions-rerun-workflow-1.yml'
@@ -54,25 +54,22 @@ jobs:
// fetch and exec job1
job1Task := runner.fetchTask(t)
_, _, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
_, job1, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
runner.execTask(t, job1Task, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
})
// RERUN-FAILURE: the run is not done
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
session.MakeRequest(t, req, http.StatusBadRequest)
// fetch and exec job2
job2Task := runner.fetchTask(t)
_, job2, _ := getTaskAndJobAndRunByTaskID(t, job2Task.Id)
runner.execTask(t, job2Task, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
})
// RERUN-1: rerun the run
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
session.MakeRequest(t, req, http.StatusOK)
// fetch and exec job1
job1TaskR1 := runner.fetchTask(t)
@@ -86,9 +83,7 @@ jobs:
})
// RERUN-2: rerun job1
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 0), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job1.ID))
session.MakeRequest(t, req, http.StatusOK)
// job2 needs job1, so rerunning job1 will also rerun job2
// fetch and exec job1
@@ -103,9 +98,7 @@ jobs:
})
// RERUN-3: rerun job2
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 1), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job2.ID))
session.MakeRequest(t, req, http.StatusOK)
// only job2 will rerun
// fetch and exec job2
@@ -0,0 +1,300 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
actions_web "code.gitea.io/gitea/routers/web/repo/actions"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsRoute(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
t.Run("testActionsRouteForIDBasedURL", testActionsRouteForIDBasedURL)
t.Run("testActionsRouteForLegacyIndexBasedURL", testActionsRouteForLegacyIndexBasedURL)
})
}
func testActionsRouteForIDBasedURL(t *testing.T) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user2Session := loginUser(t, user2.Name)
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
repo1 := createActionsTestRepo(t, user2Token, "actions-route-id-url-1", false)
runner1 := newMockRunner()
runner1.registerAsRepoRunner(t, user2.Name, repo1.Name, "mock-runner", []string{"ubuntu-latest"}, false)
repo2 := createActionsTestRepo(t, user2Token, "actions-route-id-url-2", false)
runner2 := newMockRunner()
runner2.registerAsRepoRunner(t, user2.Name, repo2.Name, "mock-runner", []string{"ubuntu-latest"}, false)
workflowTreePath := ".gitea/workflows/test.yml"
workflowContent := `name: test
on:
push:
paths:
- '.gitea/workflows/test.yml'
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo job1
`
opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+workflowTreePath, workflowContent)
createWorkflowFile(t, user2Token, user2.Name, repo1.Name, workflowTreePath, opts)
createWorkflowFile(t, user2Token, user2.Name, repo2.Name, workflowTreePath, opts)
task1 := runner1.fetchTask(t)
_, job1, run1 := getTaskAndJobAndRunByTaskID(t, task1.Id)
task2 := runner2.fetchTask(t)
_, job2, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id)
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, run1.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, 999999))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// run1 and job1 belong to repo1, success
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run1.ID, job1.ID))
resp := user2Session.MakeRequest(t, req, http.StatusOK)
var viewResp actions_web.ViewResponse
DecodeJSON(t, resp, &viewResp)
assert.Len(t, viewResp.State.Run.Jobs, 1)
assert.Equal(t, job1.ID, viewResp.State.Run.Jobs[0].ID)
// run2 and job2 do not belong to repo1, failure
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run1.ID, job2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job1.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/workflow", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/approve", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/cancel", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/artifacts/test.txt", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "DELETE", fmt.Sprintf("/%s/%s/actions/runs/%d/artifacts/test.txt", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// make the tasks complete, then test rerun
runner1.execTask(t, task1, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
})
runner2.execTask(t, task2, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run2.ID, job2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run1.ID, job2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run2.ID, job1.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
}
func testActionsRouteForLegacyIndexBasedURL(t *testing.T) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user2Session := loginUser(t, user2.Name)
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
repo := createActionsTestRepo(t, user2Token, "actions-route-legacy-url", false)
mkRun := func(id, index int64, title, sha string) *actions_model.ActionRun {
return &actions_model.ActionRun{
ID: id,
Index: index,
RepoID: repo.ID,
OwnerID: user2.ID,
Title: title,
WorkflowID: "legacy-route.yml",
TriggerUserID: user2.ID,
Ref: "refs/heads/master",
CommitSHA: sha,
Status: actions_model.StatusWaiting,
}
}
mkJob := func(id, runID int64, name, sha string) *actions_model.ActionRunJob {
return &actions_model.ActionRunJob{
ID: id,
RunID: runID,
RepoID: repo.ID,
OwnerID: user2.ID,
CommitSHA: sha,
Name: name,
Status: actions_model.StatusWaiting,
}
}
// A small ID-based run/job pair that should always resolve directly.
smallIDRun := mkRun(80, 20, "legacy route small id", "aaa001")
smallIDJob := mkJob(170, smallIDRun.ID, "legacy-small-job", smallIDRun.CommitSHA)
// Another small run used to provide a job ID that belongs to a different run.
otherSmallRun := mkRun(90, 30, "legacy route other small", "aaa002")
otherSmallJob := mkJob(180, otherSmallRun.ID, "legacy-other-small-job", otherSmallRun.CommitSHA)
// A large-ID run whose legacy run index should redirect to its ID-based URL.
normalRun := mkRun(1500, 900, "legacy route normal", "aaa003")
normalRunJob := mkJob(1600, normalRun.ID, "legacy-normal-job", normalRun.CommitSHA)
// A run whose index collides with normalRun.ID to exercise summary-page ID-first behavior.
collisionRun := mkRun(2400, 1500, "legacy route collision", "aaa004")
collisionJobIdx0 := mkJob(2600, collisionRun.ID, "legacy-collision-job-1", collisionRun.CommitSHA)
collisionJobIdx1 := mkJob(2601, collisionRun.ID, "legacy-collision-job-2", collisionRun.CommitSHA)
// A run whose job has a smaller ID than the run itself (job_id < run_id)
jobSmallerThanRunRun := mkRun(5000, 5500, "legacy route job before run", "aaa007")
jobSmallerThanRunJob := mkJob(4500, jobSmallerThanRunRun.ID, "legacy-job-before-run-job", jobSmallerThanRunRun.CommitSHA)
// A small ID-based run/job pair that collides with a different legacy run/job index pair.
ambiguousIDRun := mkRun(3, 1, "legacy route ambiguous id", "aaa005")
ambiguousIDJob := mkJob(4, ambiguousIDRun.ID, "legacy-ambiguous-id-job", ambiguousIDRun.CommitSHA)
// The legacy run/job target for the ambiguous /runs/3/jobs/4 URL.
ambiguousLegacyRun := mkRun(1501, ambiguousIDRun.ID, "legacy route ambiguous legacy", "aaa006")
ambiguousLegacyJobIdx0 := mkJob(1601, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-0", ambiguousLegacyRun.CommitSHA)
ambiguousLegacyJobIdx1 := mkJob(1602, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-1", ambiguousLegacyRun.CommitSHA)
ambiguousLegacyJobIdx2 := mkJob(1603, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-2", ambiguousLegacyRun.CommitSHA)
ambiguousLegacyJobIdx3 := mkJob(1604, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-3", ambiguousLegacyRun.CommitSHA)
ambiguousLegacyJobIdx4 := mkJob(1605, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-4", ambiguousLegacyRun.CommitSHA) // job_index=4
ambiguousLegacyJobIdx5 := mkJob(1606, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-5", ambiguousLegacyRun.CommitSHA)
ambiguousLegacyJobs := []*actions_model.ActionRunJob{
ambiguousLegacyJobIdx0,
ambiguousLegacyJobIdx1,
ambiguousLegacyJobIdx2,
ambiguousLegacyJobIdx3,
ambiguousLegacyJobIdx4,
ambiguousLegacyJobIdx5,
}
targetAmbiguousLegacyJob := ambiguousLegacyJobs[int(ambiguousIDJob.ID)]
insertBeansWithExplicitIDs(t, "action_run",
smallIDRun, otherSmallRun, normalRun, ambiguousIDRun, ambiguousLegacyRun, collisionRun, jobSmallerThanRunRun,
)
insertBeansWithExplicitIDs(t, "action_run_job",
smallIDJob, otherSmallJob, normalRunJob, ambiguousIDJob, collisionJobIdx0, collisionJobIdx1,
ambiguousLegacyJobIdx0, ambiguousLegacyJobIdx1, ambiguousLegacyJobIdx2, ambiguousLegacyJobIdx3, ambiguousLegacyJobIdx4, ambiguousLegacyJobIdx5,
jobSmallerThanRunJob,
)
t.Run("OnlyRunID", func(t *testing.T) {
// ID-based URLs must be valid
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, smallIDRun.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, normalRun.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
})
t.Run("OnlyRunIndex", func(t *testing.T) {
// legacy run index should redirect to the ID-based URL
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, normalRun.Index))
resp := user2Session.MakeRequest(t, req, http.StatusFound)
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, normalRun.ID), resp.Header().Get("Location"))
// Best-effort compatibility prefers the run ID when the same number also exists as a legacy run index.
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, collisionRun.Index))
resp = user2Session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), fmt.Sprintf(`data-run-id="%d"`, normalRun.ID)) // because collisionRun.Index == normalRun.ID
// by_index=1 should force the summary page to use the legacy run index interpretation.
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d?by_index=1", user2.Name, repo.Name, collisionRun.Index))
resp = user2Session.MakeRequest(t, req, http.StatusFound)
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, collisionRun.ID), resp.Header().Get("Location"))
})
t.Run("RunIDAndJobID", func(t *testing.T) {
// ID-based URLs must be valid
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, smallIDRun.ID, smallIDJob.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, normalRun.ID, normalRunJob.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
// URL must resolve even when job_id < run_id.
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, jobSmallerThanRunRun.ID, jobSmallerThanRunJob.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
})
t.Run("RunIndexAndJobIndex", func(t *testing.T) {
// /user2/repo2/actions/runs/3/jobs/4 is ambiguous:
// - it may resolve as the ID-based URL for run_id=3/job_id=4,
// - or as the legacy index-based URL for run_index=3/job_index=4 which should redirect to run_id=1501/job_id=1605.
idBasedURL := fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, ambiguousIDRun.ID, ambiguousIDJob.ID)
indexBasedURL := fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, ambiguousLegacyRun.Index, 4) // for ambiguousLegacyJobIdx4
assert.Equal(t, idBasedURL, indexBasedURL)
// When both interpretations are valid, prefer the ID-based target by default.
req := NewRequest(t, "GET", indexBasedURL)
user2Session.MakeRequest(t, req, http.StatusOK)
redirectURL := fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, ambiguousLegacyRun.ID, targetAmbiguousLegacyJob.ID)
// by_index=1 should explicitly force the legacy run/job index interpretation.
req = NewRequest(t, "GET", indexBasedURL+"?by_index=1")
resp := user2Session.MakeRequest(t, req, http.StatusFound)
assert.Equal(t, redirectURL, resp.Header().Get("Location"))
// legacy job index 0 should redirect to the first job's ID
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/0", user2.Name, repo.Name, collisionRun.Index))
resp = user2Session.MakeRequest(t, req, http.StatusFound)
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, collisionRun.ID, collisionJobIdx0.ID), resp.Header().Get("Location"))
// legacy job index 1 should redirect to the second job's ID
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/1", user2.Name, repo.Name, collisionRun.Index))
resp = user2Session.MakeRequest(t, req, http.StatusFound)
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, collisionRun.ID, collisionJobIdx1.ID), resp.Header().Get("Location"))
})
t.Run("InvalidURLs", func(t *testing.T) {
// the job ID from a different run should not match
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, smallIDRun.ID, otherSmallJob.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// resolve the run by index first and then return not found because the job index is out-of-range
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/2", user2.Name, repo.Name, normalRun.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// an out-of-range job index should return not found
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/2", user2.Name, repo.Name, collisionRun.Index))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// a missing run number should return not found
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, 999999))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// a missing legacy run index should return not found
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/0", user2.Name, repo.Name, 999999))
user2Session.MakeRequest(t, req, http.StatusNotFound)
})
}
func insertBeansWithExplicitIDs(t *testing.T, table string, beans ...any) {
t.Helper()
ctx, committer, err := db.TxContext(t.Context())
require.NoError(t, err)
defer committer.Close()
if setting.Database.Type.IsMSSQL() {
_, err = db.Exec(ctx, fmt.Sprintf("SET IDENTITY_INSERT [%s] ON", table))
require.NoError(t, err)
defer func() {
_, err = db.Exec(ctx, fmt.Sprintf("SET IDENTITY_INSERT [%s] OFF", table))
require.NoError(t, err)
}()
}
require.NoError(t, db.Insert(ctx, beans...))
require.NoError(t, committer.Commit())
}
@@ -50,30 +50,46 @@ func TestActionsRunnerModify(t *testing.T) {
doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, description string, expectedStatus int) {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d", baseURL, id), map[string]string{
"_csrf": GetUserCSRFToken(t, sess),
"description": description,
})
sess.MakeRequest(t, req, expectedStatus)
}
doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
"_csrf": GetUserCSRFToken(t, sess),
})
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id))
sess.MakeRequest(t, req, expectedStatus)
}
doDisable := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/update-runner?disabled=true", baseURL, id))
sess.MakeRequest(t, req, expectedStatus)
}
doEnable := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/update-runner?disabled=false", baseURL, id))
sess.MakeRequest(t, req, expectedStatus)
}
assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusNotFound)
doDisable(t, sess, baseURL, id, http.StatusNotFound)
doEnable(t, sess, baseURL, id, http.StatusNotFound)
doDelete(t, sess, baseURL, id, http.StatusNotFound)
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.Empty(t, v.Description)
assert.False(t, v.IsDisabled)
}
assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusSeeOther)
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.Equal(t, "ChangedDescription", v.Description)
doDisable(t, sess, baseURL, id, http.StatusOK)
v = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.True(t, v.IsDisabled)
doEnable(t, sess, baseURL, id, http.StatusOK)
v = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.False(t, v.IsDisabled)
doDelete(t, sess, baseURL, id, http.StatusOK)
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: id})
}
@@ -19,6 +19,7 @@ import (
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -83,7 +84,7 @@ func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []strin
func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string, ephemeral bool) {
session := loginUser(t, ownerName)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token)
req := NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var registrationToken struct {
Token string `json:"token"`
@@ -94,13 +95,13 @@ func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, run
func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task {
task := r.tryFetchTask(t, timeout...)
assert.NotNil(t, task, "failed to fetch a task")
require.NotNil(t, task, "failed to fetch a task")
return task
}
func (r *mockRunner) fetchNoTask(t *testing.T, timeout ...time.Duration) {
task := r.tryFetchTask(t, timeout...)
assert.Nil(t, task, "a task is fetched")
require.Nil(t, task, "a task is fetched")
}
const defaultFetchTaskTimeout = 1 * time.Second
@@ -113,12 +114,8 @@ func (r *mockRunner) tryFetchTask(t *testing.T, timeout ...time.Duration) *runne
ddl := time.Now().Add(fetchTimeout)
var task *runnerv1.Task
for time.Now().Before(ddl) {
resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
TasksVersion: 0,
}))
assert.NoError(t, err)
if resp.Msg.Task != nil {
task = resp.Msg.Task
task, _ = r.fetchTaskOnce(t, 0)
if task != nil {
break
}
time.Sleep(200 * time.Millisecond)
@@ -127,6 +124,17 @@ func (r *mockRunner) tryFetchTask(t *testing.T, timeout ...time.Duration) *runne
return task
}
// fetchTaskOnce performs a single FetchTask request with the given TasksVersion
// and returns the task (if any) and the TasksVersion from the response.
// Used to verify the production path where the runner sends the current version.
func (r *mockRunner) fetchTaskOnce(t *testing.T, tasksVersion int64) (*runnerv1.Task, int64) {
resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
TasksVersion: tasksVersion,
}))
require.NoError(t, err)
return resp.Msg.Task, resp.Msg.TasksVersion
}
type mockTaskOutcome struct {
result runnerv1.Result
outputs map[string]string
@@ -19,7 +19,6 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/migration"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
mirror_service "code.gitea.io/gitea/services/mirror"
repo_service "code.gitea.io/gitea/services/repository"
files_service "code.gitea.io/gitea/services/repository/files"
@@ -89,7 +88,9 @@ jobs:
assert.NoError(t, err)
// merge pull request
testPullMerge(t, testContext.Session, repo.OwnerName, repo.Name, strconv.FormatInt(apiPull.Index, 10), mergeStyle, false)
testPullMerge(t, testContext.Session, repo.OwnerName, repo.Name, strconv.FormatInt(apiPull.Index, 10), MergeOptions{
Style: mergeStyle,
})
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
return pull.MergedCommitID, "@every 2m"
@@ -101,8 +102,8 @@ jobs:
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
// enable manual-merge
doAPIEditRepository(testContext, &api.EditRepoOption{
HasPullRequests: util.ToPointer(true),
AllowManualMerge: util.ToPointer(true),
HasPullRequests: new(true),
AllowManualMerge: new(true),
})(t)
// update workflow file
@@ -168,7 +169,7 @@ func testScheduleUpdateMirrorSync(t *testing.T) {
// enable actions unit for mirror repo
assert.False(t, mirrorRepo.UnitEnabled(t.Context(), unit_model.TypeActions))
doAPIEditRepository(mirrorContext, &api.EditRepoOption{
HasActions: util.ToPointer(true),
HasActions: new(true),
})(t)
actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID})
scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID})
@@ -199,11 +200,11 @@ func testScheduleUpdateMirrorSync(t *testing.T) {
func testScheduleUpdateArchiveAndUnarchive(t *testing.T) {
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
doAPIEditRepository(testContext, &api.EditRepoOption{
Archived: util.ToPointer(true),
Archived: new(true),
})(t)
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
doAPIEditRepository(testContext, &api.EditRepoOption{
Archived: util.ToPointer(false),
Archived: new(false),
})(t)
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
assert.NoError(t, err)
@@ -214,11 +215,11 @@ func testScheduleUpdateArchiveAndUnarchive(t *testing.T) {
func testScheduleUpdateDisableAndEnableActionsUnit(t *testing.T) {
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
doAPIEditRepository(testContext, &api.EditRepoOption{
HasActions: util.ToPointer(false),
HasActions: new(false),
})(t)
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
doAPIEditRepository(testContext, &api.EditRepoOption{
HasActions: util.ToPointer(true),
HasActions: new(true),
})(t)
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
assert.NoError(t, err)
@@ -0,0 +1,81 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestActionsCollaborativeOwner(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
// user2 is the owner of the private "reusable_workflow" repo
user2Session := loginUser(t, "user2")
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
apiReusableWorkflowRepo := createActionsTestRepo(t, user2Token, "reusable_workflow", true)
reusableWorkflowRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiReusableWorkflowRepo.ID})
// user4 is the owner of the private caller repo
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
user4Session := loginUser(t, user4.Name)
user4Token := getTokenForLoggedInUser(t, user4Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
apiCallerRepo := createActionsTestRepo(t, user4Token, "caller_workflow", true)
callerRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiCallerRepo.ID})
// create a mock runner for caller
runner := newMockRunner()
runner.registerAsRepoRunner(t, callerRepo.OwnerName, callerRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
// init the workflow for caller
wfTreePath := ".gitea/workflows/test_collaborative_owner.yml"
wfFileContent := `name: Test Collaborative Owner
on: push
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo 'test collaborative owner'
`
opts := getWorkflowCreateFileOptions(user4, callerRepo.DefaultBranch, "create "+wfTreePath, wfFileContent)
createWorkflowFile(t, user4Token, callerRepo.OwnerName, callerRepo.Name, wfTreePath, opts)
// fetch the task and get its token
task := runner.fetchTask(t)
taskToken := task.Secrets["GITEA_TOKEN"]
assert.NotEmpty(t, taskToken)
// prepare for clone
dstPath := t.TempDir()
u.Path = fmt.Sprintf("%s/%s.git", "user2", "reusable_workflow")
u.User = url.UserPassword("gitea-actions", taskToken)
// the git clone will fail
doGitCloneFail(u)(t)
// add user10 to the list of collaborative owners
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name), map[string]string{
"collaborative_owner": user4.Name,
})
user2Session.MakeRequest(t, req, http.StatusOK)
// the git clone will be successful
doGitClone(dstPath, u)(t)
// remove user10 from the list of collaborative owners
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/delete?id=%d", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name, user4.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
// the git clone will fail
doGitCloneFail(u)(t)
})
}
@@ -23,14 +23,12 @@ import (
user_model "code.gitea.io/gitea/models/user"
actions_module "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/commitstatus"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
@@ -40,6 +38,7 @@ import (
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPullRequestTargetEvent(t *testing.T) {
@@ -413,7 +412,7 @@ jobs:
assert.NoError(t, err)
// create a branch
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, gitRepo, branch.CommitID, "test-create-branch")
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-create-branch")
assert.NoError(t, err)
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
Title: "add workflow",
@@ -439,7 +438,7 @@ jobs:
assert.NotNil(t, run)
// delete the branch
err = repo_service.DeleteBranch(t.Context(), user2, repo, gitRepo, "test-create-branch", nil)
err = repo_service.DeleteBranch(t.Context(), user2, repo, gitRepo, "test-create-branch")
assert.NoError(t, err)
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
Title: "add workflow",
@@ -531,9 +530,7 @@ jobs:
// create a new branch
testBranch := "test-branch"
gitRepo, err := git.OpenRepository(t.Context(), ".")
assert.NoError(t, err)
err = repo_service.CreateNewBranch(t.Context(), user2, repo, gitRepo, "main", testBranch)
err = repo_service.CreateNewBranch(t.Context(), user2, repo, "main", testBranch)
assert.NoError(t, err)
// create Pull
@@ -695,6 +692,144 @@ func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL,
assert.NoError(t, err)
}
func TestPullRequestReviewCommitStatusEvent(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // reviewer
// create a repo
repo, err := repo_service.CreateRepository(t.Context(), user2, user2, repo_service.CreateRepoOptions{
Name: "repo-pull-request-review",
Description: "test pull-request-review commit status",
AutoInit: true,
Gitignores: "Go",
License: "MIT",
Readme: "Default",
DefaultBranch: "main",
IsPrivate: false,
})
assert.NoError(t, err)
assert.NotEmpty(t, repo)
// add user4 as collaborator so they can review
ctx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository)
t.Run("AddUser4AsCollaboratorWithWriteAccess", doAPIAddCollaborator(ctx, "user4", perm.AccessModeWrite))
// add workflow file that triggers on pull_request_review
addWorkflow, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: ".gitea/workflows/pr-review.yml",
ContentReader: strings.NewReader(`name: test
on:
pull_request_review:
types: [submitted]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo helloworld
`),
},
},
Message: "add workflow",
OldBranch: "main",
NewBranch: "main",
Author: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
})
assert.NoError(t, err)
assert.NotEmpty(t, addWorkflow)
// create a branch and a PR
testBranch := "test-review-branch"
err = repo_service.CreateNewBranch(t.Context(), user2, repo, "main", testBranch)
assert.NoError(t, err)
// add a file on the test branch so the PR has changes
addFileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "test.txt",
ContentReader: strings.NewReader("test content"),
},
},
Message: "add test file",
OldBranch: testBranch,
NewBranch: testBranch,
Author: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
})
assert.NoError(t, err)
assert.NotEmpty(t, addFileResp)
sha := addFileResp.Commit.SHA
pullIssue := &issues_model.Issue{
RepoID: repo.ID,
Title: "A test PR for review",
PosterID: user2.ID,
Poster: user2,
IsPull: true,
}
pullRequest := &issues_model.PullRequest{
HeadRepoID: repo.ID,
BaseRepoID: repo.ID,
HeadBranch: testBranch,
BaseBranch: "main",
HeadRepo: repo,
BaseRepo: repo,
Type: issues_model.PullRequestGitea,
}
prOpts := &pull_service.NewPullRequestOptions{Repo: repo, Issue: pullIssue, PullRequest: pullRequest}
err = pull_service.NewPullRequest(t.Context(), prOpts)
assert.NoError(t, err)
// submit an approval review as user4
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
assert.NoError(t, err)
defer gitRepo.Close()
_, _, err = pull_service.SubmitReview(t.Context(), user4, gitRepo, pullIssue, issues_model.ReviewTypeApprove, "lgtm", sha, nil)
assert.NoError(t, err)
// verify that a commit status was created for the review event
assert.Eventually(t, func() bool {
latestCommitStatuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, sha, db.ListOptionsAll)
assert.NoError(t, err)
if len(latestCommitStatuses) == 0 {
return false
}
if latestCommitStatuses[0].State == commitstatus.CommitStatusPending {
insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context)
return true
}
return false
}, 1*time.Second, 100*time.Millisecond)
})
}
func TestWorkflowDispatchPublicApi(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -772,6 +907,27 @@ jobs:
CommitSHA: branch.CommitID,
})
assert.NotNil(t, run)
// Now trigger with rundetails
values.Set("return_run_details", "true")
req = NewRequestWithURLValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), values).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
runDetails := &api.RunDetails{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(runDetails))
assert.NotEqual(t, 0, runDetails.WorkflowRunID)
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
ID: runDetails.WorkflowRunID,
Title: "add workflow",
RepoID: repo.ID,
Event: "workflow_dispatch",
Ref: "refs/heads/main",
WorkflowID: "dispatch.yml",
CommitSHA: branch.CommitID,
})
assert.NotNil(t, run)
})
}
@@ -1409,7 +1565,7 @@ jobs:
// user4 forks the repo
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
&api.CreateForkOption{
Name: util.ToPointer("close-pull-request-with-path-fork"),
Name: new("close-pull-request-with-path-fork"),
}).AddTokenAuth(user4Token)
resp := MakeRequest(t, req, http.StatusAccepted)
var apiForkRepo api.Repository
@@ -1508,14 +1664,11 @@ jobs:
assert.NotEmpty(t, addWorkflowToBaseResp)
// Get the commit ID of the default branch
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
assert.NoError(t, err)
defer gitRepo.Close()
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
assert.NoError(t, err)
// create a branch
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, gitRepo, branch.CommitID, "test-action-run-name-with-variables")
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-action-run-name-with-variables")
assert.NoError(t, err)
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
Title: user2.LoginName + " is running this workflow",
@@ -1585,14 +1738,11 @@ jobs:
assert.NotEmpty(t, addWorkflowToBaseResp)
// Get the commit ID of the default branch
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
assert.NoError(t, err)
defer gitRepo.Close()
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
assert.NoError(t, err)
// create a branch
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, gitRepo, branch.CommitID, "test-action-run-name")
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-action-run-name")
assert.NoError(t, err)
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
Title: "run name without variables",
@@ -1623,7 +1773,7 @@ func TestPullRequestWithPathsRebase(t *testing.T) {
testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", "dir1/dir1.txt", "1")
testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", "dir2/dir2.txt", "2")
wfFileContent := `name: ci
on:
on:
pull_request:
paths:
- 'dir1/**'
@@ -1648,12 +1798,10 @@ jobs:
apiPull, err := doAPICreatePullRequest(apiCtx, "user2", repoName, repo.DefaultBranch, "update-dir2")(t)
runner.fetchNoTask(t)
assert.NoError(t, err)
testEditFile(t, session, "user2", repoName, repo.DefaultBranch, "dir1/dir1.txt", "11") // change the file in "dir1"
req := NewRequestWithValues(t, "POST",
fmt.Sprintf("/%s/%s/pulls/%d/update?style=rebase", "user2", repoName, apiPull.Index), // update by rebase
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
// change the file in "dir1"
testEditFile(t, session, "user2", repoName, repo.DefaultBranch, "dir1/dir1.txt", "11")
// update by rebase
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/pulls/%d/update?style=rebase", "user2", repoName, apiPull.Index))
session.MakeRequest(t, req, http.StatusSeeOther)
runner.fetchNoTask(t)
})
@@ -50,17 +50,14 @@ func TestActionsVariables(t *testing.T) {
doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, data string, expectedStatus int) {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", baseURL, id), map[string]string{
"_csrf": GetUserCSRFToken(t, sess),
"name": "VAR",
"data": data,
"name": "VAR",
"data": data,
})
sess.MakeRequest(t, req, expectedStatus)
}
doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
"_csrf": GetUserCSRFToken(t, sess),
})
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id))
sess.MakeRequest(t, req, expectedStatus)
}
@@ -7,10 +7,14 @@ import (
"net/http"
"testing"
"code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminConfig(t *testing.T) {
@@ -20,4 +24,46 @@ func TestAdminConfig(t *testing.T) {
req := NewRequest(t, "GET", "/-/admin/config")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
t.Run("OpenEditorWithApps", func(t *testing.T) {
cfg := setting.Config().Repository.OpenWithEditorApps
editorApps := cfg.Value(t.Context())
assert.Len(t, editorApps, 3)
assert.False(t, cfg.HasValue(t.Context()))
require.NoError(t, system.SetSettings(t.Context(), map[string]string{cfg.DynKey(): "[]"}))
config.GetDynGetter().InvalidateCache()
editorApps = cfg.Value(t.Context())
assert.Len(t, editorApps, 3)
assert.False(t, cfg.HasValue(t.Context()))
require.NoError(t, system.SetSettings(t.Context(), map[string]string{cfg.DynKey(): "[{}]"}))
config.GetDynGetter().InvalidateCache()
editorApps = cfg.Value(t.Context())
assert.Len(t, editorApps, 1)
assert.True(t, cfg.HasValue(t.Context()))
})
t.Run("InstanceWebBanner", func(t *testing.T) {
banner, rev1, has := setting.Config().Instance.WebBanner.ValueRevision(t.Context())
assert.False(t, has)
assert.Equal(t, setting.WebBannerType{}, banner)
req = NewRequestWithValues(t, "POST", "/-/admin/config", map[string]string{
"key": "instance.web_banner",
"value": `{"DisplayEnabled":true,"ContentMessage":"test-msg","StartTimeUnix":123,"EndTimeUnix":456}`,
})
session.MakeRequest(t, req, http.StatusOK)
banner, rev2, has := setting.Config().Instance.WebBanner.ValueRevision(t.Context())
assert.NotEqual(t, rev1, rev2)
assert.True(t, has)
assert.Equal(t, setting.WebBannerType{
DisplayEnabled: true,
ContentMessage: "test-msg",
StartTimeUnix: 123,
EndTimeUnix: 456,
}, banner)
})
}
@@ -43,18 +43,16 @@ func TestAdminViewUser(t *testing.T) {
func TestAdminEditUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
testSuccessfullEdit(t, user_model.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "new@e-mail.gitea"})
testSuccessfulEdit(t, user_model.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "new@e-mail.gitea"})
}
func testSuccessfullEdit(t *testing.T, formData user_model.User) {
func testSuccessfulEdit(t *testing.T, formData user_model.User) {
makeRequest(t, formData, http.StatusSeeOther)
}
func makeRequest(t *testing.T, formData user_model.User, headerCode int) {
session := loginUser(t, "user1")
csrf := GetUserCSRFToken(t, session)
req := NewRequestWithValues(t, "POST", "/-/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit", map[string]string{
"_csrf": csrf,
"user_name": formData.Name,
"login_name": formData.LoginName,
"login_type": "0-0",
@@ -96,10 +94,7 @@ func TestAdminDeleteUser(t *testing.T) {
query = "?purge=true"
}
csrf := GetUserCSRFToken(t, session)
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/-/admin/users/%d/delete%s", entry.userID, query), map[string]string{
"_csrf": csrf,
})
req := NewRequest(t, "POST", fmt.Sprintf("/-/admin/users/%d/delete%s", entry.userID, query))
session.MakeRequest(t, req, http.StatusSeeOther)
assertUserDeleted(t, entry.userID)
@@ -6,26 +6,33 @@ package integration
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/xml"
"fmt"
"io"
"mime"
"net/http"
"strings"
"testing"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/actions"
actions_service "code.gitea.io/gitea/services/actions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -45,45 +52,168 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
assert.NoError(t, err)
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
Version: 4,
Name: "artifact",
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
assert.True(t, uploadResp.Ok)
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
table := []struct {
name string
version int32
contentType string
blockID bool
noLength bool
append int
path string
}{
{
name: "artifact",
version: 4,
path: "artifact.zip",
},
{
name: "artifact2",
version: 4,
blockID: true,
},
{
name: "artifact3",
version: 4,
noLength: true,
},
{
name: "artifact4",
version: 4,
blockID: true,
noLength: true,
},
{
name: "artifact5",
version: 7,
blockID: true,
},
{
name: "artifact6",
version: 7,
append: 2,
noLength: true,
},
{
name: "artifact7",
version: 7,
append: 3,
blockID: true,
noLength: true,
},
{
name: "artifact8",
version: 7,
append: 4,
blockID: true,
},
{
name: "artifact9.json",
version: 7,
contentType: "application/json",
},
{
name: "artifact10",
version: 7,
contentType: "application/zip",
path: "artifact10.zip",
},
{
name: "artifact11.zip",
version: 7,
contentType: "application/zip",
path: "artifact11.zip",
},
}
// get upload url
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
for _, entry := range table {
t.Run(entry.name, func(t *testing.T) {
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
Version: entry.version,
Name: entry.name,
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil),
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
assert.True(t, uploadResp.Ok)
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// upload artifact chunk
body := strings.Repeat("A", 1024)
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
MakeRequest(t, req, http.StatusCreated)
h := sha256.New()
t.Logf("Create artifact confirm")
blocks := make([]string, 0, util.Iif(entry.blockID, entry.append+1, 0))
sha := sha256.Sum256([]byte(body))
// get upload url
for i := range entry.append + 1 {
url := uploadResp.SignedUploadUrl
// See https://learn.microsoft.com/en-us/rest/api/storageservices/append-block
// See https://learn.microsoft.com/en-us/rest/api/storageservices/put-block
if entry.blockID {
blockID := base64.RawURLEncoding.EncodeToString(fmt.Append([]byte("SOME_BIG_BLOCK_ID_"), i))
blocks = append(blocks, blockID)
url += "&comp=block&blockid=" + blockID
} else {
url += "&comp=appendBlock"
}
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
Name: "artifact",
Size: 1024,
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
// upload artifact chunk
body := strings.Repeat("A", 1024)
_, _ = h.Write([]byte(body))
var bodyReader io.Reader = strings.NewReader(body)
if entry.noLength {
bodyReader = io.MultiReader(bodyReader)
}
req = NewRequestWithBody(t, "PUT", url, bodyReader)
MakeRequest(t, req, http.StatusCreated)
}
if entry.blockID && entry.append > 0 {
// https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
// upload artifact blockList
blockList := &actions.BlockList{
Latest: blocks,
}
rawBlockList, err := xml.Marshal(blockList)
assert.NoError(t, err)
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
MakeRequest(t, req, http.StatusCreated)
}
sha := h.Sum(nil)
t.Logf("Create artifact confirm")
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
Name: entry.name,
Size: int64(entry.append+1) * 1024,
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha)),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId})
if entry.contentType != "" {
assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType)
} else {
assert.Equal(t, "application/zip", artifact.ContentEncodingOrType)
}
if entry.path != "" {
assert.Equal(t, entry.path, artifact.ArtifactPath)
}
assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status)
assert.Equal(t, int64(entry.append+1)*1024, artifact.FileSize)
assert.Equal(t, int64(entry.append+1)*1024, artifact.FileCompressedSize)
})
}
}
func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) {
@@ -106,8 +236,7 @@ func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) {
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload url
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
url := uploadResp.SignedUploadUrl + "&comp=block"
// upload artifact chunk
body := strings.Repeat("B", 1024)
@@ -151,8 +280,7 @@ func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) {
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload url
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
url := uploadResp.SignedUploadUrl + "&comp=block"
// upload artifact chunk
body := strings.Repeat("A", 1024)
@@ -198,9 +326,8 @@ func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload urls
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=%2f..%2fmyfile"
blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist"
url := uploadResp.SignedUploadUrl + "&comp=block&blockid=%2f..%2fmyfile"
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
// upload artifact chunk
body := strings.Repeat("A", 1024)
@@ -247,63 +374,126 @@ func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) {
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
assert.NoError(t, err)
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
Version: 4,
Name: "artifactWithChunksOutOfOrder",
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
assert.True(t, uploadResp.Ok)
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload urls
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
block1URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block1"
block2URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block2"
blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist"
// upload artifact chunks
bodyb := strings.Repeat("B", 1024)
req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb))
MakeRequest(t, req, http.StatusCreated)
bodya := strings.Repeat("A", 1024)
req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya))
MakeRequest(t, req, http.StatusCreated)
// upload artifact blockList
blockList := &actions.BlockList{
Latest: []string{
"block1",
"block2",
},
table := []struct {
name string
artifactName string
serveDirect bool
contentType string
}{
{name: "Upload-Zip", artifactName: "artifact-v4-upload", contentType: ""},
{name: "Upload-Pdf", artifactName: "report-upload.pdf", contentType: "application/pdf"},
{name: "Upload-Html", artifactName: "report-upload.html", contentType: "application/html"},
{name: "ServeDirect-Zip", artifactName: "artifact-v4-upload-serve-direct", contentType: "", serveDirect: true},
{name: "ServeDirect-Pdf", artifactName: "report-upload-serve-direct.pdf", contentType: "application/pdf", serveDirect: true},
{name: "ServeDirect-Html", artifactName: "report-upload-serve-direct.html", contentType: "application/html", serveDirect: true},
}
rawBlockList, err := xml.Marshal(blockList)
assert.NoError(t, err)
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
MakeRequest(t, req, http.StatusCreated)
t.Logf("Create artifact confirm")
for _, entry := range table {
t.Run(entry.name, func(t *testing.T) {
// Only AzureBlobStorageType supports ServeDirect Uploads
switch setting.Actions.ArtifactStorage.Type {
case setting.AzureBlobStorageType:
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.serveDirect)()
default:
if entry.serveDirect {
t.Skip()
}
}
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
Version: util.Iif[int32](entry.contentType != "", 7, 4),
Name: entry.artifactName,
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil),
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
assert.True(t, uploadResp.Ok)
if !entry.serveDirect {
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
}
sha := sha256.Sum256([]byte(bodya + bodyb))
// get upload urls
block1URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block1"))
block2URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block2"))
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
Name: "artifactWithChunksOutOfOrder",
Size: 2048,
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
// upload artifact chunks
bodyb := strings.Repeat("B", 1024)
req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb))
if entry.serveDirect {
req.Request.RequestURI = ""
nresp, err := http.DefaultClient.Do(req.Request)
require.NoError(t, err)
nresp.Body.Close()
require.Equal(t, http.StatusCreated, nresp.StatusCode)
} else {
MakeRequest(t, req, http.StatusCreated)
}
bodya := strings.Repeat("A", 1024)
req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya))
if entry.serveDirect {
req.Request.RequestURI = ""
nresp, err := http.DefaultClient.Do(req.Request)
require.NoError(t, err)
nresp.Body.Close()
require.Equal(t, http.StatusCreated, nresp.StatusCode)
} else {
MakeRequest(t, req, http.StatusCreated)
}
// upload artifact blockList
blockList := &actions.BlockList{
Latest: []string{
base64.RawURLEncoding.EncodeToString([]byte("block1")),
base64.RawURLEncoding.EncodeToString([]byte("block2")),
},
}
rawBlockList, err := xml.Marshal(blockList)
assert.NoError(t, err)
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
if entry.serveDirect {
req.Request.RequestURI = ""
nresp, err := http.DefaultClient.Do(req.Request)
require.NoError(t, err)
nresp.Body.Close()
require.Equal(t, http.StatusCreated, nresp.StatusCode)
} else {
MakeRequest(t, req, http.StatusCreated)
}
t.Logf("Create artifact confirm")
sha := sha256.Sum256([]byte(bodya + bodyb))
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
Name: entry.artifactName,
Size: 2048,
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId})
if entry.contentType != "" {
assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType)
} else {
assert.Equal(t, "application/zip", artifact.ContentEncodingOrType)
}
assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status)
assert.Equal(t, int64(2048), artifact.FileSize)
assert.Equal(t, int64(2048), artifact.FileCompressedSize)
})
}
}
func TestActionsArtifactV4DownloadSingle(t *testing.T) {
@@ -312,33 +502,97 @@ func TestActionsArtifactV4DownloadSingle(t *testing.T) {
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
assert.NoError(t, err)
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
NameFilter: wrapperspb.String("artifact-v4-download"),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var listResp actions.ListArtifactsResponse
protojson.Unmarshal(resp.Body.Bytes(), &listResp)
assert.Len(t, listResp.Artifacts, 1)
table := []struct {
Name string
ArtifactName string
FileName string
ServeDirect bool
ContentType string
ContentDisposition string
}{
{Name: "Download-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip"},
{Name: "Download-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf"},
{Name: "Download-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html"},
{Name: "ServeDirect-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip", ServeDirect: true},
{Name: "ServeDirect-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf", ServeDirect: true},
{Name: "ServeDirect-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html", ServeDirect: true},
}
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
Name: "artifact-v4-download",
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.GetSignedArtifactURLResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.NotEmpty(t, finalizeResp.SignedUrl)
for _, entry := range table {
t.Run(entry.Name, func(t *testing.T) {
switch setting.Actions.ArtifactStorage.Type {
case setting.AzureBlobStorageType:
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.ServeDirect)()
case setting.MinioStorageType:
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.MinioConfig.ServeDirect, entry.ServeDirect)()
default:
if entry.ServeDirect {
t.Skip()
}
}
req = NewRequest(t, "GET", finalizeResp.SignedUrl)
resp = MakeRequest(t, req, http.StatusOK)
body := strings.Repeat("D", 1024)
assert.Equal(t, body, resp.Body.String())
// list artifacts by name
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
NameFilter: wrapperspb.String(entry.ArtifactName),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var listResp actions.ListArtifactsResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp))
require.Len(t, listResp.Artifacts, 1)
// list artifacts by id
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
IdFilter: wrapperspb.Int64(listResp.Artifacts[0].DatabaseId),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp))
assert.Len(t, listResp.Artifacts, 1)
// acquire artifact download url
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
Name: entry.ArtifactName,
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.GetSignedArtifactURLResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp))
assert.NotEmpty(t, finalizeResp.SignedUrl)
body := strings.Repeat("D", 1024)
var contentDisposition string
if entry.ServeDirect {
externalReq, err := http.NewRequestWithContext(t.Context(), http.MethodGet, finalizeResp.SignedUrl, nil)
require.NoError(t, err)
externalResp, err := http.DefaultClient.Do(externalReq)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, externalResp.StatusCode)
assert.Equal(t, entry.ContentType, externalResp.Header.Get("Content-Type"))
contentDisposition = externalResp.Header.Get("Content-Disposition")
buf := make([]byte, 1024)
n, err := io.ReadAtLeast(externalResp.Body, buf, len(buf))
externalResp.Body.Close()
require.NoError(t, err)
assert.Equal(t, len(buf), n)
assert.Equal(t, body, string(buf))
} else {
req = NewRequest(t, "GET", finalizeResp.SignedUrl)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, entry.ContentType, resp.Header().Get("Content-Type"))
contentDisposition = resp.Header().Get("Content-Disposition")
assert.Equal(t, body, resp.Body.String())
}
disposition, param, err := mime.ParseMediaType(contentDisposition)
require.NoError(t, err)
assert.Equal(t, "inline", disposition)
assert.Equal(t, entry.FileName, param["filename"])
})
}
}
func TestActionsArtifactV4RunDownloadSinglePublicApi(t *testing.T) {
@@ -469,7 +723,7 @@ func TestActionsArtifactV4ListAndGetPublicApi(t *testing.T) {
for _, artifact := range listResp.Entries {
assert.Contains(t, artifact.URL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), artifact.ID))
assert.Contains(t, artifact.ArchiveDownloadURL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), artifact.ID))
req = NewRequestWithBody(t, "GET", listResp.Entries[0].URL, nil).
req = NewRequestWithBody(t, "GET", artifact.URL, nil).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
@@ -0,0 +1,54 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func testActionUserSignIn(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/user").
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp := MakeRequest(t, req, http.StatusOK)
var u api.User
DecodeJSON(t, resp, &u)
assert.Equal(t, "gitea-actions", u.UserName)
}
func testActionUserAccessPublicRepo(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/raw/README.md").
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "file", resp.Header().Get("x-gitea-object-type"))
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/raw/README.md").
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "file", resp.Header().Get("x-gitea-object-type"))
}
func testActionUserNoAccessOtherPrivateRepo(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo2/raw/README.md").
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
MakeRequest(t, req, http.StatusNotFound)
}
func TestActionUserAccessPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("ActionUserSignIn", testActionUserSignIn)
t.Run("ActionUserAccessPublicRepo", testActionUserAccessPublicRepo)
t.Run("ActionUserNoAccessOtherPrivateRepo", testActionUserNoAccessOtherPrivateRepo)
}
@@ -6,16 +6,21 @@ package integration
import (
"fmt"
"net/http"
"slices"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIActionsGetWorkflowRun(t *testing.T) {
@@ -26,15 +31,45 @@ func TestAPIActionsGetWorkflowRun(t *testing.T) {
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802802", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/803", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
t.Run("GetRun", func(t *testing.T) {
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802802", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/803", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
})
t.Run("GetJobSteps", func(t *testing.T) {
// Insert task steps for task_id 53 (job 198) so the API can return them once the backend loads them
_, err := db.GetEngine(t.Context()).Insert(&actions_model.ActionTaskStep{
Name: "main",
TaskID: 53,
Index: 0,
RepoID: repo.ID,
Status: actions_model.StatusSuccess,
Started: timeutil.TimeStamp(1683636528),
Stopped: timeutil.TimeStamp(1683636626),
})
require.NoError(t, err)
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs", repo.FullName())).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var jobList api.ActionWorkflowJobsResponse
err = json.Unmarshal(resp.Body.Bytes(), &jobList)
require.NoError(t, err)
job198Idx := slices.IndexFunc(jobList.Entries, func(job *api.ActionWorkflowJob) bool { return job.ID == 198 })
require.NotEqual(t, -1, job198Idx, "expected to find job 198 in run 795 jobs list")
job198 := jobList.Entries[job198Idx]
require.NotEmpty(t, job198.Steps, "job must return at least one step when task has steps")
assert.Equal(t, "main", job198.Steps[0].Name, "first step name")
})
}
func TestAPIActionsGetWorkflowJob(t *testing.T) {
@@ -134,3 +169,126 @@ func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository,
assert.Equal(t, expected, findTask1)
assert.Equal(t, expected, findTask2)
}
func TestAPIActionsRerunWorkflowRun(t *testing.T) {
defer prepareTestEnvActionsArtifacts(t)()
t.Run("NotDone", func(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/rerun", repo.FullName())).
AddTokenAuth(writeToken)
MakeRequest(t, req, http.StatusBadRequest)
})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
t.Run("Success", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
AddTokenAuth(writeToken)
resp := MakeRequest(t, req, http.StatusCreated)
var rerunResp api.ActionWorkflowRun
err := json.Unmarshal(resp.Body.Bytes(), &rerunResp)
require.NoError(t, err)
assert.Equal(t, int64(795), rerunResp.ID)
assert.Equal(t, "queued", rerunResp.Status)
assert.Equal(t, "c2d72f548424103f01ee1dc02889c1e2bff816b0", rerunResp.HeadSha)
run, err := actions_model.GetRunByRepoAndID(t.Context(), repo.ID, 795)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusWaiting, run.Status)
assert.Equal(t, timeutil.TimeStamp(0), run.Started)
assert.Equal(t, timeutil.TimeStamp(0), run.Stopped)
job198, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 198)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusWaiting, job198.Status)
assert.Equal(t, int64(0), job198.TaskID)
job199, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 199)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
assert.Equal(t, int64(0), job199.TaskID)
})
t.Run("ForbiddenWithoutWriteScope", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
AddTokenAuth(readToken)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("NotFound", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/rerun", repo.FullName())).
AddTokenAuth(writeToken)
MakeRequest(t, req, http.StatusNotFound)
})
}
func TestAPIActionsRerunWorkflowJob(t *testing.T) {
defer prepareTestEnvActionsArtifacts(t)()
t.Run("NotDone", func(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/jobs/194/rerun", repo.FullName())).
AddTokenAuth(writeToken)
MakeRequest(t, req, http.StatusBadRequest)
})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
t.Run("Success", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/199/rerun", repo.FullName())).
AddTokenAuth(writeToken)
resp := MakeRequest(t, req, http.StatusCreated)
var rerunResp api.ActionWorkflowJob
err := json.Unmarshal(resp.Body.Bytes(), &rerunResp)
require.NoError(t, err)
assert.Equal(t, int64(199), rerunResp.ID)
assert.Equal(t, "queued", rerunResp.Status)
run, err := actions_model.GetRunByRepoAndID(t.Context(), repo.ID, 795)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusWaiting, run.Status)
job198, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 198)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusSuccess, job198.Status)
assert.Equal(t, int64(53), job198.TaskID)
job199, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 199)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
assert.Equal(t, int64(0), job199.TaskID)
})
t.Run("ForbiddenWithoutWriteScope", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/199/rerun", repo.FullName())).
AddTokenAuth(readToken)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("NotFoundJob", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/999999/rerun", repo.FullName())).
AddTokenAuth(writeToken)
MakeRequest(t, req, http.StatusNotFound)
})
}
@@ -24,6 +24,12 @@ func TestAPIActionsRunner(t *testing.T) {
t.Run("RepoRunner", testActionsRunnerRepo)
}
func newRunnerUpdateRequest(t *testing.T, url string, disabled bool) *RequestWrapper {
return NewRequestWithJSON(t, http.MethodPatch, url, api.EditActionRunnerOption{
Disabled: &disabled,
})
}
func testActionsRunnerAdmin(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
@@ -45,22 +51,39 @@ func testActionsRunnerAdmin(t *testing.T) {
require.NotEqual(t, -1, idx)
expectedRunner := runnerList.Entries[idx]
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Name)
assert.False(t, expectedRunner.Disabled)
assert.False(t, expectedRunner.Ephemeral)
assert.Len(t, expectedRunner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
assert.Equal(t, "linux", expectedRunner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
disabledRunner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &disabledRunner)
assert.True(t, disabledRunner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify all returned runners can be requested and deleted
for _, runnerEntry := range runnerList.Entries {
// Verify get the runner by id
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
runnerResp = MakeRequest(t, req, http.StatusOK)
runner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &runner)
assert.Equal(t, runnerEntry.Name, runner.Name)
assert.Equal(t, runnerEntry.ID, runner.ID)
assert.Equal(t, runnerEntry.Disabled, runner.Disabled)
assert.Equal(t, runnerEntry.Ephemeral, runner.Ephemeral)
assert.ElementsMatch(t, runnerEntry.Labels, runner.Labels)
@@ -93,6 +116,7 @@ func testActionsRunnerUser(t *testing.T) {
assert.Len(t, runnerList.Entries, 1)
assert.Equal(t, "runner_to_be_deleted-user", runnerList.Entries[0].Name)
assert.Equal(t, int64(34346), runnerList.Entries[0].ID)
assert.False(t, runnerList.Entries[0].Disabled)
assert.False(t, runnerList.Entries[0].Ephemeral)
assert.Len(t, runnerList.Entries[0].Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
@@ -107,11 +131,26 @@ func testActionsRunnerUser(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-user", runner.Name)
assert.Equal(t, int64(34346), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
runnerResp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, runnerResp, &runner)
assert.True(t, runner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
@@ -136,6 +175,7 @@ func testActionsRunnerOwner(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
assert.Equal(t, int64(34347), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
@@ -165,6 +205,7 @@ func testActionsRunnerOwner(t *testing.T) {
require.NotNil(t, expectedRunner)
assert.Equal(t, "runner_to_be_deleted-org", expectedRunner.Name)
assert.Equal(t, int64(34347), expectedRunner.ID)
assert.False(t, expectedRunner.Disabled)
assert.False(t, expectedRunner.Ephemeral)
assert.Len(t, expectedRunner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
@@ -179,11 +220,26 @@ func testActionsRunnerOwner(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
assert.Equal(t, int64(34347), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
runnerResp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, runnerResp, &runner)
assert.True(t, runner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
@@ -202,6 +258,22 @@ func testActionsRunnerOwner(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("DisableReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("UpdateReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetRepoScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
@@ -244,6 +316,7 @@ func testActionsRunnerRepo(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
assert.Equal(t, int64(34348), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
@@ -269,6 +342,7 @@ func testActionsRunnerRepo(t *testing.T) {
assert.Len(t, runnerList.Entries, 1)
assert.Equal(t, "runner_to_be_deleted-repo1", runnerList.Entries[0].Name)
assert.Equal(t, int64(34348), runnerList.Entries[0].ID)
assert.False(t, runnerList.Entries[0].Disabled)
assert.False(t, runnerList.Entries[0].Ephemeral)
assert.Len(t, runnerList.Entries[0].Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
@@ -283,11 +357,26 @@ func testActionsRunnerRepo(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
assert.Equal(t, int64(34348), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
runnerResp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, runnerResp, &runner)
assert.True(t, runner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
@@ -306,6 +395,22 @@ func testActionsRunnerRepo(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("DisableReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("UpdateReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetOrganizationScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
@@ -1,88 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/tests"
ap "github.com/go-ap/activitypub"
"github.com/stretchr/testify/assert"
)
func TestActivityPubPerson(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
t.Run("ExistingPerson", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
userID := 2
username := "user2"
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/user-id/%v", userID))
resp := MakeRequest(t, req, http.StatusOK)
body := resp.Body.Bytes()
assert.Contains(t, string(body), "@context")
var person ap.Person
err := person.UnmarshalJSON(body)
assert.NoError(t, err)
assert.Equal(t, ap.PersonType, person.Type)
assert.Equal(t, username, person.PreferredUsername.String())
keyID := person.GetID().String()
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v$", userID), keyID)
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v/outbox$", userID), person.Outbox.GetID().String())
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v/inbox$", userID), person.Inbox.GetID().String())
pubKey := person.PublicKey
assert.NotNil(t, pubKey)
publicKeyID := keyID + "#main-key"
assert.Equal(t, pubKey.ID.String(), publicKeyID)
pubKeyPem := pubKey.PublicKeyPem
assert.NotNil(t, pubKeyPem)
assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem)
})
t.Run("MissingPerson", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/activitypub/user-id/999999999")
resp := MakeRequest(t, req, http.StatusNotFound)
assert.Contains(t, resp.Body.String(), "user does not exist")
})
t.Run("MissingPersonInbox", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
srv := httptest.NewServer(testWebRoutes)
defer srv.Close()
defer test.MockVariableValue(&setting.AppURL, srv.URL+"/")()
username1 := "user1"
ctx := t.Context()
user1, err := user_model.GetUserByName(ctx, username1)
assert.NoError(t, err)
user1url := srv.URL + "/api/v1/activitypub/user-id/1#main-key"
c, err := activitypub.NewClient(t.Context(), user1, user1url)
assert.NoError(t, err)
user2inboxurl := srv.URL + "/api/v1/activitypub/user-id/2/inbox"
// Signed request succeeds
resp, err := c.Post([]byte{}, user2inboxurl)
assert.NoError(t, err)
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
// Unsigned request fails
req := NewRequest(t, "POST", user2inboxurl)
MakeRequest(t, req, http.StatusInternalServerError)
})
}
@@ -4,6 +4,8 @@
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
@@ -235,7 +237,40 @@ func TestAPIRenameBranch(t *testing.T) {
MakeRequest(t, req, http.StatusCreated)
resp := testAPIRenameBranch(t, "user2", "user2", "repo1", from, "new-branch-name", http.StatusForbidden)
assert.Contains(t, resp.Body.String(), "Branch is protected by glob-based protection rules.")
assert.Contains(t, resp.Body.String(), "Failed to rename branch due to branch protection rules.")
})
t.Run("RenameBranchToMatchProtectionRulesWithAllowedUser", func(t *testing.T) {
// allow an admin (the owner in this case) to rename a regular branch to one that matches a branch protection rule
repoName := "repo1"
ownerName := "user2"
from := "regular-branch-1"
ctx := NewAPITestContext(t, ownerName, repoName, auth_model.AccessTokenScopeWriteRepository)
testAPICreateBranch(t, ctx.Session, ownerName, repoName, "", from, http.StatusCreated)
// NOTE: The protected/** branch protection rule was created in a previous test, with push enabled.
testAPIRenameBranch(t, ownerName, ownerName, repoName, from, "protected/2", http.StatusNoContent)
})
t.Run("RenameBranchToMatchProtectionRulesWithUnauthorizedUser", func(t *testing.T) {
// don't allow renaming a regular branch to a protected branch if the doer is not in the push whitelist
repoName := "repo1"
ownerName := "user2"
pushWhitelist := []string{ownerName}
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", ownerName, repoName),
&api.BranchProtection{
RuleName: "owner-protected/**",
PushWhitelistUsernames: pushWhitelist,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
from := "regular-branch-2"
ctx := NewAPITestContext(t, ownerName, repoName, auth_model.AccessTokenScopeWriteRepository)
testAPICreateBranch(t, ctx.Session, ownerName, repoName, "", from, http.StatusCreated)
unprivilegedUser := "user40"
resp := testAPIRenameBranch(t, unprivilegedUser, ownerName, repoName, from, "owner-protected/1", http.StatusForbidden)
assert.Contains(t, resp.Body.String(), "Failed to rename branch due to branch protection rules.")
})
t.Run("RenameBranchNormalScenario", func(t *testing.T) {
testAPIRenameBranch(t, "user2", "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent)
@@ -243,6 +278,79 @@ func TestAPIRenameBranch(t *testing.T) {
})
}
func TestAPIUpdateBranchReference(t *testing.T) {
defer tests.PrepareTestEnv(t)()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
ctx := NewAPITestContext(t, "user2", "update-branch", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
giteaURL.Path = ctx.GitPath()
var defaultBranch string
t.Run("CreateRepo", doAPICreateRepository(ctx, false, func(t *testing.T, repo api.Repository) {
defaultBranch = repo.DefaultBranch
}))
createBranchReq := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/branches", ctx.Username, ctx.Reponame), &api.CreateBranchRepoOption{
BranchName: "feature",
OldRefName: defaultBranch,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, createBranchReq, http.StatusCreated)
var featureInitialCommit string
t.Run("LoadFeatureBranch", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
featureInitialCommit = branch.Commit.ID
assert.NotEmpty(t, featureInitialCommit)
}))
content := base64.StdEncoding.EncodeToString([]byte("branch update test"))
var newCommit string
doAPICreateFile(ctx, "docs/update.txt", &api.CreateFileOptions{
FileOptions: api.FileOptions{
BranchName: defaultBranch,
NewBranchName: defaultBranch,
Message: "add docs/update.txt",
},
ContentBase64: content,
}, func(t *testing.T, resp api.FileResponse) {
newCommit = resp.Commit.SHA
assert.NotEmpty(t, newCommit)
})(t)
updateReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: newCommit,
OldCommitID: featureInitialCommit,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, updateReq, http.StatusNoContent)
t.Run("FastForwardApplied", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
assert.Equal(t, newCommit, branch.Commit.ID)
}))
staleReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: newCommit,
OldCommitID: featureInitialCommit,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, staleReq, http.StatusUnprocessableEntity)
nonFFReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: featureInitialCommit,
OldCommitID: newCommit,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, nonFFReq, http.StatusUnprocessableEntity)
forceReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: featureInitialCommit,
OldCommitID: newCommit,
Force: true,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, forceReq, http.StatusNoContent)
t.Run("ForceApplied", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
assert.Equal(t, featureInitialCommit, branch.Commit.ID)
}))
})
}
func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
token := getUserToken(t, doerName, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.RenameBranchRepoOption{
@@ -254,15 +362,20 @@ func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to s
func TestAPIBranchProtection(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// Branch protection on branch that not exist
testAPICreateBranchProtection(t, "master/doesnotexist", 1, http.StatusCreated)
// Can create branch protection on branch that not exist
testAPICreateBranchProtection(t, "non-existing/branch", 1, http.StatusCreated)
testAPIGetBranchProtection(t, "non-existing/branch", http.StatusOK)
testAPIDeleteBranchProtection(t, "non-existing/branch", http.StatusNoContent)
// Get branch protection on branch that exist but not branch protection
testAPIGetBranchProtection(t, "master", http.StatusNotFound)
testAPICreateBranchProtection(t, "master", 2, http.StatusCreated)
testAPICreateBranchProtection(t, "master", 1, http.StatusCreated)
// Can only create once
testAPICreateBranchProtection(t, "master", 0, http.StatusForbidden)
testAPICreateBranchProtection(t, "other-branch", 2, http.StatusCreated)
// Can't delete a protected branch
testAPIDeleteBranch(t, "master", http.StatusForbidden)
@@ -294,6 +407,8 @@ func TestAPIBranchProtection(t *testing.T) {
// Test branch deletion
testAPIDeleteBranch(t, "master", http.StatusForbidden)
testAPIDeleteBranch(t, "branch2", http.StatusNoContent)
testAPIDeleteBranch(t, "branch2", http.StatusNotFound) // deleted branch, there is a record in DB with IsDelete=true
testAPIDeleteBranch(t, "no-such-branch", http.StatusNotFound) // non-existing branch, not exist in git or DB
}
func TestAPICreateBranchWithSyncBranches(t *testing.T) {
@@ -303,7 +418,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
RepoID: 1,
})
assert.NoError(t, err)
assert.Len(t, branches, 6)
assert.Len(t, branches, 8)
// make a broke repository with no branch on database
_, err = db.DeleteByBean(t.Context(), git_model.Branch{RepoID: 1})
@@ -320,7 +435,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
RepoID: 1,
})
assert.NoError(t, err)
assert.Len(t, branches, 7)
assert.Len(t, branches, 9)
branches, err = db.Find[git_model.Branch](t.Context(), git_model.FindBranchOptions{
RepoID: 1,
@@ -19,15 +19,35 @@ import (
"github.com/stretchr/testify/assert"
)
func TestCreateForkNoLogin(t *testing.T) {
func TestAPIFork(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("CreateForkNoLogin", testCreateForkNoLogin)
t.Run("CreateForkOrgNoCreatePermission", testCreateForkOrgNoCreatePermission)
t.Run("APIForkListLimitedAndPrivateRepos", testAPIForkListLimitedAndPrivateRepos)
t.Run("GetPrivateReposForks", testGetPrivateReposForks)
}
func testCreateForkNoLogin(t *testing.T) {
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{})
MakeRequest(t, req, http.StatusUnauthorized)
}
func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testCreateForkOrgNoCreatePermission(t *testing.T) {
user4Sess := loginUser(t, "user4")
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
canCreate, err := org_model.OrgFromUser(org).CanCreateOrgRepo(t.Context(), 4)
assert.NoError(t, err)
assert.False(t, canCreate)
user4Token := getTokenForLoggedInUser(t, user4Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
Organization: &org.Name,
}).AddTokenAuth(user4Token)
MakeRequest(t, req, http.StatusForbidden)
}
func testAPIForkListLimitedAndPrivateRepos(t *testing.T) {
user1Sess := loginUser(t, "user1")
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
@@ -64,10 +84,7 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
resp := MakeRequest(t, req, http.StatusOK)
var forks []*api.Repository
DecodeJSON(t, resp, &forks)
forks := DecodeJSON(t, resp, []*api.Repository{})
assert.Empty(t, forks)
assert.Equal(t, "0", resp.Header().Get("X-Total-Count"))
})
@@ -78,9 +95,7 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
var forks []*api.Repository
DecodeJSON(t, resp, &forks)
forks := DecodeJSON(t, resp, []*api.Repository{})
assert.Len(t, forks, 2)
assert.Equal(t, "2", resp.Header().Get("X-Total-Count"))
@@ -88,28 +103,22 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
resp = MakeRequest(t, req, http.StatusOK)
forks = []*api.Repository{}
DecodeJSON(t, resp, &forks)
forks = DecodeJSON(t, resp, []*api.Repository{})
assert.Len(t, forks, 2)
assert.Equal(t, "2", resp.Header().Get("X-Total-Count"))
})
}
func TestGetPrivateReposForks(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testGetPrivateReposForks(t *testing.T) {
user1Sess := loginUser(t, "user1")
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) // private repository
privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository)
forkedRepoName := "forked-repo"
// create fork from a private repository
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+repo2.FullName()+"/forks", &api.CreateForkOption{
Organization: &privateOrg.Name,
Name: &forkedRepoName,
Name: new("forked-repo"),
}).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusAccepted)
@@ -117,8 +126,7 @@ func TestGetPrivateReposForks(t *testing.T) {
req = NewRequest(t, "GET", "/api/v1/repos/"+repo2.FullName()+"/forks").AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
forks := []*api.Repository{}
DecodeJSON(t, resp, &forks)
forks := DecodeJSON(t, resp, []*api.Repository{})
assert.Len(t, forks, 1)
assert.Equal(t, "1", resp.Header().Get("X-Total-Count"))
assert.Equal(t, "forked-repo", forks[0].Name)
@@ -63,8 +63,8 @@ func TestGPGKeys(t *testing.T) {
t.Run("CreateInvalidGPGKey", func(t *testing.T) {
testCreateInvalidGPGKey(t, tc.makeRequest, tc.token, tc.results[4])
})
t.Run("CreateNoneRegistredEmailGPGKey", func(t *testing.T) {
testCreateNoneRegistredEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[5])
t.Run("CreateNoneRegisteredEmailGPGKey", func(t *testing.T) {
testCreateNoneRegisteredEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[5])
})
t.Run("CreateValidGPGKey", func(t *testing.T) {
testCreateValidGPGKey(t, tc.makeRequest, tc.token, tc.results[6])
@@ -179,7 +179,7 @@ func testCreateInvalidGPGKey(t *testing.T, makeRequest makeRequestFunc, token st
testCreateGPGKey(t, makeRequest, token, expected, "invalid_key")
}
func testCreateNoneRegistredEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
func testCreateNoneRegisteredEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
testCreateGPGKey(t, makeRequest, token, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFmGUygBCACjCNbKvMGgp0fd5vyFW9olE1CLCSyyF9gQN2hSuzmZLuAZF2Kh
@@ -95,9 +95,7 @@ func TestHTTPSigCert(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
csrf := GetUserCSRFToken(t, session)
req := NewRequestWithValues(t, "POST", "/user/settings/keys", map[string]string{
"_csrf": csrf,
"content": "user1",
"title": "principal",
"type": "principal",
@@ -8,6 +8,7 @@ import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -179,3 +180,19 @@ func TestAPIRepoValidateIssueConfig(t *testing.T) {
assert.NotEmpty(t, issueConfigValidation.Message)
})
}
func TestAPIRepoIssueConfigRequiresCodeUnit(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
for _, path := range []string{
fmt.Sprintf("/api/v1/repos/%s/issue_config", repo.FullName()),
fmt.Sprintf("/api/v1/repos/%s/issue_config/validate", repo.FullName()),
} {
req := NewRequest(t, "GET", path).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
}
@@ -29,7 +29,7 @@ func enableRepoDependencies(t *testing.T, repoID int64) {
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypeIssues})
repoUnit.IssuesConfig().EnableDependencies = true
assert.NoError(t, repo_model.UpdateRepoUnit(t.Context(), repoUnit))
assert.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoUnit))
}
func TestAPICreateIssueDependencyCrossRepoPermission(t *testing.T) {
@@ -44,7 +44,7 @@ func TestAPIIssuesReactions(t *testing.T) {
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
Reaction: "zzz",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
MakeRequest(t, req, http.StatusNoContent)
// Add allowed reaction
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
@@ -55,7 +55,10 @@ func TestAPIIssuesReactions(t *testing.T) {
DecodeJSON(t, resp, &apiNewReaction)
// Add existing reaction
MakeRequest(t, req, http.StatusForbidden)
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
Reaction: "rocket",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Blocked user can't react to comment
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
@@ -111,7 +114,7 @@ func TestAPICommentReactions(t *testing.T) {
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
Reaction: "eyes",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
MakeRequest(t, req, http.StatusNoContent)
t.Run("UnrelatedCommentID", func(t *testing.T) {
// Using the ID of a comment that does not belong to the repository must fail
@@ -142,7 +145,10 @@ func TestAPICommentReactions(t *testing.T) {
DecodeJSON(t, resp, &apiNewReaction)
// Add existing reaction
MakeRequest(t, req, http.StatusForbidden)
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
Reaction: "+1",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Get end result of reaction list of issue #1
req = NewRequest(t, "GET", urlStr).
@@ -8,10 +8,12 @@ import (
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
@@ -53,3 +55,19 @@ about: bar
assert.Equal(t, "error occurs when parsing issue template: count=2", resp.Header().Get("X-Gitea-Warning"))
})
}
func TestAPIIssueTemplateRequiresCodeUnit(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
issueTemplatesURL := "/api/v1/repos/" + repo.FullName() + "/issue_templates"
languagesURL := "/api/v1/repos/" + repo.FullName() + "/languages"
req := NewRequest(t, "GET", issueTemplatesURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", languagesURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
@@ -19,14 +19,25 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIListIssues(t *testing.T) {
func TestAPIIssue(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("ListIssues", testAPIListIssues)
t.Run("ListIssuesPublicOnly", testAPIListIssuesPublicOnly)
t.Run("SearchIssues", testAPISearchIssues)
t.Run("SearchIssuesWithLabels", testAPISearchIssuesWithLabels)
t.Run("EditIssue", testAPIEditIssue)
t.Run("IssueContentVersion", testAPIIssueContentVersion)
t.Run("CreateIssue", testAPICreateIssue)
t.Run("CreateIssueParallel", testAPICreateIssueParallel)
}
func testAPIListIssues(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -74,9 +85,7 @@ func TestAPIListIssues(t *testing.T) {
}
}
func TestAPIListIssuesPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPIListIssuesPublicOnly(t *testing.T) {
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo1.OwnerID})
@@ -99,11 +108,10 @@ func TestAPIListIssuesPublicOnly(t *testing.T) {
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
MakeRequest(t, req, http.StatusForbidden)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPICreateIssue(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPICreateIssue(t *testing.T) {
const body, title = "apiTestBody", "apiTestTitle"
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
@@ -141,9 +149,7 @@ func TestAPICreateIssue(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
}
func TestAPICreateIssueParallel(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPICreateIssueParallel(t *testing.T) {
// FIXME: There seems to be a bug in github.com/mattn/go-sqlite3 with sqlite_unlock_notify, when doing concurrent writes to the same database,
// some requests may get stuck in "go-sqlite3.(*SQLiteRows).Next", "go-sqlite3.(*SQLiteStmt).exec" and "go-sqlite3.unlock_notify_wait",
// because the "unlock_notify_wait" never returns and the internal lock never gets releases.
@@ -151,7 +157,7 @@ func TestAPICreateIssueParallel(t *testing.T) {
// The trigger is: a previous test created issues and made the real issue indexer queue start processing, then this test does concurrent writing.
// Adding this "Sleep" makes go-sqlite3 "finish" some internal operations before concurrent writes and then won't get stuck.
// To reproduce: make a new test run these 2 tests enough times:
// > func TestBug() { for i := 0; i < 100; i++ { testAPICreateIssue(t); testAPICreateIssueParallel(t) } }
// > func testBug() { for i := 0; i < 100; i++ { testAPICreateIssue(t); testAPICreateIssueParallel(t) } }
// Usually the test gets stuck in fewer than 10 iterations without this "sleep".
time.Sleep(time.Second)
@@ -196,9 +202,7 @@ func TestAPICreateIssueParallel(t *testing.T) {
wg.Wait()
}
func TestAPIEditIssue(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPIEditIssue(t *testing.T) {
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
@@ -262,11 +266,9 @@ func TestAPIEditIssue(t *testing.T) {
assert.Equal(t, title, issueAfter.Title)
}
func TestAPISearchIssues(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// as this API was used in the frontend, it uses UI page size
expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures
func testAPISearchIssues(t *testing.T) {
defer test.MockVariableValue(&setting.API.DefaultPagingNum, 20)()
expectedIssueCount := 20 // 20 is from the fixtures
link, _ := url.Parse("/api/v1/repos/issues/search")
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue)
@@ -361,11 +363,37 @@ func TestAPISearchIssues(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 2)
query = url.Values{"created": {"1"}} // issues created by the auth user
link.RawQuery = query.Encode()
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 5)
query = url.Values{"created": {"1"}, "type": {"pulls"}} // prs created by the auth user
link.RawQuery = query.Encode()
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 3)
query = url.Values{"created_by": {"user2"}} // issues created by the user2
link.RawQuery = query.Encode()
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 9)
query = url.Values{"created_by": {"user2"}, "type": {"pulls"}} // prs created by user2
link.RawQuery = query.Encode()
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 3)
}
func TestAPISearchIssuesWithLabels(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPISearchIssuesWithLabels(t *testing.T) {
// as this API was used in the frontend, it uses UI page size
expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures
@@ -420,3 +448,55 @@ func TestAPISearchIssuesWithLabels(t *testing.T) {
DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 2)
}
func testAPIIssueContentVersion(t *testing.T) {
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)
t.Run("ResponseIncludesContentVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
apiIssue := DecodeJSON(t, resp, &api.Issue{})
assert.GreaterOrEqual(t, apiIssue.ContentVersion, 0)
})
t.Run("EditWithCorrectVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
before := DecodeJSON(t, resp, &api.Issue{})
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Body: new("updated body with correct version"),
ContentVersion: new(before.ContentVersion),
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
after := DecodeJSON(t, resp, &api.Issue{})
assert.Equal(t, "updated body with correct version", after.Body)
assert.Greater(t, after.ContentVersion, before.ContentVersion)
})
t.Run("EditWithWrongVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Body: new("should fail"),
ContentVersion: new(99999),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusConflict)
})
t.Run("EditWithoutVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Body: new("edit without version succeeds"),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
})
}
@@ -79,6 +79,12 @@ func TestAPIDeleteTrackedTime(t *testing.T) {
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
// Deletion should be scoped to the issue in the URL
time5 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 5})
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time5.ID).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
time3 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 3})
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time3.ID).
AddTokenAuth(token)
@@ -1,35 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestNodeinfo(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
req := NewRequest(t, "GET", "/api/v1/nodeinfo")
resp := MakeRequest(t, req, http.StatusOK)
VerifyJSONSchema(t, resp, "nodeinfo_2.1.json")
var nodeinfo api.NodeInfo
DecodeJSON(t, resp, &nodeinfo)
assert.True(t, nodeinfo.OpenRegistrations)
assert.Equal(t, "gitea", nodeinfo.Software.Name)
assert.Equal(t, 29, nodeinfo.Usage.Users.Total)
assert.Equal(t, 22, nodeinfo.Usage.LocalPosts)
assert.Equal(t, 3, nodeinfo.Usage.LocalComments)
}
@@ -200,7 +200,7 @@ func TestAPINotificationPUT(t *testing.T) {
assert.False(t, apiNL[0].Pinned)
//
// Now nofication ID 2 is the first in the list and is unread.
// Now notification ID 2 is the first in the list and is unread.
//
req = NewRequest(t, "GET", "/api/v1/notifications?all=true").
AddTokenAuth(token)
@@ -212,3 +212,23 @@ func TestAPINotificationPUT(t *testing.T) {
assert.True(t, apiNL[0].Unread)
assert.False(t, apiNL[0].Pinned)
}
func TestAPINotificationPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
token := getUserToken(t, user2.Name, auth_model.AccessTokenScopeReadNotification, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/notifications").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", "/api/v1/notifications/new").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d", thread5.ID)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
@@ -137,34 +137,45 @@ func TestAPIOrgGeneral(t *testing.T) {
})
t.Run("OrgEdit", func(t *testing.T) {
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "private",
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
assert.NotEqual(t, api.VisibleTypeLimited, org3.Visibility)
var apiOrg api.Organization
DecodeJSON(t, resp, &apiOrg)
org3Edit := api.EditOrgOption{
FullName: new("new full name"),
Description: new("new description"),
Website: new("https://org3-new-website.example.com"),
Location: new("new location"),
Visibility: new("limited"),
Email: new("org3-new-email@example.com"),
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
apiOrg := DecodeJSON(t, resp, &api.Organization{})
assert.Equal(t, "org3", apiOrg.Name)
assert.Equal(t, org.FullName, apiOrg.FullName)
assert.Equal(t, org.Description, apiOrg.Description)
assert.Equal(t, org.Website, apiOrg.Website)
assert.Equal(t, org.Location, apiOrg.Location)
assert.Equal(t, org.Visibility, apiOrg.Visibility)
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
assert.Equal(t, *org3Edit.Description, apiOrg.Description)
assert.Equal(t, *org3Edit.Website, apiOrg.Website)
assert.Equal(t, *org3Edit.Location, apiOrg.Location)
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
assert.Equal(t, *org3Edit.Email, apiOrg.Email)
org3 = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
assert.Equal(t, api.VisibleTypeLimited, org3.Visibility)
// empty email can clear the email, nil fields won't change the settings
req = NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &api.EditOrgOption{
Email: new(""),
}).AddTokenAuth(user1Token)
resp = MakeRequest(t, req, http.StatusOK)
apiOrg = DecodeJSON(t, resp, &api.Organization{})
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
assert.Empty(t, apiOrg.Email)
})
t.Run("OrgEditBadVisibility", func(t *testing.T) {
t.Run("OrgEditInvalidVisibility", func(t *testing.T) {
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "badvisibility",
Visibility: new("invalid-visibility"),
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
arch_module "code.gitea.io/gitea/modules/packages/arch"
"code.gitea.io/gitea/modules/test"
arch_service "code.gitea.io/gitea/services/packages/arch"
"code.gitea.io/gitea/tests"
@@ -78,34 +79,6 @@ license = MIT`)
return buf.Bytes()
}
readIndexContent := func(r io.Reader) (map[string]string, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
content := make(map[string]string)
tr := tar.NewReader(gzr)
for {
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
buf, err := io.ReadAll(tr)
if err != nil {
return nil, err
}
content[hd.Name] = string(buf)
}
return content, nil
}
compressions := []string{"gz", "xz", "zst"}
repositories := []string{"main", "testing", "with/slash", ""}
@@ -204,7 +177,7 @@ license = MIT`)
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
resp := MakeRequest(t, req, http.StatusOK)
content, err := readIndexContent(resp.Body)
content, err := test.ReadAllTarGzContent(resp.Body)
assert.NoError(t, err)
desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)]
@@ -256,7 +229,7 @@ license = MIT`)
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
resp := MakeRequest(t, req, http.StatusOK)
content, err := readIndexContent(resp.Body)
content, err := test.ReadAllTarGzContent(resp.Body)
assert.NoError(t, err)
desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)]
@@ -311,7 +284,7 @@ license = MIT`)
req = NewRequest(t, "GET", fmt.Sprintf("%s/aarch64/%s", rootURL, arch_service.IndexArchiveFilename))
resp := MakeRequest(t, req, http.StatusOK)
content, err := readIndexContent(resp.Body)
content, err := test.ReadAllTarGzContent(resp.Body)
assert.NoError(t, err)
assert.Len(t, content, 2)
@@ -326,7 +299,7 @@ license = MIT`)
req = NewRequest(t, "GET", fmt.Sprintf("%s/aarch64/%s", rootURL, arch_service.IndexArchiveFilename))
resp = MakeRequest(t, req, http.StatusOK)
content, err = readIndexContent(resp.Body)
content, err = test.ReadAllTarGzContent(resp.Body)
assert.NoError(t, err)
assert.Len(t, content, 2)
_, has = content["gitea-test-1.0.0/desc"]
@@ -17,6 +17,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
composer_module "code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers/api/packages/composer"
"code.gitea.io/gitea/tests"
@@ -27,6 +28,8 @@ func TestPackageComposer(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
otherUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31})
vendorName := "gitea"
projectName := "composer-package"
@@ -251,5 +254,85 @@ func TestPackageComposer(t *testing.T) {
assert.Equal(t, repo1.HTMLURL(), pkgs[0].Source.URL)
assert.Equal(t, "git", pkgs[0].Source.Type)
assert.Equal(t, packageVersion, pkgs[0].Source.Reference)
// Private repository links remain visible to callers who can access the repository.
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
err = packages.SetRepositoryLink(t.Context(), userPkgs[0].ID, repo2.ID)
assert.NoError(t, err)
req = NewRequest(t, "GET", fmt.Sprintf("%s/p2/%s/%s.json", url, vendorName, projectName)).
AddBasicAuth(user.Name)
resp = MakeRequest(t, req, http.StatusOK)
result = composer.PackageMetadataResponse{}
DecodeJSON(t, resp, &result)
pkgs = result.Packages[packageName]
assert.Len(t, pkgs, 1)
assert.Equal(t, repo2.HTMLURL(), pkgs[0].Source.URL)
assert.Equal(t, "git", pkgs[0].Source.Type)
assert.Equal(t, packageVersion, pkgs[0].Source.Reference)
// Callers without repository access still get the package metadata, but not the private source URL.
req = NewRequest(t, "GET", fmt.Sprintf("%s/p2/%s/%s.json", url, vendorName, projectName)).
AddBasicAuth(otherUser.Name)
resp = MakeRequest(t, req, http.StatusOK)
result = composer.PackageMetadataResponse{}
DecodeJSON(t, resp, &result)
pkgs = result.Packages[packageName]
assert.Len(t, pkgs, 1)
assert.Empty(t, pkgs[0].Source.URL)
assert.Empty(t, pkgs[0].Source.Type)
assert.Empty(t, pkgs[0].Source.Reference)
})
t.Run("WebVisibilityBadge", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
listReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages", user.Name)).
AddBasicAuth(user.Name)
listResp := MakeRequest(t, listReq, http.StatusOK)
listDoc := NewHTMLParser(t, listResp.Body)
assert.Equal(t, 0, listDoc.Find(".flex-item-title .ui.basic.label").Length())
viewReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/composer/%s/%s", user.Name, neturl.PathEscape(packageName), neturl.PathEscape(packageVersion))).
AddBasicAuth(user.Name)
viewResp := MakeRequest(t, viewReq, http.StatusOK)
viewDoc := NewHTMLParser(t, viewResp.Body)
assert.Equal(t, 0, viewDoc.Find(".issue-title-header .ui.basic.label").Length())
privatePackageName := privateUser.Name + "/private-composer-package"
privatePackageVersion := "1.0.0"
privateContent := test.WriteZipArchive(map[string]string{
"composer.json": `{
"name": "` + privatePackageName + `",
"description": "Private Package",
"type": "` + packageType + `",
"license": "` + packageLicense + `",
"authors": [
{
"name": "` + packageAuthor + `"
}
]
}`,
}).Bytes()
privateUploadURL := fmt.Sprintf("%sapi/packages/%s/composer?version=%s", setting.AppURL, privateUser.Name, privatePackageVersion)
uploadReq := NewRequestWithBody(t, "PUT", privateUploadURL, bytes.NewReader(privateContent)).
AddBasicAuth(privateUser.Name)
MakeRequest(t, uploadReq, http.StatusCreated)
privateSession := loginUser(t, privateUser.Name)
privateListReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages", privateUser.Name))
privateListResp := privateSession.MakeRequest(t, privateListReq, http.StatusOK)
privateListDoc := NewHTMLParser(t, privateListResp.Body)
assert.Equal(t, 1, privateListDoc.Find(".flex-item-title .ui.basic.label").Length())
assert.Equal(t, "Private", privateListDoc.Find(".flex-item-title .ui.basic.label").First().Text())
privateViewReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/composer/%s/%s", privateUser.Name, neturl.PathEscape(privatePackageName), neturl.PathEscape(privatePackageVersion)))
privateViewResp := privateSession.MakeRequest(t, privateViewReq, http.StatusOK)
privateViewDoc := NewHTMLParser(t, privateViewResp.Body)
assert.Equal(t, 1, privateViewDoc.Find(".issue-title-header .ui.basic.label").Length())
assert.Equal(t, "Private", privateViewDoc.Find(".issue-title-header .ui.basic.label").First().Text())
})
}
@@ -346,15 +346,16 @@ func TestPackageConan(t *testing.T) {
pb, err := packages.GetBlobByID(t.Context(), pf.BlobID)
assert.NoError(t, err)
if pf.Name == conanfileName {
switch pf.Name {
case conanfileName:
assert.True(t, pf.IsLead)
assert.Equal(t, int64(len(buildConanfileContent(name, version1))), pb.Size)
} else if pf.Name == conaninfoName {
case conaninfoName:
assert.False(t, pf.IsLead)
assert.Equal(t, int64(len(contentConaninfo)), pb.Size)
} else {
default:
assert.FailNow(t, "unknown file", "unknown file: %s", pf.Name)
}
}
@@ -88,34 +88,34 @@ func TestPackageContainer(t *testing.T) {
Token string `json:"token"`
}
defaultAuthenticateValues := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`}
wwwAuthenticateForPublic := []string{
`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`,
}
wwwAuthenticateForRequiredSignIn := []string{
`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`,
`Basic realm="Gitea Container Registry"`,
}
t.Run("Anonymous", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", setting.AppURL+"v2")
resp := MakeRequest(t, req, http.StatusUnauthorized)
assert.ElementsMatch(t, defaultAuthenticateValues, resp.Header().Values("WWW-Authenticate"))
assert.ElementsMatch(t, wwwAuthenticateForPublic, resp.Header().Values("WWW-Authenticate"))
req = NewRequest(t, "GET", setting.AppURL+"v2/token")
resp = MakeRequest(t, req, http.StatusOK)
tokenResponse := &TokenResponse{}
DecodeJSON(t, resp, &tokenResponse)
assert.NotEmpty(t, tokenResponse.Token)
tokenResponse := DecodeJSON(t, resp, &TokenResponse{})
require.NotEmpty(t, tokenResponse.Token)
anonymousToken = "Bearer " + tokenResponse.Token
req = NewRequest(t, "GET", setting.AppURL+"v2").
AddTokenAuth(anonymousToken)
req = NewRequest(t, "GET", setting.AppURL+"v2").AddTokenAuth(anonymousToken)
MakeRequest(t, req, http.StatusOK)
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
req = NewRequest(t, "GET", setting.AppURL+"v2")
MakeRequest(t, req, http.StatusUnauthorized)
resp = MakeRequest(t, req, http.StatusUnauthorized)
assert.ElementsMatch(t, wwwAuthenticateForRequiredSignIn, resp.Header().Values("WWW-Authenticate"))
req = NewRequest(t, "GET", setting.AppURL+"v2/token")
MakeRequest(t, req, http.StatusUnauthorized)
@@ -132,17 +132,13 @@ func TestPackageContainer(t *testing.T) {
req := NewRequest(t, "GET", setting.AppURL+"v2")
resp := MakeRequest(t, req, http.StatusUnauthorized)
assert.ElementsMatch(t, wwwAuthenticateForPublic, resp.Header().Values("WWW-Authenticate"))
assert.ElementsMatch(t, defaultAuthenticateValues, resp.Header().Values("WWW-Authenticate"))
req = NewRequest(t, "GET", setting.AppURL+"v2/token").
AddBasicAuth(user.Name)
req = NewRequest(t, "GET", setting.AppURL+"v2/token").AddBasicAuth(user.Name)
resp = MakeRequest(t, req, http.StatusOK)
tokenResponse := &TokenResponse{}
DecodeJSON(t, resp, &tokenResponse)
tokenResponse := DecodeJSON(t, resp, &TokenResponse{})
assert.NotEmpty(t, tokenResponse.Token)
pkgMeta, err := package_service.ParseAuthorizationToken(tokenResponse.Token)
assert.NoError(t, err)
assert.Equal(t, user.ID, pkgMeta.UserID)
@@ -826,7 +822,6 @@ func TestPackageContainer(t *testing.T) {
newOwnerName := "newUsername"
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"name": newOwnerName,
"email": "user2@example.com",
"language": "en-US",
@@ -836,7 +831,6 @@ func TestPackageContainer(t *testing.T) {
t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName))
req = NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"name": user.Name,
"email": "user2@example.com",
"language": "en-US",
@@ -7,7 +7,9 @@ import (
"bytes"
"fmt"
"io"
"mime"
"net/http"
neturl "net/url"
"testing"
"code.gitea.io/gitea/models/packages"
@@ -140,11 +142,12 @@ func TestPackageGeneric(t *testing.T) {
t.Run("ServeDirect", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
if setting.Packages.Storage.Type == setting.MinioStorageType {
switch setting.Packages.Storage.Type {
case setting.MinioStorageType:
defer test.MockVariableValue(&setting.Packages.Storage.MinioConfig.ServeDirect, true)()
} else if setting.Packages.Storage.Type == setting.AzureBlobStorageType {
case setting.AzureBlobStorageType:
defer test.MockVariableValue(&setting.Packages.Storage.AzureBlobConfig.ServeDirect, true)()
} else {
default:
t.Skip("Test skipped for non-Minio-storage and non-AzureBlob-storage.")
}
@@ -163,6 +166,7 @@ func TestPackageGeneric(t *testing.T) {
resp2, err := (&http.Client{}).Get(location)
assert.NoError(t, err)
defer resp2.Body.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode, location)
body, err := io.ReadAll(resp2.Body)
@@ -171,6 +175,27 @@ func TestPackageGeneric(t *testing.T) {
checkDownloadCount(3)
})
t.Run("WebAssetUsesFilename", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeGeneric)
assert.NoError(t, err)
assert.Len(t, pvs, 1)
pfs, err := packages.GetFilesByVersionID(t.Context(), pvs[0].ID)
assert.NoError(t, err)
assert.NotEmpty(t, pfs)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/generic/%s/%s/files/%d", user.Name, neturl.PathEscape(packageName), neturl.PathEscape(packageVersion), pfs[0].ID))
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, content, resp.Body.Bytes())
disposition, params, err := mime.ParseMediaType(resp.Header().Get("Content-Disposition"))
assert.NoError(t, err)
assert.Equal(t, "attachment", disposition)
assert.Equal(t, pfs[0].Name, params["filename"])
})
})
t.Run("Delete", func(t *testing.T) {
@@ -160,7 +160,7 @@ func TestPackageMaven(t *testing.T) {
t.Run("UploadVerifySHA1", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("Missmatch", func(t *testing.T) {
t.Run("Mismatch", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "test", http.StatusBadRequest)
@@ -10,6 +10,7 @@ import (
"encoding/xml"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
neturl "net/url"
@@ -930,4 +931,34 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("UploadMultipartForm", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
packageContent := createPackage(packageName, packageVersion).Bytes()
// Simulate dotnet nuget push which sends multipart/form-data with X-NuGet-ApiKey auth
var body bytes.Buffer
mpw := multipart.NewWriter(&body)
part, err := mpw.CreateFormFile("package", "package.nupkg")
assert.NoError(t, err)
_, err = part.Write(packageContent)
assert.NoError(t, err)
err = mpw.Close()
assert.NoError(t, err)
req := NewRequestWithBody(t, "PUT", url, &body).
SetHeader("Content-Type", mpw.FormDataContentType())
addNuGetAPIKeyHeader(req, writeToken)
MakeRequest(t, req, http.StatusCreated)
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNuGet)
assert.NoError(t, err)
assert.Len(t, pvs, 1)
// Clean up
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, packageVersion)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNoContent)
})
}
@@ -12,15 +12,16 @@ import (
"io"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests"
"github.com/ProtonMail/go-crypto/openpgp"
@@ -31,14 +32,24 @@ import (
func TestPackageRpm(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
packageName := "gitea-test"
packageVersion := "1.0.2-1"
packageArchitecture := "x86_64"
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
base64RpmPackageContent := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF
// To build an RPM package, it needs tools lik "cpio", it's not easy to do in Golang.
// So here we only use pre-built package contents. And to save space, the content is gzipped and base64 encoded.
rpmContentFromGzipBase64 := func(t testing.TB, s string) []byte {
b, err := base64.StdEncoding.DecodeString(s)
require.NoError(t, err)
zr, err := gzip.NewReader(bytes.NewReader(b))
require.NoError(t, err)
content, err := io.ReadAll(zr)
require.NoError(t, err)
return content
}
packageRpmGzipBase64 := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF
VNwk2zd2PdvZ9Sxnd3Z3NllNsmF3o6congVFsWFHRWwIImIXfRER0QcRfPBJEXvvBQvWSfZTT0VQ
8TF/MuU33zcz3+zOJGEe73lyuQBRBWKWRzDrEddjuVAkxLMc+lsFUOWfm5bvvReAalWECg/TsivU
dyKa0U61aVnl6wj0Uxe4nc8F92hZiaYE8CO/P0r7/Quegr0c7M/AvoCaGZEIWNGUqMHrhhGROIUT
@@ -66,14 +77,17 @@ Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5
9tMDyaXb7OAlk5acuPn57ss9mw6Wym0m1Fq2cej7tUt2LL4/b8enXU2fndk+fvv57ndnt55/cQob
7tpp/pEjDS7cGPZ6BY430+7danDq6f42Nw49b9F7zp6BiKpJb9s5P0AYN2+L159cnrur636rx+v1
7ae1K28QbMMcqI8CqwIrgwg9nTOp8Oj9q81plUY7ZuwXN8Vvs8wbAAA=`
rpmPackageContent, err := base64.StdEncoding.DecodeString(base64RpmPackageContent)
assert.NoError(t, err)
zr, err := gzip.NewReader(bytes.NewReader(rpmPackageContent))
assert.NoError(t, err)
packageRpmContent := rpmContentFromGzipBase64(t, packageRpmGzipBase64)
content, err := io.ReadAll(zr)
assert.NoError(t, err)
decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) {
t.Helper()
zr, err := gzip.NewReader(resp.Body)
assert.NoError(t, err)
assert.NoError(t, xml.NewDecoder(zr).Decode(v))
}
rootURL := fmt.Sprintf("/api/packages/%s/rpm", user.Name)
@@ -99,7 +113,7 @@ gpgcheck=1
gpgkey=%sapi/packages/%s/rpm/repository.key`,
strings.Join(append([]string{user.LowerName}, groupParts...), "-"),
strings.Join(append([]string{user.Name, setting.AppName}, groupParts...), " - "),
util.URLJoin(setting.AppURL, groupURL),
strings.TrimSuffix(setting.AppURL, "/")+groupURL,
setting.AppURL,
user.Name,
)
@@ -120,10 +134,10 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
t.Run("Upload", func(t *testing.T) {
url := groupURL + "/upload"
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent))
MakeRequest(t, req, http.StatusUnauthorized)
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
@@ -146,9 +160,9 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
pb, err := packages.GetBlobByID(t.Context(), pfs[0].BlobID)
assert.NoError(t, err)
assert.Equal(t, int64(len(content)), pb.Size)
assert.Equal(t, int64(len(packageRpmContent)), pb.Size)
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusConflict)
})
@@ -159,12 +173,12 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
// download the package without the file name
req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture))
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, content, resp.Body.Bytes())
assert.Equal(t, packageRpmContent, resp.Body.Bytes())
// download the package with a file name (it can be anything)
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s/any-file-name", groupURL, packageName, packageVersion, packageArchitecture))
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, content, resp.Body.Bytes())
assert.Equal(t, packageRpmContent, resp.Body.Bytes())
})
t.Run("Repository", func(t *testing.T) {
@@ -248,15 +262,6 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNATURE-----")
})
decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) {
t.Helper()
zr, err := gzip.NewReader(resp.Body)
assert.NoError(t, err)
assert.NoError(t, xml.NewDecoder(zr).Decode(v))
}
t.Run("primary.xml.gz", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@@ -331,7 +336,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
assert.Equal(t, "sha256", p.Checksum.Type)
assert.Equal(t, "f1d5d2ffcbe4a7568e98b864f40d923ecca084e9b9bcd5977ed6521c46d3fa4c", p.Checksum.Checksum)
assert.Equal(t, "https://gitea.io", p.URL)
assert.EqualValues(t, len(content), p.Size.Package)
assert.EqualValues(t, len(packageRpmContent), p.Size.Package)
assert.EqualValues(t, 13, p.Size.Installed)
assert.EqualValues(t, 272, p.Size.Archive)
assert.Equal(t, fmt.Sprintf("package/%s/%s/%s/%s", packageName, packageVersion, packageArchitecture, fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture)), p.Location.Href)
@@ -421,6 +426,417 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
})
})
t.Run("Errata", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
type updateInfo struct {
XMLName xml.Name `xml:"updates"`
Xmlns string `xml:"xmlns,attr"`
Updates []*rpm_module.Update `xml:"update"`
}
errataURL := fmt.Sprintf("%s/package/%s/%s/errata", groupURL, packageName, packageVersion)
advisory := rpm_module.Update{
From: "security@example.com",
Status: "stable",
Type: "security",
Version: "1.0",
ID: "CVE-2023-1234",
Title: "Test Security Update",
Severity: "Important",
Description: "This is a test security update.",
References: []*rpm_module.Reference{
{
Href: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-1234",
ID: "CVE-2023-1234",
Title: "CVE-2023-1234",
Type: "cve",
},
},
PkgList: []*rpm_module.Collection{
{
Short: "el9",
Packages: []*rpm_module.UpdatePackage{
{
Arch: packageArchitecture,
Name: packageName,
Release: "1",
Src: "gitea-test-1.0.2-1.src.rpm",
Version: "1.0.2",
Filename: fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture),
},
},
},
},
}
t.Run("Success", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
updates := []*rpm_module.Update{&advisory}
body, err := json.Marshal(updates)
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
url := groupURL + "/repodata"
// Check repomd.xml contains updateinfo
req = NewRequest(t, "GET", url+"/repomd.xml")
resp := MakeRequest(t, req, http.StatusOK)
type testRepoData struct {
Type string `xml:"type,attr"`
}
var repomd struct {
Data []*testRepoData `xml:"data"`
}
err = xml.NewDecoder(resp.Body).Decode(&repomd)
require.NoError(t, err)
found := slices.IndexFunc(repomd.Data, func(s *testRepoData) bool {
return s.Type == "updateinfo"
}) >= 0
assert.True(t, found, "updateinfo not found in repomd.xml")
// Now check updateinfo.xml.gz
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
resp = MakeRequest(t, req, http.StatusOK)
var result updateInfo
decodeGzipXML(t, resp, &result)
assert.Equal(t, "http://linux.duke.edu/metadata/updateinfo", result.Xmlns)
assert.Len(t, result.Updates, 1)
assert.Equal(t, "CVE-2023-1234", result.Updates[0].ID)
assert.NotEmpty(t, result.Updates[0].Issued.Date)
assert.NotEmpty(t, result.Updates[0].Updated.Date)
})
t.Run("InvalidJSON", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithBody(t, "POST", errataURL, strings.NewReader("invalid json")).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusBadRequest)
})
t.Run("NullElementsInJSON", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Send a payload with null inside arrays
payload := `[
{
"id": "CVE-2023-5678",
"from": "security@example.com",
"status": "stable",
"type": "security",
"version": "1.0",
"title": "Test Null Elements",
"severity": "Important",
"description": "Test null elements",
"pkg_list": [
null,
{
"short": "el9",
"packages": [
null,
{
"arch": "x86_64",
"name": "gitea",
"release": "1",
"src": "gitea-1.0.0-1.src.rpm",
"version": "1.0.0",
"filename": "gitea-1.0.0-1.x86_64.rpm"
}
]
}
]
}
]`
req := NewRequestWithBody(t, "POST", errataURL, strings.NewReader(payload)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
// Verify it was stored correctly (skipping nulls)
url := groupURL + "/repodata"
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
resp := MakeRequest(t, req, http.StatusOK)
var result updateInfo
decodeGzipXML(t, resp, &result)
// We need to find the new advisory CVE-2023-5678
var newAdvisory *rpm_module.Update
for _, u := range result.Updates {
if u.ID == "CVE-2023-5678" {
newAdvisory = u
break
}
}
assert.NotNil(t, newAdvisory)
assert.Len(t, newAdvisory.PkgList, 1)
assert.Equal(t, "el9", newAdvisory.PkgList[0].Short)
assert.Len(t, newAdvisory.PkgList[0].Packages, 1)
assert.Equal(t, "gitea", newAdvisory.PkgList[0].Packages[0].Name)
})
t.Run("PackageNotFound", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
badURL := fmt.Sprintf("%s/package/%s/non-existent-version/errata", groupURL, packageName)
updates := []*rpm_module.Update{&advisory}
body, err := json.Marshal(updates)
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", badURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("MergeAdvisories", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Upload a second advisory with the same ID but a different package
advisory2 := advisory
advisory2.PkgList = []*rpm_module.Collection{
{
Short: "el9",
Packages: []*rpm_module.UpdatePackage{
{
Arch: packageArchitecture,
Name: "another-package",
Release: "1",
Src: "another-package-1.0.0-1.src.rpm",
Version: "1.0.0",
Filename: "another-package-1.0.0-1.x86_64.rpm",
},
},
},
}
updates := []*rpm_module.Update{&advisory2}
body, err := json.Marshal(updates)
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
// Check updateinfo.xml.gz again
url := groupURL + "/repodata"
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
resp := MakeRequest(t, req, http.StatusOK)
var result updateInfo
decodeGzipXML(t, resp, &result)
var targetUpdate *rpm_module.Update
for _, u := range result.Updates {
if u.ID == "CVE-2023-1234" {
targetUpdate = u
break
}
}
assert.NotNil(t, targetUpdate)
// Verify that package lists are merged into the same collection
assert.Len(t, targetUpdate.PkgList, 1)
assert.Len(t, targetUpdate.PkgList[0].Packages, 2)
assert.Equal(t, "another-package", result.Updates[0].PkgList[0].Packages[1].Name)
})
t.Run("NewCollection", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Upload a third advisory with the same ID but a different collection
advisory3 := advisory
advisory3.PkgList = []*rpm_module.Collection{
{
Short: "el8",
Packages: []*rpm_module.UpdatePackage{
{
Arch: packageArchitecture,
Name: packageName,
Release: "1",
Src: "gitea-test-1.0.2-1.src.rpm",
Version: "1.0.2",
Filename: fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture),
},
},
},
}
updates := []*rpm_module.Update{&advisory3}
body, err := json.Marshal(updates)
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
// Check updateinfo.xml.gz again
url := groupURL + "/repodata"
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
resp := MakeRequest(t, req, http.StatusOK)
var result updateInfo
decodeGzipXML(t, resp, &result)
var targetUpdate *rpm_module.Update
for _, u := range result.Updates {
if u.ID == "CVE-2023-1234" {
targetUpdate = u
break
}
}
assert.NotNil(t, targetUpdate)
// Verify that we now have 2 collections
assert.Len(t, targetUpdate.PkgList, 2)
// We need to be careful with order, map iteration is random
// Let's check both exist
shorts := []string{targetUpdate.PkgList[0].Short, targetUpdate.PkgList[1].Short}
assert.Contains(t, shorts, "el9")
assert.Contains(t, shorts, "el8")
})
t.Run("Idempotency", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
updates := []*rpm_module.Update{&advisory}
body, err := json.Marshal(updates)
assert.NoError(t, err)
// Post twice
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
req = NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
// Check updateinfo.xml.gz
url := groupURL + "/repodata"
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
resp := MakeRequest(t, req, http.StatusOK)
var result updateInfo
decodeGzipXML(t, resp, &result)
var targetUpdate *rpm_module.Update
for _, u := range result.Updates {
if u.ID == "CVE-2023-1234" {
targetUpdate = u
break
}
}
assert.NotNil(t, targetUpdate)
assert.Len(t, targetUpdate.PkgList, 2)
var el9Coll *rpm_module.Collection
for _, coll := range targetUpdate.PkgList {
if coll.Short == "el9" {
el9Coll = coll
break
}
}
assert.NotNil(t, el9Coll)
assert.Len(t, el9Coll.Packages, 2)
})
})
t.Run("NoArch", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
noarchPackageName := "gitea-noarch-test"
noarchPackageVersion := "1.0.0-1"
noarchPackageGzipBase64 := `H4sICKC05mkAA2dpdGVhLW5vYXJjaC10ZXN0LTEuMC4wLTEubm9hcmNoLnJwbQDtmLtrFEEcx+dy` +
`p0ZROYliFMUTLJJiz53HPgbBBwZR0Jgiglo5sztzLt6Lu0uIYmEhFhFUsBUJahd8gHZWKvgnpLES` +
`VIwYo43a6Dm7+zvfhbXsF+ZmP/N7zM5y03wXZt89yyOjbiXqKGHVG6IVnLQ6qt2xcNku2xZG/6wc` +
`WvL70qXbr3PwuAyh4gMz74TnW2YumqJVZl76vQPKrQEeTjn/2swFM6rAb9N61Ezr84sQPwfx9xA/` +
`b8Il6nEpWSgwVz7lrqOZ9oWjqKZSShIQT/k4cHWIAtcJHNsR1OGKSp8w5QcMM+4SiSV1OSY293wi` +
`TAMmmBauL1XoeJ7mXAqhafL6BXzzSd+N2eFP9x8/nHt25u7CBbN49t8/YKZMmTJlypQpU6ZMmTJl` +
`ypTpP1biiXS73Sso8TR+8U1KCOU+mHkXSnyN3HPICc3oh5yeTxL7Jn3A88Brgd8Ab0Q/fJTlZmwC` +
`XgC2gd+h1FfZDbwI9SPAHyB+EPgjxMeAP0O/ceAvED8B/BVYp1xYC1wDXg/nuww8CPvFvlHePG6A` +
`+D3gjcBd4KG0X24d1B9N63Nw3sKxND9XApaQPwQcAm8HVsAMWANz4CrwjpQHrsJ+8D0GnkIcvsfA` +
`C9j/OPBLyL8GPA/xmZj3oj/8OZT4cwij0WStFK+VmiI4JSoKjf8EZdMgevVgRjbqqg1/mEMHxtGR` +
`erupgkhHKkTVqD4xhdLuf27VswLL7VZQbjVrf3mZ9KVX9IZJqkZyaG+j1mypdluF+6KqGhU11R5G` +
`EItXRqKKKf6xNiZOVxsiSW7vF5NqrKV0NDWMqNmfWRixsptYkgx+iV1O/Mn+nldpHSYlq4KCZtRA` +
`lTNRE3E4lDWp6mGjZaUHTWomOtrykdCUKxxgLjTzfKG0J2wqFfeVFjyMzT1CfC255lRRn5HQDT2J` +
`mcDMdQRzeNqL0NBmhEimlTDpnoeV7xGPYSmpZ3vcljQIbYf6SmjJXEwoVT6xpc+k4wkS/7fSC97t` +
`xvcCFbdcTO92X57apoHNSquLs6s3b3s0uHXPpes77+yZnp5eaa7m3PxbJ3YYvwFme3wbyRUAAA==`
noarchRpmPackageContent := rpmContentFromGzipBase64(t, noarchPackageGzipBase64)
// Upload the noarch RPM
req := NewRequestWithBody(t, "PUT", groupURL+"/upload", bytes.NewReader(noarchRpmPackageContent)).AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
// The noarch package should appear in primary.xml when any arch is requested.
// At this point the group contains the existing x86_64 package + this noarch one.
req = NewRequest(t, "GET", groupURL+"/repodata/primary.xml.gz")
resp := MakeRequest(t, req, http.StatusOK)
type PackageSummary struct {
Name string `xml:"name"`
Architecture string `xml:"arch"`
}
type PrimaryMetadata struct {
XMLName xml.Name `xml:"metadata"`
PackageCount int `xml:"packages,attr"`
Packages []PackageSummary `xml:"package"`
}
var primary PrimaryMetadata
decodeGzipXML(t, resp, &primary)
// Both the arch-specific package uploaded earlier and the noarch one must be present.
assert.Equal(t, 2, primary.PackageCount)
assert.Len(t, primary.Packages, 2)
archNames := make([]string, 0, len(primary.Packages))
for _, p := range primary.Packages {
archNames = append(archNames, p.Architecture)
}
assert.Contains(t, archNames, packageArchitecture) // x86_64 from the Upload subtest
assert.Contains(t, archNames, "noarch")
// noarch package must be downloadable regardless of the arch path used.
for _, arch := range []string{"noarch", "x86_64", "aarch64", "my_arch"} {
req = NewRequest(t, "GET", fmt.Sprintf(
"%s/package/%s/%s/%s",
groupURL, noarchPackageName, noarchPackageVersion, arch,
))
MakeRequest(t, req, http.StatusOK)
}
// Clean up: delete via the canonical noarch path.
req = NewRequest(t, "DELETE", fmt.Sprintf(
"%s/package/%s/%s/noarch",
groupURL, noarchPackageName, noarchPackageVersion,
)).AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNoContent)
// After deletion, the noarch package must no longer be reachable via any arch.
for _, arch := range []string{"noarch", "x86_64", "aarch64"} {
req = NewRequest(t, "GET", fmt.Sprintf(
"%s/package/%s/%s/%s",
groupURL, noarchPackageName, noarchPackageVersion, arch,
))
MakeRequest(t, req, http.StatusNotFound)
}
// The x86_64 package from the Upload subtest must still be present.
req = NewRequest(t, "GET", groupURL+"/repodata/primary.xml.gz")
resp = MakeRequest(t, req, http.StatusOK)
var primaryAfter PrimaryMetadata
decodeGzipXML(t, resp, &primaryAfter)
assert.Equal(t, 1, primaryAfter.PackageCount)
assert.Equal(t, packageArchitecture, primaryAfter.Packages[0].Architecture)
})
t.Run("Delete", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@@ -442,7 +858,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
t.Run("UploadSign", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
url := groupURL + "/upload?sign=true"
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
@@ -18,6 +18,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
swift_module "code.gitea.io/gitea/modules/packages/swift"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
swift_router "code.gitea.io/gitea/routers/api/packages/swift"
"code.gitea.io/gitea/tests"
@@ -35,9 +36,26 @@ func TestPackageSwift(t *testing.T) {
packageID := packageScope + "." + packageName
packageVersion := "1.0.3"
packageVersion2 := "1.0.4"
packageVersion3 := "1.0.5"
packageAuthor := "KN4CK3R"
packageDescription := "Gitea Test Package"
packageRepositoryURL := "https://gitea.io/gitea/gitea"
packageCodeRepositoryURL := "https://gitea.io/gitea/gitea" // this one is not used as a property, it is meta
packageLicenseURL := "https://opensource.org/license/mit"
packageRepositoryURL1 := "https://gitea.io/gitea/repo"
packageRepositoryURLs := []string{packageRepositoryURL1, "https://gitea.io/gitea/repo.git", "ssh://git@gitea.io/gitea/repo.git"}
makePackageMetadataJSON := func(ver string) string {
tmpl := `{
"name":"` + packageName + `",
"version":"%s",
"description":"` + packageDescription + `",
"codeRepository":"` + packageCodeRepositoryURL + `",
"licenseURL":"` + packageLicenseURL + `",
"author":{"givenName":"` + packageAuthor + `"},
"repositoryURLs":["` + strings.Join(packageRepositoryURLs, `","`) + `"]
}`
return fmt.Sprintf(tmpl, ver)
}
contentManifest1 := "// swift-tools-version:5.7\n//\n// Package.swift"
contentManifest2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
@@ -146,7 +164,7 @@ func TestPackageSwift(t *testing.T) {
"Package.swift": contentManifest1,
"Package@swift-5.6.swift": contentManifest2,
}),
`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
makePackageMetadataJSON(packageVersion),
)
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeSwift)
@@ -164,8 +182,8 @@ func TestPackageSwift(t *testing.T) {
assert.Len(t, metadata.Manifests, 2)
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
assert.Len(t, pd.VersionProperties, 1)
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
assert.Len(t, pd.VersionProperties, 3)
assert.Equal(t, packageRepositoryURL1, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
pfs, err := packages.GetFilesByVersionID(t.Context(), pvs[0].ID)
assert.NoError(t, err)
@@ -234,7 +252,7 @@ func TestPackageSwift(t *testing.T) {
"Package.swift": contentManifest1,
"Package@swift-5.6.swift": contentManifest2,
}),
`{"name":"`+packageName+`","version":"`+packageVersion2+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
makePackageMetadataJSON(packageVersion2),
)
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeSwift)
@@ -252,8 +270,8 @@ func TestPackageSwift(t *testing.T) {
assert.Len(t, metadata.Manifests, 2)
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
assert.Len(t, pd.VersionProperties, 1)
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
assert.Len(t, pd.VersionProperties, 3)
assert.Equal(t, packageRepositoryURL1, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
pfs, err := packages.GetFilesByVersionID(t.Context(), thisPackageVersion.ID)
assert.NoError(t, err)
@@ -318,7 +336,7 @@ func TestPackageSwift(t *testing.T) {
AddBasicAuth(user.Name)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, body, resp.Body.String())
assert.JSONEq(t, body, resp.Body.String())
})
t.Run("PackageVersionMetadata", func(t *testing.T) {
@@ -354,8 +372,11 @@ func TestPackageSwift(t *testing.T) {
assert.Equal(t, packageVersion, result.Metadata.Version)
assert.Equal(t, packageDescription, result.Metadata.Description)
assert.Equal(t, "Swift", result.Metadata.ProgrammingLanguage.Name)
assert.Equal(t, packageLicenseURL, result.Metadata.LicenseURL)
require.NotNil(t, result.Metadata.Author)
assert.Equal(t, packageAuthor, result.Metadata.Author.Name)
assert.Equal(t, packageAuthor, result.Metadata.Author.GivenName)
assert.ElementsMatch(t, packageRepositoryURLs, result.Metadata.RepositoryURLs)
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s.json", url, packageScope, packageName, packageVersion)).
AddBasicAuth(user.Name)
@@ -364,6 +385,41 @@ func TestPackageSwift(t *testing.T) {
assert.Equal(t, body, resp.Body.String())
})
t.Run("UploadEmptyJSONMetadata", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion3)
var body bytes.Buffer
mpw := multipart.NewWriter(&body)
part, err := mpw.CreateFormFile("source-archive", "source-archive.zip")
require.NoError(t, err)
_, err = io.Copy(part, test.WriteZipArchive(map[string]string{
"Package.swift": contentManifest1,
"Package@swift-5.6.swift": contentManifest2,
}))
require.NoError(t, err)
require.NoError(t, mpw.WriteField("metadata", "{}"))
require.NoError(t, mpw.Close())
req := NewRequestWithBody(t, "PUT", uploadURL, &body).
SetHeader("Content-Type", mpw.FormDataContentType()).
SetHeader("Accept", swift_router.AcceptJSON).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion3)).
AddBasicAuth(user.Name).
SetHeader("Accept", swift_router.AcceptJSON)
resp := MakeRequest(t, req, http.StatusOK)
result := DecodeJSON(t, resp, &swift_router.PackageVersionMetadataResponse{})
assert.Nil(t, result.Metadata.Author)
assert.Empty(t, result.Metadata.RepositoryURLs)
assert.Empty(t, result.Metadata.CodeRepository)
assert.Empty(t, result.Metadata.LicenseURL)
})
t.Run("DownloadManifest", func(t *testing.T) {
manifestURL := fmt.Sprintf("%s/%s/%s/%s/Package.swift", url, packageScope, packageName, packageVersion)
@@ -421,7 +477,7 @@ func TestPackageSwift(t *testing.T) {
req = NewRequest(t, "GET", url+"/identifiers?url=https://unknown.host/")
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", url+"/identifiers?url="+packageRepositoryURL).
req = NewRequest(t, "GET", url+"/identifiers?url="+packageRepositoryURL1).
SetHeader("Accept", swift_router.AcceptJSON)
resp = MakeRequest(t, req, http.StatusOK)
File diff suppressed because one or more lines are too long
@@ -85,6 +85,50 @@ func TestPackageAPI(t *testing.T) {
assert.Equal(t, user.Name, p.Creator.UserName)
})
t.Run("DeleteEntirePackage", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
packageName := "test-package-entire-delete"
for _, version := range []string{"1.0.1", "1.0.2"} {
url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/file.bin", user.Name, packageName, version)
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1})).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
}
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/generic/%s", user.Name, packageName)).
AddTokenAuth(tokenWritePackage)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s", user.Name, packageName)).
AddTokenAuth(tokenReadPackage)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("DeletePackageVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
packageName := "test-package-version-delete"
for _, version := range []string{"1.0.1", "1.0.2"} {
url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/file.bin", user.Name, packageName, version)
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1})).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
}
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.1", user.Name, packageName)).
AddTokenAuth(tokenWritePackage)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.1", user.Name, packageName)).
AddTokenAuth(tokenReadPackage)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.2", user.Name, packageName)).
AddTokenAuth(tokenReadPackage)
MakeRequest(t, req, http.StatusOK)
})
t.Run("ListPackageVersions", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@@ -84,7 +84,7 @@ func TestAPIPrivateServ(t *testing.T) {
assert.Empty(t, results)
// Add reading deploy key
deployKey, err := asymkey_model.AddDeployKey(ctx, 19, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
deployKey, err := asymkey_model.AddDeployKey(ctx, 19 /* repo id */, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
assert.NoError(t, err)
// Can pull from repo we're a deploy key for
@@ -106,17 +106,17 @@ func TestAPIPrivateServ(t *testing.T) {
assert.Empty(t, results)
// Cannot pull from a private repo we're not associated with
results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
assert.Error(t, extra.Error)
assert.Empty(t, results)
// Cannot pull from a public repo we're not associated with
results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
assert.Error(t, extra.Error)
assert.Empty(t, results)
// Add writing deploy key
deployKey, err = asymkey_model.AddDeployKey(ctx, 20, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
deployKey, err = asymkey_model.AddDeployKey(ctx, 20 /* repo id */, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
assert.NoError(t, err)
// Cannot push to a private repo with reading key
@@ -0,0 +1,107 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIUserReposPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/repos").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var repos []api.Repository
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
req = NewRequest(t, "GET", "/api/v1/users/user2/repos").
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
}
func repoNames(repos []api.Repository) []string {
names := make([]string, 0, len(repos))
for _, repo := range repos {
names = append(names, repo.FullName)
}
return names
}
func TestAPIRepoByIDPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/repositories/1").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", "/api/v1/repositories/2").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIActivityFeedsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser)
req := NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var activities []api.Activity
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
AddTokenAuth(publicToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assertPublicActivitiesOnly(t, activities)
orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
AddTokenAuth(orgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicOrgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
AddTokenAuth(publicOrgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assertPublicActivitiesOnly(t, activities)
}
func assertPublicActivitiesOnly(t *testing.T, activities []api.Activity) {
t.Helper()
for _, activity := range activities {
assert.False(t, activity.IsPrivate)
if activity.Repo != nil {
assert.False(t, activity.Repo.Private)
}
}
}
@@ -15,9 +15,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -362,6 +364,79 @@ func TestAPIPullReviewRequest(t *testing.T) {
MakeRequest(t, req, http.StatusNoContent)
}
func TestAPIPullReviewCommentResolveEndpoints(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ctx := t.Context()
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
require.NoError(t, pullIssue.LoadAttributes(ctx))
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: pullIssue.PosterID})
require.NoError(t, pullIssue.LoadPullRequest(ctx))
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
require.NoError(t, err)
defer gitRepo.Close()
latestCommitID, err := gitRepo.GetRefCommitID(pullIssue.PullRequest.GetGitHeadRefName())
require.NoError(t, err)
codeComment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, pullIssue, 1, "resolve comment", "README.md", false, 0, latestCommitID, nil)
require.NoError(t, err)
require.NotNil(t, codeComment)
session := loginUser(t, doer.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
resolveURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/resolve", repo.OwnerName, repo.Name, codeComment.ID)
unresolveURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/unresolve", repo.OwnerName, repo.Name, codeComment.ID)
req := NewRequest(t, http.MethodPost, resolveURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify comment is resolved
updatedComment, err := issues_model.GetCommentByID(ctx, codeComment.ID)
require.NoError(t, err)
assert.NotZero(t, updatedComment.ResolveDoerID)
assert.Equal(t, doer.ID, updatedComment.ResolveDoerID)
// Resolving again should be idempotent
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, http.MethodPost, unresolveURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify comment is unresolved
updatedComment, err = issues_model.GetCommentByID(ctx, codeComment.ID)
require.NoError(t, err)
assert.Zero(t, updatedComment.ResolveDoerID)
// Unresolving again should be idempotent
MakeRequest(t, req, http.StatusNoContent)
// Non-existing comment ID
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/999999/resolve", repo.OwnerName, repo.Name)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
// Non-code-comment
plainComment, err := issue_service.CreateIssueComment(ctx, doer, repo, pullIssue, "not a review comment", nil)
require.NoError(t, err)
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/resolve", repo.OwnerName, repo.Name, plainComment.ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusBadRequest)
// Test permission check: use a user without write access for target repo to test 403 response
unauthorizedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
require.NotEqual(t, pullIssue.PosterID, unauthorizedUser.ID)
unauthorizedSession := loginUser(t, unauthorizedUser.Name)
unauthorizedToken := getTokenForLoggedInUser(t, unauthorizedSession, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository)
req = NewRequest(t, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", repo.OwnerName, repo.Name, plainComment.ID)).AddTokenAuth(unauthorizedToken)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, http.MethodPost, resolveURL).AddTokenAuth(unauthorizedToken)
MakeRequest(t, req, http.StatusForbidden)
}
func TestAPIPullReviewStayDismissed(t *testing.T) {
// This test against issue https://github.com/go-gitea/gitea/issues/28542
// where old reviews surface after a review request got dismissed.
@@ -423,7 +498,7 @@ func TestAPIPullReviewStayDismissed(t *testing.T) {
pullIssue.ID, user8.ID, 1, 1, 2, false)
// user8 dismiss review
permUser8, err := access_model.GetUserRepoPermission(t.Context(), pullIssue.Repo, user8)
permUser8, err := access_model.GetIndividualUserRepoPermission(t.Context(), pullIssue.Repo, user8)
assert.NoError(t, err)
_, err = issue_service.ReviewRequest(t.Context(), pullIssue, user8, &permUser8, user8, false)
assert.NoError(t, err)
@@ -270,13 +270,20 @@ func TestAPICreatePullSuccess(t *testing.T) {
owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
session := loginUser(t, owner11.Name)
prTitle := "test pull request title " + time.Now().String()
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
Head: owner11.Name + ":master",
Base: "master",
Title: "create a failure pr",
Title: prTitle,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// Also test that AllowMaintainerEdit is true by default, the "false" case is covered by TestAPICreatePullBasePermission
prIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: prTitle})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: prIssue.ID})
assert.True(t, pr.AllowMaintainerEdit)
MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
}
@@ -290,11 +297,14 @@ func TestAPICreatePullBasePermission(t *testing.T) {
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
session := loginUser(t, user4.Name)
prTitle := "test pull request title " + time.Now().String()
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
opts := &api.CreatePullRequestOption{
Head: repo11.OwnerName + ":master",
Base: "master",
Title: "create a failure pr",
Title: prTitle,
AllowMaintainerEdit: new(false),
}
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
@@ -306,6 +316,11 @@ func TestAPICreatePullBasePermission(t *testing.T) {
// create again
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// Also test that AllowMaintainerEdit is set to false, the default "true" case is covered by TestAPICreatePullSuccess
prIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: prTitle})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: prIssue.ID})
assert.False(t, pr.AllowMaintainerEdit)
}
func TestAPICreatePullHeadPermission(t *testing.T) {
@@ -442,9 +457,8 @@ func TestAPIEditPull(t *testing.T) {
Base: "master",
Title: title,
}).AddTokenAuth(token)
apiPull := new(api.PullRequest)
resp := MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, apiPull)
apiPull := DecodeJSON(t, resp, &api.PullRequest{})
assert.Equal(t, "master", apiPull.Base.Name)
newTitle := "edit a this pr"
@@ -455,8 +469,9 @@ func TestAPIEditPull(t *testing.T) {
Body: &newBody,
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, apiPull)
apiPull = DecodeJSON(t, resp, &api.PullRequest{})
assert.Equal(t, "feature/1", apiPull.Base.Name)
// check comment history
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
err := pull.LoadIssue(t.Context())
@@ -468,6 +483,62 @@ func TestAPIEditPull(t *testing.T) {
Base: "not-exist",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
t.Run("PullContentVersion", func(t *testing.T) {
testAPIPullContentVersion(t, pull.ID)
})
}
func testAPIPullContentVersion(t *testing.T, pullID int64) {
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pullID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pull.BaseRepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner.Name, repo.Name, pull.Index)
t.Run("ResponseIncludesContentVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
apiPull := DecodeJSON(t, resp, &api.PullRequest{})
assert.GreaterOrEqual(t, apiPull.ContentVersion, 0)
})
t.Run("EditWithCorrectVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
before := DecodeJSON(t, resp, &api.PullRequest{})
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
Body: new("updated body with correct version"),
ContentVersion: new(before.ContentVersion),
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
after := DecodeJSON(t, resp, &api.PullRequest{})
assert.Equal(t, "updated body with correct version", after.Body)
assert.Greater(t, after.ContentVersion, before.ContentVersion)
})
t.Run("EditWithWrongVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
Body: new("should fail"),
ContentVersion: new(99999),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusConflict)
})
t.Run("EditWithoutVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
Body: new("edit without version succeeds"),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
})
}
func doAPIGetPullFiles(ctx APITestContext, pr *api.PullRequest, callback func(*testing.T, []*api.ChangedFile)) func(*testing.T) {
@@ -13,15 +13,15 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) {
func testAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) {
// Limit the allowed release types (since by default there is no restriction)
defer test.MockVariableValue(&setting.Repository.Release.AllowedTypes, ".exe")()
defer tests.PrepareTestEnv(t)()
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9})
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
@@ -38,3 +38,34 @@ func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) {
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
}
func testAPIDraftReleaseAttachmentAccess(t *testing.T) {
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 13})
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
reader := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
listURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", repoOwner.Name, repo.Name, release.ID)
getURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", repoOwner.Name, repo.Name, release.ID, attachment.ID)
MakeRequest(t, NewRequest(t, "GET", listURL), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", getURL), http.StatusNotFound)
readerToken := getUserToken(t, reader.LowerName, auth_model.AccessTokenScopeReadRepository)
MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(readerToken), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(readerToken), http.StatusNotFound)
ownerReadToken := getUserToken(t, repoOwner.LowerName, auth_model.AccessTokenScopeReadRepository)
MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(ownerReadToken), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(ownerReadToken), http.StatusNotFound)
ownerToken := getUserToken(t, repoOwner.LowerName, auth_model.AccessTokenScopeWriteRepository)
resp := MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(ownerToken), http.StatusOK)
var attachments []*api.Attachment
DecodeJSON(t, resp, &attachments)
if assert.Len(t, attachments, 1) {
assert.Equal(t, attachment.ID, attachments[0].ID)
}
MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(ownerToken), http.StatusOK)
}
@@ -29,12 +29,22 @@ import (
"github.com/stretchr/testify/assert"
)
func TestAPIListReleases(t *testing.T) {
func TestAPIReleaseRead(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("DraftReleaseAttachmentAccess", testAPIDraftReleaseAttachmentAccess)
t.Run("ListReleasesWithWriteToken", testAPIListReleasesWithWriteToken)
t.Run("ListReleasesWithReadToken", testAPIListReleasesWithReadToken)
t.Run("GetDraftRelease", testAPIGetDraftRelease)
t.Run("GetLatestRelease", testAPIGetLatestRelease)
t.Run("GetReleaseByTag", testAPIGetReleaseByTag)
t.Run("GetDraftReleaseByTag", testAPIGetDraftReleaseByTag)
t.Run("EditReleaseAttachmentWithUnallowedFile", testAPIEditReleaseAttachmentWithUnallowedFile) // failed attempt, so it is also a read test
}
func testAPIListReleasesWithWriteToken(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository)
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository)
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
@@ -81,6 +91,72 @@ func TestAPIListReleases(t *testing.T) {
testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft")
}
func testAPIListReleasesWithReadToken(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository)
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
var apiReleases []*api.Release
DecodeJSON(t, resp, &apiReleases)
if assert.Len(t, apiReleases, 2) {
for _, release := range apiReleases {
switch release.ID {
case 1:
assert.False(t, release.IsDraft)
assert.False(t, release.IsPrerelease)
assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/1/assets"), release.UploadURL)
case 5:
assert.False(t, release.IsDraft)
assert.True(t, release.IsPrerelease)
assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/5/assets"), release.UploadURL)
default:
assert.NoError(t, fmt.Errorf("unexpected release: %v", release))
}
}
}
// test filter
testFilterByLen := func(auth bool, query url.Values, expectedLength int, msgAndArgs ...string) {
link.RawQuery = query.Encode()
req := NewRequest(t, "GET", link.String())
if auth {
req.AddTokenAuth(token)
}
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiReleases)
assert.Len(t, apiReleases, expectedLength, msgAndArgs)
}
testFilterByLen(false, url.Values{"draft": {"true"}}, 0, "anon should not see drafts")
testFilterByLen(true, url.Values{"draft": {"true"}}, 0, "repo owner with read token should not see drafts")
testFilterByLen(true, url.Values{"draft": {"false"}}, 2, "exclude drafts")
testFilterByLen(true, url.Values{"draft": {"false"}, "pre-release": {"false"}}, 1, "exclude drafts and pre-releases")
testFilterByLen(true, url.Values{"pre-release": {"true"}}, 1, "only get pre-release")
testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft")
}
func testAPIGetDraftRelease(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 4})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
reader := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d", owner.Name, repo.Name, release.ID)
MakeRequest(t, NewRequest(t, "GET", urlStr), http.StatusNotFound)
readerToken := getUserToken(t, reader.LowerName, auth_model.AccessTokenScopeReadRepository)
MakeRequest(t, NewRequest(t, "GET", urlStr).AddTokenAuth(readerToken), http.StatusNotFound)
ownerToken := getUserToken(t, owner.LowerName, auth_model.AccessTokenScopeWriteRepository)
resp := MakeRequest(t, NewRequest(t, "GET", urlStr).AddTokenAuth(ownerToken), http.StatusOK)
var apiRelease api.Release
DecodeJSON(t, resp, &apiRelease)
assert.Equal(t, release.Title, apiRelease.Title)
}
func createNewReleaseUsingAPI(t *testing.T, token string, owner *user_model.User, repo *repo_model.Repository, name, target, title, desc string) *api.Release {
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases", owner.Name, repo.Name)
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateReleaseOption{
@@ -230,9 +306,7 @@ func TestAPICreateReleaseGivenInvalidTarget(t *testing.T) {
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIGetLatestRelease(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPIGetLatestRelease(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -245,9 +319,7 @@ func TestAPIGetLatestRelease(t *testing.T) {
assert.Equal(t, "testing-release", release.Title)
}
func TestAPIGetReleaseByTag(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPIGetReleaseByTag(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -271,9 +343,7 @@ func TestAPIGetReleaseByTag(t *testing.T) {
assert.NotEmpty(t, err.Message)
}
func TestAPIGetDraftReleaseByTag(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPIGetDraftReleaseByTag(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -335,7 +405,7 @@ func TestAPIDeleteReleaseByTagName(t *testing.T) {
func TestAPIUploadAssetRelease(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Attachment.MaxSize, 1)()
defer test.MockVariableValue(&setting.Repository.Release.FileMaxSize, 1)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -352,7 +422,7 @@ func TestAPIUploadAssetRelease(t *testing.T) {
defer tests.PrintCurrentTest(t)()
const filename = "image.png"
performUpload := func(t *testing.T, uploadURL string, buf []byte, expectedStatus int) *httptest.ResponseRecorder {
performUpload := func(t *testing.T, uploadURL string, _ []byte, _ int) *httptest.ResponseRecorder {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("attachment", filename)
@@ -29,10 +29,10 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
session := loginUser(t, user1.LowerName)
// public only token should be forbidden
// public-only token cannot see a private repo
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeWriteRepository)
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches", repo3.Name)) // a plain repo
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
@@ -46,7 +46,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
assert.Equal(t, "master", branches[1].Name)
link2, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch", repo3.Name))
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
resp = MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(token), http.StatusOK)
bs, err = io.ReadAll(resp.Body)
@@ -55,7 +55,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
assert.NoError(t, json.Unmarshal(bs, &branch))
assert.Equal(t, "test_branch", branch.Name)
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
req := NewRequest(t, "POST", link.String()).AddTokenAuth(token)
req.Header.Add("Content-Type", "application/json")
@@ -81,7 +81,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
link3, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch2", repo3.Name))
MakeRequest(t, NewRequest(t, "DELETE", link3.String()), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(token), http.StatusNoContent)
assert.NoError(t, err)
@@ -121,10 +121,10 @@ func TestAPIRepoBranchesMirror(t *testing.T) {
resp = MakeRequest(t, req, http.StatusForbidden)
bs, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
resp = MakeRequest(t, NewRequest(t, "DELETE", link2.String()).AddTokenAuth(token), http.StatusForbidden)
bs, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
}
@@ -5,46 +5,60 @@ package integration
import (
"net/http"
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPICompareBranches(t *testing.T) {
defer tests.PrepareTestEnv(t)()
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
session2 := loginUser(t, "user2")
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Login as User2.
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
t.Run("CompareBranches", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("CompareBranches", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token2)
resp := MakeRequest(t, req, http.StatusOK)
apiResp := DecodeJSON(t, resp, &api.Compare{})
assert.Equal(t, 2, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 2)
})
var apiResp *api.Compare
DecodeJSON(t, resp, &apiResp)
t.Run("CompareCommits", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
assert.Equal(t, 2, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 2)
})
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token2)
resp := MakeRequest(t, req, http.StatusOK)
apiResp := DecodeJSON(t, resp, &api.Compare{})
assert.Equal(t, 1, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 1)
})
t.Run("CompareCommits", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
t.Run("CompareForkOnlyCommit", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
var apiResp *api.Compare
DecodeJSON(t, resp, &apiResp)
user13 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13})
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
user13Sess := loginUser(t, "user13")
user13Token := getTokenForLoggedInUser(t, user13Sess, auth_model.AccessTokenScopeWriteRepository)
assert.Equal(t, 1, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 1)
_, err := createFileInBranch(user13, repo11, createFileInBranchOptions{OldBranch: "master", NewBranch: "new-branch"}, map[string]string{"file.txt": "content"})
require.NoError(t, err)
req := NewRequestf(t, "GET", "/api/v1/repos/user12/repo10/compare/master...user13:new-branch").AddTokenAuth(user13Token)
resp := MakeRequest(t, req, http.StatusOK)
apiResp := DecodeJSON(t, resp, &api.Compare{})
assert.Equal(t, 1, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 1)
})
})
}
@@ -418,5 +418,20 @@ func TestAPIRepoEdit(t *testing.T) {
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo1.Name), &repoEditOption).
AddTokenAuth(token4)
MakeRequest(t, req, http.StatusForbidden)
// Test updating pull request settings without setting has_pull_requests
repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
url = fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{
DefaultDeleteBranchAfterMerge: &bTrue,
}).AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.True(t, repo.DefaultDeleteBranchAfterMerge)
// reset
req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{
DefaultDeleteBranchAfterMerge: &bFalse,
}).AddTokenAuth(token2)
_ = MakeRequest(t, req, http.StatusOK)
})
}
@@ -19,7 +19,6 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"github.com/stretchr/testify/assert"
@@ -53,8 +52,8 @@ func getCreateFileOptions() api.CreateFileOptions {
func normalizeFileContentResponseCommitTime(c *api.ContentsResponse) {
// decoded JSON response may contain different timezone from the one parsed by git commit
// so we need to normalize the time to UTC to make "assert.Equal" pass
c.LastCommitterDate = util.ToPointer(c.LastCommitterDate.UTC())
c.LastAuthorDate = util.ToPointer(c.LastAuthorDate.UTC())
c.LastCommitterDate = new(c.LastCommitterDate.UTC())
c.LastAuthorDate = new(c.LastAuthorDate.UTC())
}
type apiFileResponseInfo struct {
@@ -75,9 +74,9 @@ func getExpectedFileResponseForCreate(info apiFileResponseInfo) *api.FileRespons
Name: path.Base(info.treePath),
Path: info.treePath,
SHA: sha,
LastCommitSHA: util.ToPointer(info.lastCommitSHA),
LastCommitterDate: util.ToPointer(info.lastCommitterWhen),
LastAuthorDate: util.ToPointer(info.lastAuthorWhen),
LastCommitSHA: new(info.lastCommitSHA),
LastCommitterDate: new(info.lastCommitterWhen),
LastAuthorDate: new(info.lastAuthorWhen),
Size: 16,
Type: "file",
Encoding: &encoding,
@@ -20,21 +20,19 @@ import (
func getDeleteFileOptions() *api.DeleteFileOptions {
return &api.DeleteFileOptions{
FileOptionsWithSHA: api.FileOptionsWithSHA{
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "Removing the file new/file.txt",
Author: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
Committer: api.Identity{
Name: "Jane Doe",
Email: "janedoe@example.com",
},
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "Removing the file new/file.txt",
Author: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
Committer: api.Identity{
Name: "Jane Doe",
Email: "janedoe@example.com",
},
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
},
}
}
@@ -9,7 +9,6 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -25,8 +24,9 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
// Test with LFS
onGiteaRun(t, func(t *testing.T, u *url.URL) {
createLFSTestRepository(t, "repo-lfs-test")
httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteRepository)
doAPICreateRepository(httpContext, false, func(t *testing.T, repository api.Repository) {
t.Run("repo-lfs-test", func(t *testing.T) {
u.Path = httpContext.GitPath()
dstPath := t.TempDir()
@@ -41,7 +41,7 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0]
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs)
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo-lfs-test/media/"+lfs).AddTokenAuth(httpContext.Token)
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
assert.Equal(t, testFileSizeSmall, respLFS.Length)
})
@@ -5,6 +5,7 @@ package integration
import (
"context"
"errors"
"strings"
"testing"
@@ -19,6 +20,9 @@ import (
type createFileInBranchOptions struct {
OldBranch, NewBranch string
CommitMessage string
CommitterName string
CommitterEmail string
}
func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) *api.FilesResponse {
@@ -29,7 +33,17 @@ func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_mode
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) (*api.FilesResponse, error) {
ctx := context.TODO()
opts := &files_service.ChangeRepoFilesOptions{OldBranch: createOpts.OldBranch, NewBranch: createOpts.NewBranch}
opts := &files_service.ChangeRepoFilesOptions{
OldBranch: createOpts.OldBranch,
NewBranch: createOpts.NewBranch,
Message: createOpts.CommitMessage,
}
if createOpts.CommitterName != "" || createOpts.CommitterEmail != "" {
opts.Committer = &files_service.IdentityOptions{
GitUserName: createOpts.CommitterName,
GitUserEmail: createOpts.CommitterEmail,
}
}
for path, content := range files {
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
Operation: "create",
@@ -59,7 +73,7 @@ func deleteFileInBranch(user *user_model.User, repo *repo_model.Repository, tree
func createOrReplaceFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) error {
_, err := deleteFileInBranch(user, repo, treePath, branchName)
if err != nil && !files_service.IsErrRepoFileDoesNotExist(err) {
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
}
@@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"github.com/stretchr/testify/assert"
@@ -28,21 +27,19 @@ func getUpdateFileOptions() *api.UpdateFileOptions {
content := "This is updated text"
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
return &api.UpdateFileOptions{
FileOptionsWithSHA: api.FileOptionsWithSHA{
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "My update of new/file.txt",
Author: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
Committer: api.Identity{
Name: "Anne Doe",
Email: "annedoe@example.com",
},
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "My update of new/file.txt",
Author: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
Committer: api.Identity{
Name: "Anne Doe",
Email: "annedoe@example.com",
},
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
},
ContentBase64: contentEncoded,
}
@@ -61,9 +58,9 @@ func getExpectedFileResponseForUpdate(info apiFileResponseInfo) *api.FileRespons
Name: path.Base(info.treePath),
Path: info.treePath,
SHA: sha,
LastCommitSHA: util.ToPointer(info.lastCommitSHA),
LastCommitterDate: util.ToPointer(info.lastCommitterWhen),
LastAuthorDate: util.ToPointer(info.lastAuthorWhen),
LastCommitSHA: new(info.lastCommitSHA),
LastCommitterDate: new(info.lastCommitterWhen),
LastAuthorDate: new(info.lastAuthorWhen),
Type: "file",
Size: 20,
Encoding: &encoding,
@@ -180,6 +177,15 @@ func TestAPIUpdateFile(t *testing.T) {
assert.Equal(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
assert.Equal(t, updateFileOptions.Message+"\n", fileResponse.Commit.Message)
// Test updating a file without SHA (should create the file)
updateFileOptions = getUpdateFileOptions()
updateFileOptions.SHA = ""
req = NewRequestWithJSON(t, "PUT", "/api/v1/repos/user2/repo1/contents/update-create.txt", &updateFileOptions).AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, &fileResponse)
assert.Equal(t, "08bd14b2e2852529157324de9c226b3364e76136", fileResponse.Content.SHA)
assert.Equal(t, setting.AppURL+"user2/repo1/raw/branch/master/update-create.txt", *fileResponse.Content.DownloadURL)
// Test updating a file and renaming it
updateFileOptions = getUpdateFileOptions()
updateFileOptions.BranchName = repo1.DefaultBranch
@@ -161,9 +161,88 @@ func TestAPIChangeFiles(t *testing.T) {
assert.Equal(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL)
assert.Equal(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL)
assert.Nil(t, filesResponse.Files[2])
assert.Equal(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message)
// Test fails creating a file in a branch that already exists without force
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
changeFilesOptions.NewBranchName = "develop"
changeFilesOptions.ForcePush = false
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusUnprocessableEntity)
assert.Contains(t, resp.Body.String(), `"message":"branch already exists [name: develop]"`)
// Test succeeds creating a file in a branch that already exists with force
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
changeFilesOptions.NewBranchName = "develop"
changeFilesOptions.ForcePush = true
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, &filesResponse)
expectedCreateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/develop/new/file%d.txt", fileID)
expectedCreateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/develop/new/file%d.txt", fileID)
expectedUpdateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/develop/update/file%d.txt", fileID)
expectedUpdateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/develop/update/file%d.txt", fileID)
assert.Equal(t, expectedCreateSHA, filesResponse.Files[0].SHA)
assert.Equal(t, expectedCreateHTMLURL, *filesResponse.Files[0].HTMLURL)
assert.Equal(t, expectedCreateDownloadURL, *filesResponse.Files[0].DownloadURL)
assert.Equal(t, expectedUpdateSHA, filesResponse.Files[1].SHA)
assert.Equal(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL)
assert.Equal(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL)
assert.Nil(t, filesResponse.Files[2])
assert.Equal(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message)
// Test fails creating a file in a branch that already exists with force and branch protection enabled
protectionReq := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.BranchProtection{
RuleName: "develop",
BranchName: "develop",
Priority: 1,
EnablePush: true,
EnableForcePush: false,
}).AddTokenAuth(token2)
MakeRequest(t, protectionReq, http.StatusCreated)
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
changeFilesOptions.NewBranchName = "develop"
changeFilesOptions.ForcePush = true
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusForbidden)
assert.Contains(t, resp.Body.String(), `"message":"branch develop is protected from force push"`)
// Test updating a file and renaming it
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
@@ -17,7 +17,6 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/stretchr/testify/assert"
@@ -35,9 +34,9 @@ func getExpectedContentsListResponseForContents(ref, refType, lastCommitSHA stri
Name: path.Base(treePath),
Path: treePath,
SHA: sha,
LastCommitSHA: util.ToPointer(lastCommitSHA),
LastCommitterDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastAuthorDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastCommitSHA: new(lastCommitSHA),
LastCommitterDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastAuthorDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
Type: "file",
Size: 30,
URL: &selfURL,
@@ -80,7 +79,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
// Make a new branch in repo1
newBranch := "test_branch"
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, repo1.DefaultBranch, newBranch)
assert.NoError(t, err)
commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
@@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/stretchr/testify/assert"
@@ -34,17 +33,17 @@ func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string)
Name: treePath,
Path: treePath,
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
LastCommitSHA: util.ToPointer(lastCommitSHA),
LastCommitterDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastAuthorDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastCommitSHA: new(lastCommitSHA),
LastCommitterDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastAuthorDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
Type: "file",
Size: 30,
Encoding: util.ToPointer("base64"),
Content: util.ToPointer("IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"),
Encoding: new("base64"),
Content: new("IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"),
URL: &selfURL,
HTMLURL: &htmlURL,
GitURL: &gitURL,
DownloadURL: util.ToPointer(setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath),
DownloadURL: new(setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath),
Links: &api.FileLinksResponse{
Self: &selfURL,
GitURL: &gitURL,
@@ -61,7 +60,7 @@ func TestAPIGetContents(t *testing.T) {
})
}
func testAPIGetContents(t *testing.T, u *url.URL) {
func testAPIGetContents(t *testing.T, _ *url.URL) {
/*** SETUP ***/
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
@@ -85,7 +84,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
// Make a new branch in repo1
newBranch := "test_branch"
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, repo1.DefaultBranch, newBranch)
require.NoError(t, err)
commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
@@ -256,8 +255,8 @@ func testAPIGetContentsExt(t *testing.T) {
assert.Equal(t, "jpeg.jpg", respFile.Name)
assert.Nil(t, respFile.Encoding)
assert.Nil(t, respFile.Content)
assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize)
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
assert.Equal(t, new(int64(107)), respFile.LfsSize)
assert.Equal(t, new("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
})
t.Run("FileContents", func(t *testing.T) {
// by default, no file content or commit info is returned
@@ -296,7 +295,7 @@ func testAPIGetContentsExt(t *testing.T) {
assert.NotNil(t, respFile.Content)
assert.Nil(t, contentsResponse.FileContents.LastCommitSHA)
assert.Nil(t, contentsResponse.FileContents.LastCommitMessage)
assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize)
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
assert.Equal(t, new(int64(107)), respFile.LfsSize)
assert.Equal(t, new("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
})
}
@@ -12,10 +12,8 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -30,8 +28,8 @@ func TestAPIGitTags(t *testing.T) {
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
// Set up git config for the tagger
_ = gitcmd.NewCommand("config", "user.name").AddDynamicArguments(user.Name).Run(t.Context(), &gitcmd.RunOpts{Dir: repo.RepoPath()})
_ = gitcmd.NewCommand("config", "user.email").AddDynamicArguments(user.Email).Run(t.Context(), &gitcmd.RunOpts{Dir: repo.RepoPath()})
_ = gitrepo.GitConfigSet(t.Context(), repo, "user.name", user.Name)
_ = gitrepo.GitConfigSet(t.Context(), repo, "user.email", user.Email)
gitRepo, _ := gitrepo.OpenRepository(t.Context(), repo)
defer gitRepo.Close()
@@ -59,7 +57,7 @@ func TestAPIGitTags(t *testing.T) {
assert.Equal(t, aTagMessage+"\n", tag.Message)
assert.Equal(t, user.Name, tag.Tagger.Name)
assert.Equal(t, user.Email, tag.Tagger.Email)
assert.Equal(t, util.URLJoin(repo.APIURL(), "git/tags", aTag.ID.String()), tag.URL)
assert.Equal(t, repo.APIURL()+"/git/tags/"+aTag.ID.String(), tag.URL)
// Should NOT work for lightweight tags
badReq := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s", user.Name, repo.Name, commit.ID.String()).
@@ -34,6 +34,7 @@ func TestAPICreateHook(t *testing.T) {
"url": "http://example.com/",
},
AuthorizationHeader: "Bearer s3cr3t",
Name: " CI notifications ",
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
@@ -41,4 +42,54 @@ func TestAPICreateHook(t *testing.T) {
DecodeJSON(t, resp, &apiHook)
assert.Equal(t, "http://example.com/", apiHook.Config["url"])
assert.Equal(t, "Bearer s3cr3t", apiHook.AuthorizationHeader)
assert.Equal(t, "CI notifications", apiHook.Name)
newName := "Deploy hook"
patchReq := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d", owner.Name, repo.Name, apiHook.ID), api.EditHookOption{
Name: &newName,
}).AddTokenAuth(token)
patchResp := MakeRequest(t, patchReq, http.StatusOK)
var patched *api.Hook
DecodeJSON(t, patchResp, &patched)
assert.Equal(t, newName, patched.Name)
hooksURL := fmt.Sprintf("/api/v1/repos/%s/%s/hooks", owner.Name, repo.Name)
// Create with Name field omitted: Name should be ""
req2 := NewRequestWithJSON(t, "POST", hooksURL, api.CreateHookOption{
Type: "gitea",
Config: api.CreateHookOptionConfig{
"content_type": "json",
"url": "http://example.com/",
},
}).AddTokenAuth(token)
resp2 := MakeRequest(t, req2, http.StatusCreated)
var created *api.Hook
DecodeJSON(t, resp2, &created)
assert.Empty(t, created.Name)
hookURL := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d", owner.Name, repo.Name, created.ID)
// PATCH with Name set: existing Name must be updated
setName := "original"
setReq := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{
Name: &setName,
}).AddTokenAuth(token)
MakeRequest(t, setReq, http.StatusOK)
// PATCH without Name field: name must remain "original"
patchReq2 := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{}).AddTokenAuth(token)
patchResp2 := MakeRequest(t, patchReq2, http.StatusOK)
var notCleared *api.Hook
DecodeJSON(t, patchResp2, &notCleared)
assert.Equal(t, "original", notCleared.Name)
// PATCH with Name: "" explicitly: Name should be cleared to ""
clearReq := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{
Name: new(""),
}).AddTokenAuth(token)
clearResp := MakeRequest(t, clearReq, http.StatusOK)
var cleared *api.Hook
DecodeJSON(t, clearResp, &cleared)
assert.Empty(t, cleared.Name)
}
@@ -28,7 +28,6 @@ func TestRepoLanguages(t *testing.T) {
// Save new file to master branch
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
"_csrf": doc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": "test.go",
"content": "package main",
@@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPILFSNotStarted(t *testing.T) {
@@ -59,12 +60,12 @@ func TestAPILFSMediaType(t *testing.T) {
MakeRequest(t, req, http.StatusUnsupportedMediaType)
}
func createLFSTestRepository(t *testing.T, name string) *repo_model.Repository {
ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
func createLFSTestRepository(t *testing.T, repoName string) *repo_model.Repository {
ctx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
t.Run("CreateRepo", doAPICreateRepository(ctx, false))
repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", "lfs-"+name+"-repo")
assert.NoError(t, err)
repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", repoName)
require.NoError(t, err)
return repo
}
@@ -74,7 +75,7 @@ func TestAPILFSBatch(t *testing.T) {
setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "batch")
repo := createLFSTestRepository(t, "lfs-batch-repo")
content := []byte("dummy1")
oid := storeObjectInRepo(t, repo.ID, &content)
@@ -253,7 +254,7 @@ func TestAPILFSBatch(t *testing.T) {
assert.NoError(t, err)
assert.True(t, exist)
repo2 := createLFSTestRepository(t, "batch2")
repo2 := createLFSTestRepository(t, "lfs-batch2-repo")
content := []byte("dummy0")
storeObjectInRepo(t, repo2.ID, &content)
@@ -316,6 +317,7 @@ func TestAPILFSBatch(t *testing.T) {
ul := br.Objects[0].Actions["upload"]
assert.NotNil(t, ul)
assert.NotEmpty(t, ul.Href)
assert.Equal(t, "chunked", ul.Header["Transfer-Encoding"], "git-lfs client needs Transfer-Encoding to do chunked transfer")
assert.Contains(t, br.Objects[0].Actions, "verify")
vl := br.Objects[0].Actions["verify"]
assert.NotNil(t, vl)
@@ -329,7 +331,7 @@ func TestAPILFSUpload(t *testing.T) {
setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "upload")
repo := createLFSTestRepository(t, "lfs-upload-repo")
content := []byte("dummy3")
oid := storeObjectInRepo(t, repo.ID, &content)
@@ -433,7 +435,7 @@ func TestAPILFSVerify(t *testing.T) {
setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "verify")
repo := createLFSTestRepository(t, "lfs-verify-repo")
content := []byte("dummy3")
oid := storeObjectInRepo(t, repo.ID, &content)
@@ -43,7 +43,6 @@ func TestAPIRepoLicense(t *testing.T) {
// Save new file to master branch
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
"_csrf": doc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": "LICENSE",
"content": testLicenseContent,
@@ -671,6 +671,16 @@ func TestAPIGenerateRepo(t *testing.T) {
templateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 44})
assertGeneratedRepoIsUsable := func(t *testing.T, ownerName string, repo *api.Repository) {
t.Helper()
assert.NotEmpty(t, repo.DefaultBranch)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s", ownerName, repo.Name, repo.DefaultBranch).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
branch := DecodeJSON(t, resp, &api.Branch{})
assert.Equal(t, repo.DefaultBranch, branch.Name)
}
// user
repo := new(api.Repository)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
@@ -684,6 +694,7 @@ func TestAPIGenerateRepo(t *testing.T) {
DecodeJSON(t, resp, repo)
assert.Equal(t, "new-repo", repo.Name)
assertGeneratedRepoIsUsable(t, user.Name, repo)
// org
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
@@ -697,6 +708,7 @@ func TestAPIGenerateRepo(t *testing.T) {
DecodeJSON(t, resp, repo)
assert.Equal(t, "new-repo", repo.Name)
assertGeneratedRepoIsUsable(t, "org3", repo)
}
func TestAPIRepoGetReviewers(t *testing.T) {
@@ -502,11 +502,12 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model
}
unauthorizedLevel := auth_model.Write
if categoryIsRequired {
if minRequiredLevel == auth_model.Read {
switch minRequiredLevel {
case auth_model.Read:
unauthorizedLevel = auth_model.NoAccess
} else if minRequiredLevel == auth_model.Write {
case auth_model.Write:
unauthorizedLevel = auth_model.Read
} else {
default:
assert.FailNow(t, "Invalid test case", "Unknown access token scope level: %v", minRequiredLevel)
}
}
@@ -514,10 +515,10 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model
if unauthorizedLevel == auth_model.NoAccess {
continue
}
cateogoryUnauthorizedScopes := auth_model.GetRequiredScopes(
categoryUnauthorizedScopes := auth_model.GetRequiredScopes(
unauthorizedLevel,
category)
unauthorizedScopes = append(unauthorizedScopes, cateogoryUnauthorizedScopes...)
unauthorizedScopes = append(unauthorizedScopes, categoryUnauthorizedScopes...)
}
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, unauthorizedScopes)
@@ -15,6 +15,21 @@ import (
"github.com/stretchr/testify/assert"
)
func TestPermissionsAPI(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("TokenNeeded", testTokenNeeded)
t.Run("WithOwnerUser", testWithOwnerUser)
t.Run("CanWriteUser", testCanWriteUser)
t.Run("AdminUser", testAdminUser)
t.Run("AdminCanNotCreateRepo", testAdminCanNotCreateRepo)
t.Run("CanReadUser", testCanReadUser)
t.Run("UnknownUser", testUnknownUser)
t.Run("UnknownOrganization", testUnknownOrganization)
t.Run("HiddenMemberPermissionsForbidden", testHiddenMemberPermissionsForbidden)
t.Run("PrivateOrgPermissionsNotFound", testPrivateOrgPermissionsNotFound)
}
type apiUserOrgPermTestCase struct {
LoginUser string
User string
@@ -22,16 +37,12 @@ type apiUserOrgPermTestCase struct {
ExpectedOrganizationPermissions api.OrganizationPermissions
}
func TestTokenNeeded(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testTokenNeeded(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/org6/permissions")
MakeRequest(t, req, http.StatusUnauthorized)
}
func sampleTest(t *testing.T, auoptc apiUserOrgPermTestCase) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, auoptc.LoginUser)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser)
@@ -48,7 +59,7 @@ func sampleTest(t *testing.T, auoptc apiUserOrgPermTestCase) {
assert.Equal(t, auoptc.ExpectedOrganizationPermissions.CanCreateRepository, apiOP.CanCreateRepository)
}
func TestWithOwnerUser(t *testing.T) {
func testWithOwnerUser(t *testing.T) {
sampleTest(t, apiUserOrgPermTestCase{
LoginUser: "user2",
User: "user2",
@@ -63,7 +74,7 @@ func TestWithOwnerUser(t *testing.T) {
})
}
func TestCanWriteUser(t *testing.T) {
func testCanWriteUser(t *testing.T) {
sampleTest(t, apiUserOrgPermTestCase{
LoginUser: "user4",
User: "user4",
@@ -78,7 +89,7 @@ func TestCanWriteUser(t *testing.T) {
})
}
func TestAdminUser(t *testing.T) {
func testAdminUser(t *testing.T) {
sampleTest(t, apiUserOrgPermTestCase{
LoginUser: "user1",
User: "user28",
@@ -93,7 +104,7 @@ func TestAdminUser(t *testing.T) {
})
}
func TestAdminCanNotCreateRepo(t *testing.T) {
func testAdminCanNotCreateRepo(t *testing.T) {
sampleTest(t, apiUserOrgPermTestCase{
LoginUser: "user1",
User: "user28",
@@ -108,7 +119,7 @@ func TestAdminCanNotCreateRepo(t *testing.T) {
})
}
func TestCanReadUser(t *testing.T) {
func testCanReadUser(t *testing.T) {
sampleTest(t, apiUserOrgPermTestCase{
LoginUser: "user1",
User: "user24",
@@ -123,31 +134,62 @@ func TestCanReadUser(t *testing.T) {
})
}
func TestUnknowUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testUnknownUser(t *testing.T) {
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
req := NewRequest(t, "GET", "/api/v1/users/unknow/orgs/org25/permissions").
req := NewRequest(t, "GET", "/api/v1/users/unknown/orgs/org25/permissions").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusNotFound)
var apiError api.APIError
DecodeJSON(t, resp, &apiError)
assert.Equal(t, "user redirect does not exist [name: unknow]", apiError.Message)
assert.Equal(t, "user redirect does not exist [name: unknown]", apiError.Message)
}
func TestUnknowOrganization(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testUnknownOrganization(t *testing.T) {
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/unknow/permissions").
req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/unknown/permissions").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusNotFound)
var apiError api.APIError
DecodeJSON(t, resp, &apiError)
assert.Equal(t, "GetUserByName", apiError.Message)
}
func testHiddenMemberPermissionsForbidden(t *testing.T) {
session := loginUser(t, "user8")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
req := NewRequest(t, "GET", "/api/v1/users/user5/orgs/privated_org/permissions").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
adminSession := loginUser(t, "user1")
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
adminReq := NewRequest(t, "GET", "/api/v1/users/user5/orgs/privated_org/permissions").
AddTokenAuth(adminToken)
resp := MakeRequest(t, adminReq, http.StatusOK)
var apiOP api.OrganizationPermissions
DecodeJSON(t, resp, &apiOP)
assert.Equal(t, api.OrganizationPermissions{
IsOwner: false,
IsAdmin: false,
CanWrite: true,
CanRead: true,
CanCreateRepository: true,
}, apiOP)
}
func testPrivateOrgPermissionsNotFound(t *testing.T) {
session := loginUser(t, "user8")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
req := NewRequest(t, "GET", "/api/v1/users/user5/orgs/privated_org/permissions").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
@@ -155,3 +155,44 @@ func TestMyOrgs(t *testing.T) {
},
}, orgs)
}
func TestMyOrgsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
normalUsername := "user2"
token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/orgs").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var orgs []*api.Organization
DecodeJSON(t, resp, &orgs)
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org17"})
assert.Equal(t, []*api.Organization{
{
ID: 17,
Name: org17.Name,
UserName: org17.Name,
FullName: org17.FullName,
Email: org17.Email,
AvatarURL: org17.AvatarLink(t.Context()),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
{
ID: 3,
Name: org3.Name,
UserName: org3.Name,
FullName: org3.FullName,
Email: org3.Email,
AvatarURL: org3.AvatarLink(t.Context()),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
}, orgs)
}
@@ -0,0 +1,177 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/require"
)
func TestAPIPublicOnlySelfUserRoutes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user31"})
require.True(t, privateUser.Visibility.IsPrivate())
privateSession := loginUser(t, privateUser.Name)
privateReadUserToken := getTokenForLoggedInUser(t, privateSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeReadUser,
)
privateWriteUserToken := getTokenForLoggedInUser(t, privateSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeWriteUser,
)
t.Run("PrivateProfileForbidden", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
})
t.Run("PrivateSensitiveSelfRoutesForbidden", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/settings").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
hideEmail := true
settingsReq := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{
HideEmail: &hideEmail,
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, settingsReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/emails").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
emailReq := NewRequestWithJSON(t, "POST", "/api/v1/user/emails", &api.CreateEmailOption{
Emails: []string{"user31-public-only@example.com"},
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, emailReq, http.StatusForbidden)
keyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", api.CreateKeyOption{
Title: "public-only-private-key",
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, keyReq, http.StatusForbidden)
oauthReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &api.CreateOAuth2ApplicationOptions{
Name: "public-only-private-oauth-app",
RedirectURIs: []string{"https://example.com/callback"},
ConfidentialClient: true,
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, oauthReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_keys").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
gpgKeyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys", &api.CreateGPGKeyOption{
ArmoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\ncomment\n-----END PGP PUBLIC KEY BLOCK-----",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, gpgKeyReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_key_token").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
gpgVerifyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_key_verify", &api.VerifyGPGKeyOption{
KeyID: "deadbeef",
Signature: "invalid-signature",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, gpgVerifyReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/actions/variables").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/actions/secrets/PRIVATE_SECRET").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
variableReq := NewRequestWithJSON(t, "POST", "/api/v1/user/actions/variables/PRIVATE_VAR", api.CreateVariableOption{
Value: "private-value",
Description: "must stay private",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, variableReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "POST", "/api/v1/user/actions/runners/registration-token").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/hooks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
hookReq := NewRequestWithJSON(t, "POST", "/api/v1/user/hooks", api.CreateHookOption{
Type: "gitea",
Config: api.CreateHookOptionConfig{
"content_type": "json",
"url": "http://example.com/",
},
Name: "public-only-private-hook",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, hookReq, http.StatusForbidden)
avatarReq := NewRequestWithJSON(t, "POST", "/api/v1/user/avatar", &api.UpdateUserAvatarOption{
Image: "aGVsbG8=",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, avatarReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/avatar").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/times").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/stopwatches").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/teams").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/blocks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/blocks/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
})
t.Run("PublicRepoRoutesFilterAndRejectMutations", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
publicSession := loginUser(t, "user2")
fullWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopeWriteUser,
auth_model.AccessTokenScopeWriteRepository,
)
publicOnlyReadRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeReadUser,
auth_model.AccessTokenScopeReadRepository,
)
publicOnlyWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeWriteUser,
auth_model.AccessTokenScopeWriteRepository,
)
publicRepoName := "public-only-visible-self-repo"
privateRepoName := "public-only-hidden-self-repo"
resp := MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: publicRepoName,
Private: false,
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
publicRepo := DecodeJSON(t, resp, &api.Repository{})
require.Equal(t, "user2/"+publicRepoName, publicRepo.FullName)
resp = MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: privateRepoName,
Private: true,
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
privateRepo := DecodeJSON(t, resp, &api.Repository{})
require.Equal(t, "user2/"+privateRepoName, privateRepo.FullName)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/"+privateRepoName).AddTokenAuth(publicOnlyReadRepoToken), http.StatusNotFound)
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/repos").AddTokenAuth(publicOnlyReadRepoToken), http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
foundPublicRepo := false
for _, repo := range repos {
require.NotEqual(t, privateRepo.FullName, repo.FullName)
if repo.FullName == publicRepo.FullName {
foundPublicRepo = true
}
}
require.True(t, foundPublicRepo)
MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: "public-only-rejected-self-repo",
Private: false,
}).AddTokenAuth(publicOnlyWriteRepoToken), http.StatusForbidden)
})
}
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIStar(t *testing.T) {
@@ -155,3 +156,24 @@ func TestAPIStarDisabled(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
}
func TestAPIStarPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/starred").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
if assert.Len(t, repos, 1) {
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
req = NewRequest(t, "GET", "/api/v1/users/user2/starred").
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
repos = DecodeJSON(t, resp, []api.Repository{})
require.Len(t, repos, 1)
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
@@ -94,3 +94,28 @@ func TestAPIWatch(t *testing.T) {
MakeRequest(t, req, http.StatusNoContent)
})
}
func TestAPIWatchPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
writeRepoToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadUser)
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/repos/user2/repo1/subscription").AddTokenAuth(writeRepoToken), http.StatusOK)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/repos/user2/repo2/subscription").AddTokenAuth(writeRepoToken), http.StatusOK)
resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(publicOnlyToken), http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
for _, r := range repos {
assert.False(t, r.Private, "private repo %s leaked via /user/subscriptions", r.FullName)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user1/subscriptions").AddTokenAuth(publicOnlyToken), http.StatusOK)
repos = DecodeJSON(t, resp, []api.Repository{})
for _, r := range repos {
assert.False(t, r.Private, "private repo %s leaked via /users/{username}/subscriptions", r.FullName)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
}
@@ -14,6 +14,7 @@ import (
"strings"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test"
@@ -26,6 +27,15 @@ import (
"github.com/stretchr/testify/require"
)
type attachmentScopeCase struct {
name string
url string
readIssueStatus int
readRepoStatus int
publicOnlyIssueStatus int
publicOnlyRepoStatus int
}
func testGeneratePngBytes() []byte {
myImage := image.NewRGBA(image.Rect(0, 0, 32, 32))
var buff bytes.Buffer
@@ -33,15 +43,15 @@ func testGeneratePngBytes() []byte {
return buff.Bytes()
}
func testCreateIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL, filename string, content []byte, expectedStatus int) string {
return testCreateAttachment(t, session, csrf, repoURL, "issues", filename, content, expectedStatus)
func testCreateIssueAttachment(t *testing.T, session *TestSession, repoURL, filename string, content []byte, expectedStatus int) string {
return testCreateAttachment(t, session, repoURL, "issues", filename, content, expectedStatus)
}
func testCreateReleaseAttachment(t *testing.T, session *TestSession, csrf, repoURL, filename string, content []byte, expectedStatus int) string {
return testCreateAttachment(t, session, csrf, repoURL, "releases", filename, content, expectedStatus)
func testCreateReleaseAttachment(t *testing.T, session *TestSession, repoURL, filename string, content []byte, expectedStatus int) string {
return testCreateAttachment(t, session, repoURL, "releases", filename, content, expectedStatus)
}
func testCreateAttachment(t *testing.T, session *TestSession, csrf, repoURL, issueOrRelease, filename string, content []byte, expectedStatus int) string {
func testCreateAttachment(t *testing.T, session *TestSession, repoURL, issueOrRelease, filename string, content []byte, expectedStatus int) string {
body := &bytes.Buffer{}
// Setup multi-part
@@ -54,7 +64,6 @@ func testCreateAttachment(t *testing.T, session *TestSession, csrf, repoURL, iss
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", repoURL+"/"+issueOrRelease+"/attachments", body)
req.Header.Add("X-Csrf-Token", csrf)
req.Header.Add("Content-Type", writer.FormDataContentType())
resp := session.MakeRequest(t, req, expectedStatus)
@@ -66,15 +75,13 @@ func testCreateAttachment(t *testing.T, session *TestSession, csrf, repoURL, iss
return obj["uuid"]
}
func testDeleteIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL, uuid string, expectedStatus int) {
func testDeleteIssueAttachment(t *testing.T, session *TestSession, repoURL, uuid string, expectedStatus int) {
req := NewRequestWithValues(t, "POST", repoURL+"/issues/attachments/remove", map[string]string{"file": uuid})
req.Header.Add("X-Csrf-Token", csrf)
session.MakeRequest(t, req, expectedStatus)
}
func testDeleteReleaseAttachment(t *testing.T, session *TestSession, csrf, repoURL, uuid string, expectedStatus int) {
func testDeleteReleaseAttachment(t *testing.T, session *TestSession, repoURL, uuid string, expectedStatus int) {
req := NewRequestWithValues(t, "POST", repoURL+"/releases/attachments/remove", map[string]string{"file": uuid})
req.Header.Add("X-Csrf-Token", csrf)
session.MakeRequest(t, req, expectedStatus)
}
@@ -100,20 +107,20 @@ func testUploadAttachmentDeleteTemp(t *testing.T) {
defer web.RouteMock(route_web.RouterMockPointBeforeWebRoutes, func(resp http.ResponseWriter, req *http.Request) {
tmpFileCountDuringUpload = countTmpFile()
})()
_ = testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
_ = testCreateIssueAttachment(t, session, "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
assert.Equal(t, 1, tmpFileCountDuringUpload, "the temp file should exist when uploaded size exceeds the parse form's max memory")
assert.Equal(t, 0, countTmpFile(), "the temp file should be deleted after upload")
}
func testCreateAnonymousAttachment(t *testing.T) {
session := emptyTestSession(t)
testCreateIssueAttachment(t, session, GetAnonymousCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
testCreateIssueAttachment(t, session, "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
}
func testCreateUser2IssueAttachment(t *testing.T) {
const repoURL = "user2/repo1"
session := loginUser(t, "user2")
uuid := testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
uuid := testCreateIssueAttachment(t, session, repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
req := NewRequest(t, "GET", repoURL+"/issues/new")
resp := session.MakeRequest(t, req, http.StatusOK)
@@ -123,7 +130,6 @@ func testCreateUser2IssueAttachment(t *testing.T) {
assert.True(t, exists, "The template has changed")
postData := map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"title": "New Issue With Attachment",
"content": "some content",
"files": uuid,
@@ -187,24 +193,118 @@ func testDeleteAttachmentPermissions(t *testing.T) {
ownerSession := loginUser(t, "user2")
readonlySession := loginUser(t, "user5")
ownerCSRF := GetUserCSRFToken(t, ownerSession)
readonlyCSRF := GetUserCSRFToken(t, readonlySession)
issueFromOwner := testCreateIssueAttachment(t, ownerSession, repoURL, "owner-issue.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, readonlySession, repoURL, issueFromOwner, http.StatusForbidden)
issueFromOwner := testCreateIssueAttachment(t, ownerSession, ownerCSRF, repoURL, "owner-issue.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, readonlySession, readonlyCSRF, repoURL, issueFromOwner, http.StatusForbidden)
issueFromReader := testCreateIssueAttachment(t, readonlySession, repoURL, "reader-issue.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, ownerSession, repoURL, issueFromReader, http.StatusOK)
issueFromReader := testCreateIssueAttachment(t, readonlySession, readonlyCSRF, repoURL, "reader-issue.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, repoURL, issueFromReader, http.StatusOK)
testCreateReleaseAttachment(t, readonlySession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound)
testCreateReleaseAttachment(t, readonlySession, readonlyCSRF, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound)
crossRepoUUID := testCreateIssueAttachment(t, ownerSession, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
testDeleteIssueAttachment(t, ownerSession, repoURL, crossRepoUUID, http.StatusOK)
crossRepoUUID := testCreateIssueAttachment(t, ownerSession, ownerCSRF, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, repoURL, crossRepoUUID, http.StatusOK)
releaseUUID := testCreateReleaseAttachment(t, ownerSession, ownerCSRF, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK)
testDeleteReleaseAttachment(t, ownerSession, ownerCSRF, repoURL, releaseUUID, http.StatusOK)
releaseUUID := testCreateReleaseAttachment(t, ownerSession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK)
testDeleteReleaseAttachment(t, ownerSession, repoURL, releaseUUID, http.StatusOK)
// test deleting release attachment from another repo
testDeleteReleaseAttachment(t, ownerSession, ownerCSRF, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
testDeleteReleaseAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
}
func TestAttachmentTokenScopes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
for _, uuid := range []string{
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
} {
_, err := storage.Attachments.Save(repo_model.AttachmentRelativePath(uuid), strings.NewReader("hello world"), -1)
require.NoError(t, err)
}
readIssueToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadIssue)
readRepoToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
miscToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadMisc)
publicOnlyIssueToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
publicOnlyRepoToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
cases := []attachmentScopeCase{
{
name: "GlobalPublicIssueAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusOK,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "RepoPublicIssueAttachment",
url: "/user2/repo1/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusOK,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "GlobalPrivateIssueAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "RepoPrivateIssueAttachment",
url: "/user2/repo2/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "GlobalPublicReleaseAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusOK,
},
{
name: "RepoPublicReleaseAttachment",
url: "/user2/repo1/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusOK,
},
{
name: "GlobalPrivateReleaseAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "RepoPrivateReleaseAttachment",
url: "/user2/repo2/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(miscToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(readIssueToken), tc.readIssueStatus)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(readRepoToken), tc.readRepoStatus)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(publicOnlyIssueToken), tc.publicOnlyIssueStatus)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(publicOnlyRepoToken), tc.publicOnlyRepoStatus)
})
}
}

Some files were not shown because too many files have changed in this diff Show More